我正在llvm clang Apple LLVM版本8.0.0(clang-800.0.42.1)上反汇编以下代码:
int main() {
float a=0.151234;
float b=0.2;
float c=a+b;
printf("%f", c);
}
我没有使用-O规范进行编译,但是我也尝试了使用-O0(给出相同的值)和-O2(实际上是计算值并将其存储为预先计算的)
产生的拆卸如下(我删除了不相关的零件)
-> 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)
...
显然,它正在执行以下操作:
我发现效率低下是因为:
鉴于编译器总是正确的,为什么选择这种策略?
最佳答案
-O0
(未优化)是默认的。它告诉编译器您希望它快速编译(较短的编译时间),而不是花费额外的时间来编译高效的代码。
(-O0
并不是真正的优化;例如,gcc仍将消除if(1 == 2){ }
块内的代码。尤其是gcc比大多数其他编译器仍能在-O0
上使用乘除法进行除法,因为它仍然通过C的多个内部表示来转换C源最终发出asm之前的逻辑。)
另外,即使在-O3
上,“编译器总是正确的”实在是过分夸张。编译器在大规模方面非常出色,但是在单个循环中仍然普遍存在次要的遗漏优化。通常,其影响非常小,但是循环中的指令(或uops)浪费了,可能会浪费无序的执行重新排序窗口中的空间,并且在与另一个线程共享内核时对超线程的友好程度也会降低。有关在简单的特定情况下击败编译器的更多信息,请参见C++ code for testing the Collatz conjecture faster than hand-written assembly - why?。
更为重要的是,-O0
还意味着将所有类似于volatile
的变量都处理为一致调试。即,您可以设置一个断点或单个步骤并修改C变量的值,然后继续执行并使程序按照您希望在C抽象机上运行C源的方式工作。因此,编译器无法进行任何常量传播或值范围简化。 (例如,一个已知为非负的整数可以简化使用该整数的过程,或者在条件始终为true或false时进行计算。)
(它不像volatile
那样糟糕:在一个语句中多次引用同一变量并不总是导致多次加载;在-O0
中,编译器仍会在单个表达式中进行某种程度的优化。)
编译器必须通过在语句之间将所有变量存储/重新加载到其内存地址来专门针对-O0
进行反优化。 (在C和C++中,每个变量都有一个地址,除非已使用register
关键字声明(现在已过时)并且从未使用过它的地址。根据其他变量的按条件规则优化地址是可行的,但不是尚未完成-O0
)
不幸的是,调试信息格式无法通过寄存器跟踪变量的位置,因此,如果没有这种缓慢而愚蠢的代码生成器,就不可能进行完全一致的调试。
如果不需要它,则可以使用-Og
进行灯光优化,而无需进行一致调试所需的反优化。 GCC手册建议在通常的编辑/编译/运行周期中使用它,但是在调试时,您会“自动优化”许多具有自动存储功能的局部变量。全局变量和函数参数通常至少在函数边界处仍具有其实际值。
更糟糕的是,即使您使用GDB的-O0
命令在另一个源代码行继续执行,jump
仍使代码仍然有效。因此,每个C语句都必须编译成一个完全独立的指令块。 (Is it possible to "jump"/"skip" in GDB debugger?)for()
循环不能转换为idiomatic (for asm) do{}while()
loops和其他限制。
由于上述所有原因,(微)基准测试未优化的代码会浪费大量时间;结果取决于您如何编写源代码的愚蠢细节,而这些细节在使用常规优化进行编译时并不重要。 -O0
与-O3
的性能不是线性相关的;某些代码将比其他代码更快。-O0
代码中的瓶颈通常与-O3
不同-经常在保存在内存中的循环计数器上,从而创建一个〜6循环的循环承载依赖链。这可以在编译器生成的asm中创建有趣的效果,例如Adding a redundant assignment speeds up code when compiled without optimization(从asm角度来看很有趣,但对于C而言不是。)
“否则,我的基准将被优化”对于查看-O0
代码的性能不是有效的理由。
有关示例,请参见C loop optimization help for final assignment,以及有关调整-O0
的兔子洞的更多详细信息。
获得有趣的编译器输出
如果要查看编译器如何添加2个变量,则编写一个接受args并返回值的函数。请记住,您只想查看asm,而不是运行它,因此对于应为运行时变量的任何内容,您都不需要main
或任何数字文字值。
有关此问题的更多信息,请参见How to remove "noise" from GCC/clang assembly output?。
float foo(float a, float b) {
float c=a+b;
return c;
}
用
clang -O3
(on the Godbolt compiler explorer)编译到预期的 addss xmm0, xmm1
ret
但是使用
-O0
会将args溢出到堆栈内存中。 (Godbolt使用编译器发出的调试信息根据它们来自哪个C语句对asm指令进行颜色编码。我添加了换行符以显示每个语句的块,但是您可以在上面的Godbolt链接上突出显示该颜色。通常可以非常方便地在优化的编译器输出中找到内部循环的有趣部分。)gcc -fverbose-asm
将在每行上添加注释,将操作数名称显示为C vars。在优化的代码中,通常是内部tmp名称,而在未优化的代码中,通常是C源代码中的实际变量。我手动注释了clang输出,因为它没有执行此操作。# 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
有趣的事实:使用
register float c = a+b;
,返回值可以保留在语句之间的XMM0中,而不是被溢出/重新加载。变量没有地址。 (我在Godbolt链接中包含了该功能的版本。)register
关键字在优化的代码中无效(除了使获取变量的地址出错,例如本地的const
如何阻止您意外修改某些内容外)。我不建议使用它,但有趣的是,它确实会影响未优化的代码。有关的:
__attribute__((always_inline))
可以强制内联,但是并不能优化复制以创建函数args,更不用说将函数优化到调用方中了。 关于c - 为什么clang用-O0产生效率低的asm(对于这个简单的浮点数和)?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/53366394/