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

Ste*_*ini 4 c assembly x86-64 compiler-optimization llvm-codegen

我在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);
}
Run Code Online (Sandbox Code Playgroud)

我编译时没有-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)
    ...
Run Code Online (Sandbox Code Playgroud)

显然它正在做以下事情:

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

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

  1. 一切都可以在注册表中完成.我之前没有使用a和b,所以它可以跳过任何涉及堆栈的操作.
  2. 即使它想要使用堆栈,如果它使用不同的顺序执行操作,它也可以节省从堆栈重新加载xmm0.

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

Pet*_*des 17

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

另外,"编译器总是正确的"甚至是夸大其词-O0.编译器在很大程度上非常好,但是在单循环中仍然很少发生遗漏优化.通常具有非常低的影响,但循环中浪费的指令(或uops)可能会占用无序执行重新排序窗口中的空间,并且在与另一个线程共享核心时不会超线程友好.请参阅C++代码,以便比手写汇编更快地测试Collat​​z猜想 - 为什么?有关在简单的特定情况下击败编译器的更多信息.


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

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

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

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

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


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

-O0循环不能转换为惯用(form)-Og循环和其他限制.

由于上述所有原因,(微观)对未优化代码进行基准测试是一个巨大的浪费时间; 结果取决于您编写源代码的愚蠢细节,当您使用常规优化进行编译时无关紧要. -O0jump性能不是线性相关的; 有些代码比其他代码更快.

for()代码中的瓶颈通常不同于do{}while()- 通常在保存在内存中的循环计数器上,创建一个~6个循环的循环依赖链.这可以在编译器生成的asm中创建有趣的效果,例如 添加冗余分配可以在没有优化的情况下编译时加速代码(从asm角度来看这很有趣,但对于C 来说却不是这样).

"我的基准测试优化了",这不是查看-O0代码性能的有效理由.有关示例的最终分配,请参阅C循环优化帮助,以及有关调整的兔子洞的更多详细信息-O3.


获得有趣的编译输出

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

另请参见如何从GCC /铿锵声组件输出中删除"噪音"?了解更多相关信息.

float foo(float a, float b) {
    float c=a+b;
    return c;
}
Run Code Online (Sandbox Code Playgroud)

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

    addss   xmm0, xmm1
    ret
Run Code Online (Sandbox Code Playgroud)

但随着-O0它溢出了堆栈内存.(Godbolt使用编译器发出的调试信息来根据它们来自哪个C语句对asm指令进行颜色编码.我添加了换行符来显示每个语句的块,但是你可以在上面的Godbolt链接上看到这个带有颜色突出显示的内容通常非常方便在优化的编译器输出中找到内部循环的有趣部分.)

# 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
Run Code Online (Sandbox Code Playgroud)

有趣的事实:使用时-O0,返回值可以保留在语句之间的XMM0中,而不是溢出/重新加载.变量没有地址.(我在Godbolt链接中包含了该函数的版本.)

  • 请注意,至少clang实际上是从每个变量开始的,每个变量在堆栈上为其分配了内存。如果可能的话,最先的优化过程之一(我猜为-O0省略了这些过程)将它们转变为一堆SSA变量。因此,至少在clang上,没有进行“反优化”,只是关闭了正常的优化。 (3认同)

归档时间:

查看次数:

333 次

最近记录:

6 年,4 月 前