在编译过程中,什么时候删除这些无关紧要的代码(没有效果的代码)? < >上面的C++代码似乎在英特尔汇编中产生以下代码: volatile int num = 0; num = num + 10; P>按照C++中的“IF”规则,一个实现可以自由地做任何它想要的,只要可观察的行为符合标准。具体来说,在C++17,4.6/1中(作为一个示例):
为什么编译器不会生成汇编代码:在编译过程中,什么时候删除这些无关紧要的代码(没有效果的代码)? < >上面的C++代码似乎在英特尔汇编中产生以下代码: volatile int num = 0; num = num + 10; P>按照C++中的“IF”规则,一个实现可以自由地做任何它想要的,只要可观察的行为符合标准。具体来说,在C++17,4.6/1中(作为一个示例):,c++,gcc,assembly,x86,C++,Gcc,Assembly,X86,为什么编译器不会生成汇编代码: volatile int num = 0; num = num + 0; 这种琐碎的代码在编译过程的哪一部分被删除。是否有任何编译器标志会使GCC编译器不进行此类优化 > P>按照C++中的“IF”规则,一个实现可以自由地做任何它想要的,只要可观察的行为符合标准。具体来说,在C++17,4.6/1中(作为一个示例): 。。。一致性实现需要模拟(仅)抽象机器的可观察行为,如下所述 这一规定有时被称为“仿佛”规则,因为实施可以自由地忽略本国际标准的任何要求,只要其
volatile int num = 0;
num = num + 0;
这种琐碎的代码在编译过程的哪一部分被删除。是否有任何编译器标志会使GCC编译器不进行此类优化 > P>按照C++中的“IF”规则,一个实现可以自由地做任何它想要的,只要可观察的行为符合标准。具体来说,在C++17,4.6/1中(作为一个示例):
。。。一致性实现需要模拟(仅)抽象机器的可观察行为,如下所述
这一规定有时被称为“仿佛”规则,因为实施可以自由地忽略本国际标准的任何要求,只要其结果是符合要求,只要可以从程序的可观察行为中确定
例如,如果一个实际的实现可以推断出它的值没有被使用,并且不会产生影响程序的可观察行为的副作用,那么它就不需要计算表达式的一部分
至于如何控制gcc
,我的第一个建议是使用-O0
标志关闭所有优化。您可以通过使用各种-f
选项获得更精细的控制,但-O0
应该是一个好的开始。叮当声将在-O0
处发出添加eax、0
,但gcc、ICC和MSVC都不会发出。见下文
gcc-O0
并不意味着“没有优化”。gcc没有一种“脑死的直译”模式,它尝试将每个C表达式的每个组件直接音译到asm指令
GCC的-O0
并不打算完全未优化。它的目的是“快速编译”,并使调试产生预期的结果(即使使用调试器修改C变量,或跳转到函数中的另一行)。因此,它会溢出/重新加载每个C语句周围的所有内容,假设在此类块之前停止的调试器可以异步修改内存。(有趣的后果示例和更详细的解释:)
对于gcc-O0
来说,没有太多的需求来生成更慢的代码(例如,忘记了0
是加法标识),因此没有人实现这一选项。如果这种行为是可选的,它甚至可能会使gcc变慢。(或者可能有这样一个选项,但它在默认情况下即使在-O0
,也会打开,因为它很快,不会影响调试,而且很有用。通常,当调试构建运行得足够快,可以使用时,人们会喜欢它,特别是对于大型或实时项目。)
正如@Basile Starynkevitch在中所解释的,gcc总是在生成可执行文件的过程中通过其内部表示进行转换。只要这样做,就会产生一些优化
例如,gcc的“除以常数”算法使用or移位(2的幂)代替idiv
指令。但是clang-O0
将使用idiv
表示x/=2
在这种情况下,Clang的-O0
优化程度也低于gcc:
mov DWORD PTR [rbp-4], 0
mov eax, DWORD PTR [rbp-4]
add eax, 0
mov DWORD PTR [rbp-4], eax
正如您所说,gcc省略了无用的addeax,0
。ICC17多次存储/重新加载。在调试模式下,MSVC通常非常直白,但即使它也避免发出addeax,0
Clang也是Godbolt上4个x86编译器中唯一一个将idiv
用于返回x/2的编译器代码>。其他的都是SAR+CMOV或其他实现C符号划分语义的工具。@vu1p3n0x:我想kris是在问为什么gcc只加载num
并再次存储它,而不使用addeax,0
指令(因为gcc即使在-O0
上也会优化该部分)。相关的,并回答了以下问题:。gcc没有“完全愚蠢”的模式。在生成可执行文件的过程中,它总是通过其内部表示进行转换。有人猜测,它从未“被删除”,但实际上从未被添加。编译器不必花那么多功夫来实现x+0
不需要代码。如果您只是想在机器代码中保留立即字节以供进一步修补,那么不清楚为什么不使用10
版本,并在默认情况下将其修补为0
,或者根据需要修补为其他-128..+127
值。(还请注意,如果您的目标是二进制修补,您可能希望使用一些32b或64b常量来获得足够大的立即数编码的add
,因为-128..+127
范围中的值将只使用imm8编码(值为单字节)…至少对于任何通用汇编程序(由gcc使用))。有趣的是,您没有明确回答“在编译过程的哪个部分”的问题,尽管隐式地说,当解析的文本转换为“gcc用于生成最终机器代码的内容”的内部表示时,它显然是作为内部步骤之一发生的。甚至不能命名它,诱惑使用“C++抽象机”,但是GCC所使用的内部表示肯定比纯C++抽象机具有更多的属性和特征,并且我不知道GCC开发者是否有它的名字。@ Po7G:GCC的内部表示之一称为GIMPLE。另一个是寄存器转移语言(RTL),我想。IDK什么时候发生的。你可以在不同的步骤中甩掉内部代表,但我不是这方面的专家。
void foo(void) {
volatile int num = 0;
num = num + 0;
}
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