Когда в процессе компиляции будет удален тривиальный код (код, который не имеет эффекта)?

volatile int num = 0;
num = num + 10;

Вышеупомянутый код С++, похоже, создает следующий код в сборке intel:

mov DWORD PTR [rbp-4], 0
mov eax, DWORD PTR [rbp-4]
add eax, 10
mov DWORD PTR [rbp-4], eax

Если я сменил код на С++ на

volatile int num = 0;
num = num + 0;

почему компилятор не производит код сборки как:

mov DWORD PTR [rbp-4], 0
mov eax, DWORD PTR [rbp-4]
add eax, 0
mov DWORD PTR [rbp-4], eax

gcc7.2 -O0 оставляет вне add eax, 0, но все остальные инструкции одинаковы (Godbolt).

В какой части процесса компиляции этот трехмерный код удаляется. Есть ли какой-либо флаг компилятора, который заставит компилятор GCC не выполнять такие оптимизации.

Ответ 1

clang будет выделять add eax, 0 в -O0, но ни один из gcc, ICC и MSVC не будет. См. Ниже.


gcc -O0 не означает "без оптимизации". gcc не имеет режима "braindead literal translation", где он пытается транслитерировать каждый компонент каждого выражения C непосредственно в инструкцию asm.

GCC -O0 не является полностью не оптимизированным. Он предназначен для "компиляции" и делает отладку ожидаемыми результатами (даже если вы изменяете переменные C с помощью отладчика или переходите к другой строке внутри этой функции). Таким образом, он разливает/перезагружает все вокруг каждого оператора C, предполагая, что память может быть асинхронно модифицирована отладчиком, остановленным до такого блока. (Интересный пример последствий и более подробное объяснение: Почему целочисленное деление на -1 (отрицательное) приводит к FPE?)


Для gcc -O0 не требуется большой спрос даже на более медленный код (например, забыв, что 0 является аддитивным идентификатором), поэтому никто не реализовал для этого вариант. И это может даже сделать gcc медленнее, если это поведение было необязательным. (Или, может быть, есть такой вариант, но он по умолчанию даже в -O0, потому что он быстрый, не повредит отладку и полезен. Обычно людям нравится, когда их отладочные сборки работают достаточно быстро, чтобы их можно было использовать, особенно для больших или в режиме реального времени.)

Как @Basile Starynkevitch объясняет в Отключить все опции оптимизации в GCC, gcc всегда трансформирует свои внутренние представления на пути к созданию исполняемого файла. Просто все это приводит к некоторым видам оптимизаций.

Например, даже при -O0, gcc "делить на константный" алгоритм использует мультипликативный с фиксированной точкой обратный или сдвиг (для степеней 2) вместо инструкции idiv. Но clang -O0 будет использовать idiv для x /= 2.


Clang -O0 тоже оптимизирует меньше gcc в этом случае:

void foo(void) {
    volatile int num = 0;
    num = num + 0;
}

выход asm на Godbolt для x86-64

    push    rbp
    mov     rbp, rsp

    # your asm block from the question, but with 0 instead of 10
    mov     dword ptr [rbp - 4], 0
    mov     eax, dword ptr [rbp - 4]
    add     eax, 0
    mov     dword ptr [rbp - 4], eax

    pop     rbp
    ret

Как вы говорите, gcc не использует бесполезный add eax,0. ICC17 хранит/перезагружает несколько раз. MSVC обычно является чрезвычайно буквальным в режиме отладки, но даже он избегает испускания add eax,0.

Clang также является единственным из 4 компиляторов x86 на Godbolt, который будет использовать idiv для return x/2;. Остальные все SAR + CMOV или что-то другое, чтобы реализовать C подписанную семантику деления.

Ответ 2

В соответствии с правилом "как будто" на С++ реализация свободно разрешается делать все, что захочет, при условии, что наблюдаемое поведение соответствует стандарту. В частности, в C++17, 4.6/1 (как один пример):

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

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

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

Что касается управления gcc, моим первым предложением было бы отключить всю оптимизацию, используя флаг -O0. Вы можете получить более тонкое управление с помощью различных опций -f<blah>, но -O0 должно быть хорошим началом.