-O0
(未优化)是默认值。它告诉编译器您希望它快速编译(短编译时间),not花费额外的时间编译以生成高效的代码。
(-O0
并不是字面上没有优化;例如gcc 仍然会消除里面的代码if(1 == 2){ }
块。特别是 gcc 比大多数其他编译器仍然更多地执行诸如使用乘法逆元进行除法之类的事情-O0
,因为在最终发出 asm 之前,它仍然会通过逻辑的多个内部表示来转换您的 C 源代码。)
另外,“编译器总是对的”即使在-O3
。编译器在大规模方面非常出色,但在单个循环中,轻微的优化失误仍然很常见。通常影响很小,但循环中浪费的指令(或微指令)可能会占用无序执行重新排序窗口中的空间,并且在与另一个线程共享核心时对超线程不太友好。看用于测试 Collatz 猜想的 C++ 代码比手写汇编更快 - 为什么? https://stackoverflow.com/questions/40354978/c-code-for-testing-the-collatz-conjecture-faster-than-hand-written-assembly有关在简单的特定情况下击败编译器的更多信息。
更重要的是,-O0
也意味着对待所有变量类似于volatile
为了一致的调试。即,您可以设置断点或单步,然后modifyC 变量的值,然后继续执行并使程序按照您期望在 C 抽象机上运行的 C 源代码的方式工作。因此编译器无法进行任何常量传播或值范围简化。 (例如,已知非负整数可以使用它来简化事情,或者使某些 if 条件始终为真或始终为假。)
(它不是quite和......一样烂volatile
:在一个语句中对同一变量的多次引用并不总是导致多次加载;在-O0
编译器仍然会在单个表达式中进行一定程度的优化。)
编译器必须专门针对以下情况进行反优化-O0
通过在语句之间将所有变量存储/重新加载到它们的内存地址。 (在 C 和 C++ 中,每个变量都有一个地址,除非它是用(现已废弃)声明的register
关键字并且其地址从未被占用。根据其他变量的假设规则,可以优化地址,但不能在-O0
)
不幸的是,调试信息格式无法通过寄存器跟踪变量的位置,因此如果没有这种缓慢而愚蠢的代码生成,完全一致的调试是不可能的。
如果你不需要这个,你可以编译-Og
用于轻度优化,并且没有一致调试所需的反优化。 GCC 手册建议将其用于通常的编辑/编译/运行周期,但在调试时,您将通过自动存储来“优化”许多局部变量。全局变量和函数参数通常仍然具有其实际值,至少在函数边界处如此。
更糟糕,-O0
即使您使用 GDB,编写的代码仍然可以工作jump
命令在不同的源代码行继续执行。因此,每个 C 语句都必须编译成完全独立的指令块。 (是否可以在 GDB 调试器中“跳转”/“跳过”? https://stackoverflow.com/questions/4116632/is-it-possible-to-jump-skip-in-gdb-debugger)
for()
循环不能转化为惯用语(用于 asm)do{}while() loops https://stackoverflow.com/questions/47783926/why-are-loops-always-compiled-into-do-while-style-tail-jump和其他限制。
由于上述所有原因,(微观)基准测试未优化的代码会浪费大量时间;结果取决于您如何编写源代码的愚蠢细节,当您使用正常优化进行编译时,这些细节并不重要。-O0
vs. -O3
性能不是线性相关的;有些代码会比其他代码加速得多.
瓶颈在-O0
代码通常会不同于-O3
- 通常在保存在内存中的循环计数器上,创建约 6 周期循环携带的依赖链。这可以在编译器生成的 asm 中创建有趣的效果,例如添加冗余赋值可以在未经优化的情况下编译时加快代码速度 https://stackoverflow.com/questions/49189685/adding-a-redundant-assignment-speeds-up-code-when-compiled-without-optimization(从汇编的角度来看这很有趣,但是not for C.)
“我的基准测试以其他方式进行了优化”并不是查看性能的有效理由-O0
代码。
看最终分配的 C 循环优化帮助 https://stackoverflow.com/questions/32000917/c-loop-optimization-help-for-final-assignment/32001196#32001196有关调整兔子洞的示例和更多详细信息-O0
is.
获得有趣的编译器输出
如果你想看看编译器如何添加 2 个变量,编写一个接受参数并返回值的函数。请记住,您只想查看汇编,而不是运行它,因此您不需要main
或任何应该是运行时变量的数字文字值。
也可以看看如何从 GCC/clang 汇编输出中消除“噪音”? https://stackoverflow.com/questions/38552116/how-to-remove-noise-from-gcc-clang-assembly-output了解更多相关信息。
float foo(float a, float b) {
float c=a+b;
return c;
}
编译为clang -O3
(在 Godbolt 编译器资源管理器上 https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(j:1,lang:c%2B%2B,source:'float+foo(float+a,+float+b)+%7B%0A++++float+c%3Da%2Bb%3B%0A++++return+c%3B%0A%7D%0A%0Afloat+bar(float+a,+float+b)+%7B%0A++++register+float+c%3Da%2Bb%3B%0A++++//+can+stay+in+xmm0+between+statements+at+-O0%0A++++//+because+of+the+register+keyword.%0A++++return+c%3B%0A%7D%0A'),l:'5',n:'0',o:'C%2B%2B+source+%231',t:'0')),k:33.14447592067989,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:clang700,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c%2B%2B,libs:!(),options:'-xc+-O3+-Wall',source:1),l:'5',n:'0',o:'x86-64+clang+7.0.0+(Editor+%231,+Compiler+%232)+C%2B%2B',t:'0')),header:(),k:30.23910429342857,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:g82,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c%2B%2B,libs:!(),options:'-xc+-O0+-Wall',source:1),l:'5',n:'0',o:'x86-64+gcc+8.2+(Editor+%231,+Compiler+%231)+C%2B%2B',t:'0')),k:36.61641978589154,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4)到预期的
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 链接中。)
The register
关键字在优化代码中没有任何作用(除了使获取变量地址时出错,例如如何const
在本地可以防止您意外修改某些内容)。我不建议使用它,但有趣的是它确实会影响未优化的代码。
Related:
-
简单构造函数的复杂编译器输出 https://stackoverflow.com/questions/55329728/complex-compiler-output-for-simple-constructor- 传递参数时变量的每个副本通常会在 asm 中产生额外的副本。
-
为什么这个 C++ 包装类没有被内联掉? https://stackoverflow.com/questions/54073295/why-is-this-c-wrapper-class-not-being-inlined-away/54074497#54074497
__attribute__((always_inline))
可以强制内联,但不会优化创建函数参数的复制,更不用说将函数优化到调用者中。