在编译过程中,琐碎的(没有效果的)代码什么时候会被删除

When will the trivial (code that has no effect) code gets removed in compilation process?

本文关键字:代码 什么时候 删除 有效果 过程中 编译      更新时间:2023-10-16
volatile int num = 0;
num = num + 10;

上面的C++代码似乎在英特尔汇编中产生了以下代码:

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

如果我将C++代码更改为

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编译器不进行此类优化。

clang将在-O0发射add eax, 0,但gcc、ICC和MSVC都不会。请参见下文。


gcc -O0并不意味着"没有优化"。gcc没有"死脑筋的直译"模式,它试图将每个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;
}

x86-64 Godbolt上的asm输出

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也是Godbolt上4个x86编译器中唯一一个将idiv用于return x/2;的编译器。其他的都是SAR+CMOV或其他什么来实现C的符号划分语义。

根据;就好像";在C++中,只要可观察的行为符合标准,实现就可以自由地做任何它想做的事情。具体地,在C++17, 4.6/1(作为一个示例)中:

。。。需要一致的实现来模拟(仅)抽象机器的可观察行为,如下所述。

该条款有时被称为";就好像";规则,因为只要结果是遵守了该要求,只要可以从程序的可观察行为中确定,实现就可以自由地忽略本国际标准的任何要求。

例如,如果一个实际实现可以推断出它的值没有被使用,并且没有产生影响程序可观察行为的副作用,那么它就不需要评估表达式的一部分。

关于如何控制gcc,我的第一个建议是使用-O0标志关闭所有优化。通过使用各种-f<blah>选项,您可以获得更精细的控制,但-O0应该是一个良好的开端。