[英]Why does clang produces wrong results for my c code compiled with -O1 but not with -O0?
[英]Why does clang produce inefficient asm with -O0 (for this simple floating point sum)?
我正在 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
处使用乘法逆除法之类的事情,因为它在最终发出 asm 之前,仍然通过逻辑的多个内部表示来转换您的 C 源代码。)
另外,即使在-O3
处,“编译器总是正确的”也是夸大其词。 编译器在大规模方面非常出色,但在单个循环中仍会出现轻微的遗漏优化。 通常具有非常低的影响,但循环中浪费的指令(或 uops)会占用乱序执行重新排序窗口中的空间,并且在与另一个线程共享一个内核时对超线程不那么友好。 请参阅C++ 代码以比手写程序集更快地测试 Collatz 猜想 - 为什么? 有关在简单的特定情况下击败编译器的更多信息。
更重要的是, -O0
还意味着将所有类似于volatile
变量处理为一致的调试。 即这样您就可以设置断点或单步并修改C 变量的值,然后继续执行并使程序按照您期望的方式工作,因为您的 C 源代码在 C 抽象机上运行。 所以编译器不能做任何常数传播或值范围的简化。 (例如,一个已知为非负的整数可以简化使用它的事情,或者使一些 if 条件始终为真或始终为假。)
(这不是那么糟糕,很为volatile
:一个语句中对同一变量多次引用并不总是导致多个负载;在-O0
编译器依然会有所优化一个表达式中。)
编译器必须通过在 statements 之间存储/重新加载所有变量到它们的内存地址来专门针对-O0
进行反优化。 (在 C 和 C++ 中,每个变量都有一个地址,除非它是用(现在已经过时的) register
关键字声明的,并且从未被占用。根据其他变量的 as-if 规则优化地址是可能的,但不是没有在-O0
完成)
不幸的是,调试信息格式无法通过寄存器跟踪变量的位置,因此如果没有这种缓慢而愚蠢的代码生成,就不可能实现完全一致的调试。
如果您不需要这个,您可以使用-Og
进行编译以进行轻度优化,而无需进行一致调试所需的反优化。 GCC 手册建议将其用于通常的编辑/编译/运行周期,但您将在调试时为许多具有自动存储的局部变量“优化”。 全局变量和函数参数通常仍然具有它们的实际值,至少在函数边界处是这样。
更糟糕的是,即使您使用 GDB 的jump
命令在不同的源代码行继续执行, -O0
会使代码仍然有效。 因此,每个 C 语句都必须编译成一个完全独立的指令块。 ( 是否可以在 GDB 调试器中“跳转”/“跳过”? )
for()
循环不能转换为惯用的(for asm) do{}while()
循环和其他限制。
由于上述所有原因, (微)基准测试未优化的代码是一种巨大的时间浪费; 结果取决于您如何编写源代码的愚蠢细节,当您使用正常优化进行编译时,这些细节无关紧要。 -O0
与-O3
性能不是线性相关的; 某些代码的速度会比其他代码快得多。
-O0
代码中的瓶颈通常与-O3
不同 - 通常在保存在内存中的循环计数器上,创建一个 ~6 周期循环携带的依赖链。 这可以在编译器生成的 asm 中创建有趣的效果,例如在没有优化的情况下编译时添加冗余赋值可以加快代码的速度(从 asm 的角度来看这很有趣,但对于 C 则不然。)
“否则我的基准测试优化了”不是查看-O0
代码性能的有效理由。 有关示例以及有关调整-O0
的兔子洞的更多详细信息,请参阅最终分配的 C 循环优化帮助。
如果您想查看编译器如何添加 2 个变量,请编写一个接受 args 并返回一个 value 的函数。 请记住,您只想查看 asm,而不是运行它,因此对于任何应该是运行时变量的内容,您都不需要main
或任何数字文字值。
另请参阅如何从 GCC/clang 程序集输出中去除“噪音”? 有关更多信息。
float foo(float a, float b) {
float c=a+b;
return c;
}
使用clang -O3
( 在 Godbolt 编译器资源管理器上) 编译为预期的
addss xmm0, xmm1
ret
但是使用-O0
它将参数溢出到堆栈内存。 (Godbolt 使用编译器发出的调试信息根据它们来自哪个 C 语句对 asm 指令进行颜色编码。我添加了换行符以显示每个语句的块,但是您可以在上面的 Godbolt 链接上看到带有颜色突出显示的内容. 在优化的编译器输出中找到内循环的有趣部分通常非常方便。)
gcc -fverbose-asm
将在每一行上添加注释,将操作数名称显示为 C 变量。 在优化代码中,通常是内部 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,更不用说将函数优化到调用者中了。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.