繁体   English   中英

为什么 clang 使用 -O0 产生低效的 asm(对于这个简单的浮点和)?

[英]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)
    ...

显然它正在执行以下操作:

  1. 将两个浮点数加载到寄存器 xmm0 和 xmm1
  2. 把它们放在堆栈中
  3. 从堆栈中加载一个值(不是之前的 xmm0)到 xmm0
  4. 执行加法。
  5. 将结果存回堆栈。

我发现它效率低下,因为:

  1. 一切都可以在注册表中完成。 我稍后不再使用 a 和 b,因此它可以跳过任何涉及堆栈的操作。
  2. 即使它想使用堆栈,如果它以不同的顺序执行操作,它也可以避免从堆栈重新加载 xmm0。

既然编译器永远是对的,那它为什么选择这种策略呢?

-O0 (未优化)是默认值 它告诉编译器您希望它快速编译(编译时间短),而不是花费额外的时间来编译以生成高效的代码。

-O0并不是字面上没有优化;例如 gcc 仍然会消除if(1 == 2){ }块中的代码。尤其是 gcc 比大多数其他编译器仍然执行诸如在-O0处使用乘法逆除法之类的事情,因为它在最终发出 asm 之前,仍然通过逻辑的多个内部表示来转换您的 C 源代码。)

另外,即使在-O3处,“编译器总是正确的”也是夸大其词。 编译器在大规模方面非常出色,但在单个循环中仍会出现轻微的遗漏优化。 通常具有非常低的影响,但循环中浪费的指令(或 uops)会占用乱序执行重新排序窗口中的空间,并且在与另一个线程共享一个内核时对超线程不那么友好。 请参阅C++ 代码以比手写程序集更快地测试 Collat​​z 猜想 - 为什么? 有关在简单的特定情况下击败编译器的更多信息。


更重要的是, -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如何阻止您意外修改某些内容)。 我不建议使用它,但有趣的是它确实会影响未优化的代码。


有关的:

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM