在编译过程中,琐碎的(没有效果的)代码什么时候会被删除
When will the trivial (code that has no effect) code gets removed in compilation process?
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
应该是一个良好的开端。
- 什么时候调用组成单元对象的析构函数
- 什么时候在C++中返回常量引用是个好主意
- 什么时候调用析构函数
- boost odeint什么时候真正调用观测者
- 编译器对数组声明大小的计算。什么时候发生?
- 什么时候最好在子进程中使用 CPU 或 I/O 密集型代码 [ C++ ]
- 您应该在什么时候创建自己的异常类型
- 我什么时候会默认(而不是删除)基类中的复制和移动操作
- 什么时候可以使用常量装饰调用我的重载函数?
- unordered_map什么时候返回 -1?
- QCoreApplication什么时候有效?
- sizeof(size_t) 和 sizeof(ptrdiff_t) 什么时候会有所不同?
- 什么时候用指针调用C++类构造函数
- 为什么我的代码说"Yes"什么时候应该说"No"?
- gcc 什么时候编译未使用的模板代码?
- 在编译过程中,琐碎的(没有效果的)代码什么时候会被删除
- 我们什么时候应该封装代码才能成为"Class"?
- 宏什么时候能让代码比函数更漂亮
- 我们什么时候在使用Connector/c++的代码中释放对象?
- 当我不输出变量时,代码中断;我什么时候工作。