为什么clang使用-O0(对于这个简单的浮点和)生成低效的asm?
我正在llvm clang Apple llvm 8.0.0版(clang-800.0.42.1)上反汇编此代码: 我使用no-O规范进行编译,但我也尝试使用-O0(给出相同的结果)和-O2(实际计算值并存储预计算的值) 由此产生的拆卸如下(我拆下了不相关的零件)为什么clang使用-O0(对于这个简单的浮点和)生成低效的asm?,c,assembly,x86-64,compiler-optimization,llvm-codegen,C,Assembly,X86 64,Compiler Optimization,Llvm Codegen,我正在llvm clang Apple llvm 8.0.0版(clang-800.0.42.1)上反汇编此代码: 我使用no-O规范进行编译,但我也尝试使用-O0(给出相同的结果)和-O2(实际计算值并存储预计算的值) 由此产生的拆卸如下(我拆下了不相关的零件) ->0x10000F30:pushq%rbp 0x10000F31:movq%rsp,%rbp 0x10000F34:subq$0x10,%rsp 0x10000F38:leaq 0x6d(%rip),%rdi 0x10000F3F:
->0x10000F30:pushq%rbp
0x10000F31:movq%rsp,%rbp
0x10000F34:subq$0x10,%rsp
0x10000F38:leaq 0x6d(%rip),%rdi
0x10000F3F:movss 0x5d(%rip),%xmm0
0x10000F47:MOVS 0x59(%rip),%xmm1
0x10000F4F:movss%xmm1,-0x4(%rbp)
0x10000F54:movss%xmm0,-0x8(%rbp)
0x10000F59:movss-0x4(%rbp),%xmm0
0x100000f5e:地址-0x8(%rbp),%xmm0
0x100000f63:movss%xmm0,-0xc(%rbp)
...
显然,它正在做以下工作:
-O0
(未优化)是默认值。它告诉编译器您希望它快速编译(短编译时间),而不是花费额外的编译时间来生成高效的代码
(-O0
实际上并不是没有优化;例如,如果(1==2){块。尤其是gcc,与大多数其他编译器相比,它仍然会在-O0
处使用乘法逆进行除法,因为它在最终发出asm之前,仍然会通过逻辑的多个内部表示来转换C源代码。)
另外,“编译器总是正确的”甚至在-O3
上也是一种夸张。编译器在大规模上非常优秀,但在单循环中,遗漏的小优化仍然很常见。循环中的指令(或UOP)通常影响很小,但被浪费,这会占用无序执行重新排序窗口中的空间,并且在与另一个线程共享内核时,对超线程的友好性会降低。有关在简单特定情况下击败编译器的更多信息,请参阅
更重要的是,
-O0
还意味着处理与volatile
类似的所有变量,以便进行一致的调试。i、 因此,您可以设置断点或单步并修改C变量的值,然后继续执行,并使程序按照您希望在C抽象机上运行的C源代码的方式工作。因此编译器不能进行任何常量传播或值范围简化。(例如,已知为非负的整数可以使用它简化事情,或者使某些if条件始终为真或始终为假。)
(这并不像volatile
那么糟糕:在一条语句中多次引用同一个变量并不总是导致多次加载;在-O0
中,编译器仍然会在单个表达式中进行一些优化。)
编译器必须专门针对-O0
进行反优化,方法是在语句之间存储/重新加载所有变量到它们的内存地址。(C和C++中,每个变量都有一个地址,除非它被声明为(现在过时)<代码>登记器< /Cord>关键字,并且从未有过地址。根据其他变量的IAS规则,优化地址是可能的,但是在<代码> -O0<代码> < <
不幸的是,调试信息格式无法通过寄存器跟踪变量的位置,因此如果没有这种缓慢而愚蠢的代码生成,就不可能进行完全一致的调试
如果不需要,可以使用-Og
进行编译以进行轻度优化,而无需进行一致调试所需的反优化。GCC手册建议在通常的编辑/编译/运行周期中使用它,但在调试时,您将对许多具有自动存储功能的局部变量进行“优化”。全局变量和函数参数通常仍然有它们的实际值,至少在函数边界上是这样
更糟糕的是,
-O0
使得即使使用GDB的jump
命令在不同的源代码行继续执行,代码仍然可以工作。因此,每个C语句都必须编译成完全独立的指令块。()
for()
循环无法转换为,以及其他限制
基于以上所有原因,(微观)基准测试未优化的代码是一种巨大的时间浪费;结果取决于如何编写源代码的愚蠢细节,而这些细节在使用常规优化编译时并不重要-O0
与-O3
性能之间没有线性关系;有些代码的速度比其他代码快得多
-O0
代码中的瓶颈通常不同于-O3
——通常在内存中保存的循环计数器上,从而创建一个~6个循环的循环携带依赖链。这可以在编译器生成的asm中创建有趣的效果,比如(从asm的角度来看,这很有趣,但对于C来说不是。)
“否则我的基准优化了”不是查看-O0
代码性能的有效理由。
有关调整-O0
的兔子洞的示例和更多详细信息,请参见
获取有趣的编译器输出 如果您想了解编译器如何添加2个变量,write
int main() {
float a=0.151234;
float b=0.2;
float c=a+b;
printf("%f", c);
}
-> 0x100000f30 <+0>: pushq %rbp
0x100000f31 <+1>: movq %rsp, %rbp
0x100000f34 <+4>: subq $0x10, %rsp
0x100000f38 <+8>: leaq 0x6d(%rip), %rdi
0x100000f3f <+15>: movss 0x5d(%rip), %xmm0
0x100000f47 <+23>: movss 0x59(%rip), %xmm1
0x100000f4f <+31>: movss %xmm1, -0x4(%rbp)
0x100000f54 <+36>: movss %xmm0, -0x8(%rbp)
0x100000f59 <+41>: movss -0x4(%rbp), %xmm0
0x100000f5e <+46>: addss -0x8(%rbp), %xmm0
0x100000f63 <+51>: movss %xmm0, -0xc(%rbp)
...
float foo(float a, float b) {
float c=a+b;
return c;
}
addss xmm0, xmm1
ret
# clang7.0 -O0 also on Godbolt
foo:
push rbp
mov rbp, rsp # make a traditional stack frame
movss DWORD PTR [rbp-20], xmm0 # spill the register args
movss DWORD PTR [rbp-24], xmm1 # into the red zone (below RSP)
movss xmm0, DWORD PTR [rbp-20] # a
addss xmm0, DWORD PTR [rbp-24] # +b
movss DWORD PTR [rbp-4], xmm0 # store c
movss xmm0, DWORD PTR [rbp-4] # return 0
pop rbp # epilogue
ret