5

我在 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。

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

4

1 回答 1

25

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

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

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


更重要的是,-O0还意味着将所有变量都视为类似以volatile进行一致的调试。也就是说,您可以设置断点或单步并修改C 变量的值,然后继续执行并让程序按照您在 C 抽象机器上运行的 C 源代码所期望的方式工作。所以编译器不能做任何常量传播或值范围简化。(例如,已知为非负的整数可以使用它来简化事情,或者使某些条件始终为真或始终为假。)

(它并没有那么糟糕volatile:在一个语句中对同一个变量的多次引用并不总是导致多次加载;at-O0编译器仍然会在单个表达式中进行一些优化。)

-O0编译器必须通过在语句之间将所有变量存储/重新加载到它们的内存地址来专门进行反优化。(在 C 和 C++ 中,每个变量都有一个地址,除非它是用(现在已过时的)register关键字声明的并且从未使用过它的地址。根据其他变量的 as-if 规则优化地址是可能的,但不是t 完成-O0

不幸的是,调试信息格式无法通过寄存器跟踪变量的位置,因此如果没有这种缓慢而愚蠢的代码生成,就不可能进行完全一致的调试。

如果你不需要这个,你可以编译以-Og进行轻度优化,而无需进行一致调试所需的反优化。GCC 手册建议将它用于通常的编辑/编译/运行周期,但在调试时,您将针对许多具有自动存储功能的局部变量“优化”。全局变量和函数参数通常仍然具有它们的实际值,至少在函数边界处是这样。


更糟糕的是,-O0即使您使用 GDB 的jump命令在不同的源代码行继续执行,代码仍然可以工作。因此,每个 C 语句都必须编译成完全独立的指令块。(是否可以在 GDB 调试器中“跳转”/“跳过”?

for()循环不能转换为惯用的(对于 asm)do{}while()循环和其他限制。

由于上述所有原因,(微)基准测试未优化的代码是浪费时间。结果取决于您编写源代码的愚蠢细节,当您使用正常优化进行编译时,这些细节并不重要。 -O0-O3性能不是线性相关的;有些代码会比其他代码加速得多

代码中的瓶颈-O0通常不同于-O3- 通常在保存在内存中的循环计数器上,创建一个 ~6 循环循环携带的依赖链。这可以在编译器生成的 asm 中产生有趣的效果,例如 在没有优化的情况下编译时添加冗余赋值会加速代码(从 asm 的角度来看这很有趣,但对于 C语言却不是。)

“否则我的基准优化了”并不是查看-O0代码性能的有效理由。有关最终分配的示例和有关调整的兔子洞的更多详细信息,请参阅C 循环优化帮助-O0


获得有趣的编译器输出

如果您想查看编译器如何添加 2 个变量,请编写一个接受 args 并返回 value 的函数main请记住,您只想查看 asm,而不是运行它,因此对于应该是运行时变量的任何内容,您不需要 a或任何数字文字值。

另请参阅如何从 GCC/clang 程序集输出中删除“噪音”?有关此的更多信息。

float foo(float a, float b) {
    float c=a+b;
    return c;
}

clang -O3在 Godbolt 编译器资源管理器上)编译到预期的

    addss   xmm0, xmm1
    ret

但是-O0它会将args溢出到堆栈内存中。(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在本地如何阻止您意外修改某些内容)。我不推荐使用它,但有趣的是它确实会影响未优化的代码。


有关的:

于 2018-11-18T23:34:01.647 回答