当前位置: 首页 > 知识库问答 >
问题:

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

尹雅健
2023-03-14

我在llvm clang Apple llvm version8.0.0(clang-800.0.42.1)上反汇编此代码:

int main() {
    float a=0.151234;
    float b=0.2;
    float c=a+b;
    printf("%f", c);
}

我使用no-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)
    ...
    null
    null

共有1个答案

慕宜民
2023-03-14

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

(-O0并不完全是优化;例如,如果(1==2){}块,gcc仍然会消除中的代码。特别是gcc比大多数其他编译器仍然会在-O0中使用乘法逆运算进行除法,因为在最终发出ASM之前,它仍然会通过逻辑的多个内部表示来转换您的C源代码。)

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

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

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

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

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

如果不需要,可以使用-og进行编译,以进行轻度优化,而无需进行一致调试所需的反优化。GCC手册建议将其用于通常的编辑/编译/运行周期,但在调试时,您将得到许多具有自动存储的局部变量的“优化”。全局和函数arg通常仍有其实际值,至少在函数边界处是如此。

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

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

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

“我的基准优化了,否则”不是查看-O0代码性能的有效理由。有关优化-O0的兔子洞的示例和更多细节,请参见C循环优化帮助中的最终赋值。

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

使用clang-o3(在Godbolt编译器资源管理器上)编译为预期的

    addss   xmm0, xmm1
    ret

但是对于-o0,它会将arg溢出到堆栈内存中。(Godbolt使用编译器发出的调试信息对asm指令进行颜色编码,根据它们来自哪个C语句。我添加了换行符来显示每个语句的块,但您可以在上面的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
    null

 类似资料:
  • 我非常困惑为什么gcc会为const数组上的简单for循环生成这种(看似)非最佳代码。 结果: 我主要关心的是: 为什么无用的第一个元素比较在?这永远不会命中,也永远不会被分支回。它最终只是第一次迭代的重复代码。 < li >有没有更好的方法来编写这个非常简单的循环,这样gcc就不会产生这种奇怪的代码? < li >有没有我可以利用的编译器标志/优化?< code>O3只是展开循环,我也不希望这样

  • 在这个打印从1到10000000的所有数字、Haskell版本和C版本的简单程序中,为什么Haskell版本如此缓慢,以及哪些命令有助于学习如何提高Haskell程序的性能? 下面是一份报告,包含重现我激动人心的事件所需的所有细节,制作报告时会打印出来源,包括Makefile的来源:

  • 我无法理解的是,为什么当我使用Eclipse将所有依赖项提取到jar中的文件夹结构中时,jar不再作为一个正确的Spring Boot应用程序运行。 为了重复我在这里所做的事情,只需使用一个简单的Spring Boot应用程序,并从Eclipse中选择以下内容: 导出-->Runnable JAR-->选择Main Class-->将所需库提取到Jar中。 我只是想了解Spring实际上是如何通过

  • 我在学校上课: 然后我创建了School的两个实例,并比较了两个实例的相等性: 即使我设置了相同的和到

  • 所以浮点运算是不精确的,但这并不能完全解释这里发生的事情:

  • 问题内容: 很简单的一行: 失败与: 而扩展为: 工作良好。 问题答案: 您使用错误。使用这种方式: 通用形式为: