Подтвердить что ты не робот

Как вести оптимизацию GCC на основе утверждений без затрат времени исполнения?

У меня есть макрос, используемый во всем моем коде, который в режиме отладки:

#define contract(condition) \
    if (!(condition)) \
        throw exception("a contract has been violated");

... но в режиме деблокирования:

#define contract(condition) \
    if (!(condition)) \
        __builtin_unreachable();

Что это делает над assert(), так это то, что в сборках релизов компилятор может сильно оптимизировать код благодаря распространению UB.

Например, тестирование с помощью следующего кода:

int foo(int i) {
    contract(i == 1);
    return i;
}

// ...

foo(0);

... генерирует исключение в режиме отладки, но производит сборку для безусловного return 1; в режиме выпуска:

foo(int):
        mov     eax, 1
        ret

Условие и все, что от него зависело, были оптимизированы.

Моя проблема возникает с более сложными условиями. Когда компилятор не может доказать, что условие не имеет побочного эффекта, оно не оптимизирует его, что является штрафом за рутину по сравнению с использованием контракта.

Есть ли способ выразить, что условие в контракте не имеет побочного эффекта, так что оно всегда оптимизировано?

4b9b3361

Ответ 1

Невозможно принудительно оптимизировать код, если он был мертвым кодом, потому что GCC всегда должен жаловаться на стандарт.

С другой стороны, выражение можно проверить, чтобы не иметь побочных эффектов, используя атрибут error, который будет показывать ошибку всякий раз, когда вызов функции не может быть оптимизирован.

Пример макроса, который проверяет, что оптимизировано и распространяется UB:

#define _contract(condition) \
    {
        ([&]() __attribute__ ((noinline,error ("contract could not be optimized out"))) {
            if (condition) {} // using the condition in if seem to hide `unused` warnings.
        }());
        if (!(condition))
            __builtin_unreachable();
    }

Атрибут ошибки не работает без оптимизации (поэтому этот макрос может использоваться только для компиляции режима release\оптимизации). Обратите внимание, что при привязке отображается ошибка, указывающая на то, что контракт имеет побочные эффекты.

Тест, который показывает ошибку с неоптимизированным контрактом.

Тест, который оптимизирует контракт, но имеет ли распространение UB с ним.

Ответ 2

Итак, это не ответ, а некоторые интересные результаты, которые могли бы привести где-то.

В итоге у меня появился следующий код игрушки:

#define contract(x) \
    if (![&]() __attribute__((pure, noinline)) { return (x); }()) \
        __builtin_unreachable();

bool noSideEffect(int i);

int foo(int i) {
    contract(noSideEffect(i));

    contract(i == 1);

    return i;
}

Вы можете следовать за собой дома;)

noSideEffect - это функция, которая, как мы знаем, не имеет побочных эффектов, но компилятор не делает.
Это было так:

  • GCC имеет __attribute__((pure)), чтобы отметить функцию как не имеющую побочного эффекта.

  • Квалификационный noSideEffect с атрибутом pure полностью удаляет вызов функции. Ницца!

  • Но мы не можем изменить объявление noSideEffect. Итак, как насчет обертывания его вызова внутри самой функции pure? И поскольку мы пытаемся сделать автономный макрос, лямбда хорошо звучит.

  • Удивительно, но это не сработает... если мы не добавим noinline к лямбда! Я полагаю, что оптимизатор сначала встраивает лямбда, теряя атрибут pure на пути, прежде чем рассматривать возможность оптимизации вызова noSideEffect. С помощью noinline, несколько интуитивно понятным способом, оптимизатор способен стереть все. Отлично!

  • Однако теперь есть два вопроса: с атрибутом noinline компилятор создает тело для каждой лямбда, даже если они никогда не используются. Мех - компоновщик, вероятно, сможет выбросить их в любом случае.
    Но что еще более важно... Мы фактически потеряли оптимизацию, которую включил __builtin_unreachable(): (

Чтобы подвести итог, вы можете удалить или вернуть обратно noinline в фрагмент выше, и в итоге получится один из следующих результатов:

С noinline

; Unused code
foo(int)::{lambda()#2}::operator()() const:
        mov     rax, QWORD PTR [rdi]
        cmp     DWORD PTR [rax], 1
        sete    al
        ret
foo(int)::{lambda()#1}::operator()() const:
        mov     rax, QWORD PTR [rdi]
        mov     edi, DWORD PTR [rax]
        jmp     noSideEffect(int)

; No function call, but the access to i is performed
foo(int):
        mov     eax, edi
        ret

Без noinline

; No unused code
; Access to i has been optimized out,
; but the call to `noSideEffect` has been kept.
foo(int):
        sub     rsp, 8
        call    noSideEffect(int)
        mov     eax, 1
        add     rsp, 8
        ret

Ответ 3

Есть ли способ выразить, что условие в контракте не имеет побочного эффекта, так что оно всегда оптимизировано?

Неверно.

Известно, что вы не можете взять большую коллекцию утверждений, превратить их в предположения (через __builtin_unreachable) и ожидать хороших результатов (например, Утверждения пессимистичны, Предположения оптимистичны Джона Реджера).

Некоторые подсказки:

  • CLANG, уже имея встроенный __builtin_unreachable, ввел __ builtin_assume именно для этой цели.

  • N4425 - Обобщенные динамические предположения (*) отмечает, что:

    GCC явно не предоставляет общую предположение, но общие предположения могут быть закодированы с использованием комбинации управляющего потока и __builtin_unreachable внутреннего

    ...

    В существующих реализациях, которые предоставляют общие допущения, используется некоторое ключевое слово в пространстве с сохраненным идентификатором (__assume, __builtin_assume и т.д.). Поскольку аргумент выражения не оценивается (побочные эффекты отбрасываются), указание этого в терминах специальной библиотечной функции (например, std::assume) кажется трудным.

  • Библиотека поддержки рекомендаций (GSL, размещенная корпорацией Майкрософт, но никак не специфичная для Microsoft) имеет "просто" этот код:

    #ifdef _MSC_VER
    #define GSL_ASSUME(cond) __assume(cond)
    #elif defined(__clang__)
    #define GSL_ASSUME(cond) __builtin_assume(cond)
    #elif defined(__GNUC__)
    #define GSL_ASSUME(cond) ((cond) ? static_cast<void>(0) : __builtin_unreachable())
    #else
    #define GSL_ASSUME(cond) static_cast<void>(!!(cond))
    #endif
    

    и отмечает, что:

    // GSL_ASSUME(cond)
    //
    // Tell the optimizer that the predicate cond must hold. It is unspecified
    // whether or not cond is actually evaluated.
    

<суб > *) Бумага отклонено: Руководство EWG предназначалось для обеспечения функциональности в предлагаемых договорных условиях. Суб >