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

问题描述 投票:1回答:1

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

鉴于编译器总是正确的,为什么选择这种策略呢?

c assembly llvm x86-64 compiler-optimization
1个回答
16
投票

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

-O0实际上并不是优化;例如gcc仍然会消除if(1 == 2){ }块中的代码。特别是gcc比其他大多数编译器还要做更多的事情,例如在-O0上使用乘法反转进行除法,因为它仍然通过多个内部表示来转换你的C源代码。最终发布asm之前的逻辑。)

另外,即使在-O3,“编译器总是正确的”也是夸大其词。编译器在很大程度上非常好,但是在单循环中仍然很少发生遗漏优化。通常具有非常低的影响,但循环中浪费的指令(或uops)可能会占用无序执行重新排序窗口中的空间,并且在与另一个线程共享核心时不会出现超线程友好。有关在简单特定情况下击败编译器的更多信息,请参阅C++ code for testing the Collatz conjecture faster than hand-written assembly - why?


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

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

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

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

如果您不需要这个,可以使用-Og进行编译以进行光优化,而无需进行一致性调试所需的反优化。 GCC手册推荐它用于通常的编辑/编译/运行周期,但是在调试时,您将通过自动存储为许多局部变量“优化”。全局和函数args通常仍具有实际值,至少在函数边界。


更糟糕的是,即使您使用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 -O3on the Godbolt compiler explorer)到预期的

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


Related:

© www.soinside.com 2019 - 2024. All rights reserved.