在g++中,优化级别-O3是否危险?

292

我从多个渠道听说(尤其是我的一位同事),在g++中使用优化级别-O3进行编译某种程度上是“危险的”,通常情况下应该避免,除非有必要。

这是真的吗?如果是,为什么?我应该坚持使用-O2吗?


48
只有当你依赖于未定义的行为时才会有危险。即使那样,如果优化级别出了问题,我也会感到惊讶。 - Seth Carnegie
5
编译器仍然受限于产生一个行为“如同”它编译了你的代码一样的程序。我不知道-O3被认为特别有缺陷吗?我想也许它会基于某些假设做出奇怪和美妙的事情,从而使未定义的行为变得更加糟糕,但那应该是你自己的责任。因此,通常来说这是没问题的。 - BoBTFish
5
更高级别的优化确实更容易发生编译器错误。我自己也遇到过几次,但通常它们仍然相当罕见。 - Mysticial
30
-O2 打开了 -fstrict-aliasing,如果您的代码能够通过此选项,则它可能会通过其他优化选项,因为这是一个人们一遍又一遍做错的选项。尽管如此,-fpredictive-commoning 只在 -O3 中存在,启用它可能会导致代码中的并发不正确而引入错误。您的代码越正确,优化的风险就越小;-) - Steve Jessop
7
@PlasmaHH,我认为"stricter"不是"-Ofast"的好描述,例如它关闭了对NaN的符合IEEE标准的处理。 - Jonathan Wakely
显示剩余3条评论
5个回答

288
在gcc早期版本(2.8等)和egcs、redhat 2.96时,-O3有时会出现一些错误。但这是十多年前的事情了,-O3在错误方面与其他优化级别并没有太大区别。

然而,它往往会暴露人们依赖于未定义行为的情况,因为它更严格地依赖于语言规则,特别是边角情况。

作为个人的注释,我使用-O3运行金融行业的生产软件已经多年了,还没有遇到过如果我使用-O2就不会出现的错误。

根据广大需求,以下是补充内容:

-O3,特别是像-funroll-loops(不被-O3启用)这样的附加标志,有时会导致生成更多的机器代码。在某些情况下(例如在具有异常小的L1指令缓存的CPU上),这可能会因为所有内部循环的代码都不能再适应于L1I,而引起减速。通常,gcc尽力不生成太多的代码,但由于通常优化通用情况,所以这种情况可能会发生。特别容易出现此问题的选项(如循环展开)通常不包括在-O3中,并在manpage中标记。因此,通常最好在需要生成快速代码时使用-O3,并只在适当的情况下(例如在分析器指示L1I未命中时)退回到-O2或-Os(试图优化代码大小)。

如果你想将优化推向极致,你可以通过--param在gcc中进行微调,调整某些优化所涉及的成本。此外,请注意,现在gcc具有能够在函数中放置属性以控制针对这些函数的优化设置的功能,因此当你发现在一个函数中使用-O3存在问题(或者希望尝试仅针对该函数使用特殊标志)时,你不需要使用O2编译整个文件甚至整个项目。

另一方面,在使用-Ofast时需要小心,因为它声明:

-Ofast启用所有-O3优化。 它还启用了并非所有标准都有效的优化

这让我得出结论,-O3的目的是要完全符合标准的程序。


4
我通常使用与此相反的编译选项。我总是使用-Os或-O2(有时O2可以生成更小的可执行文件)。在进行性能分析后,我会在执行时间较长的代码部分使用O3,这样就可以获得高达20%的速度提升。 - CoffeDeveloper
5
我会尽力进行翻译,保持原意并使之更易懂。以下是需要翻译的内容:我这样做是为了速度。大多数情况下,O3会使事情变得更慢。我不确定原因,但我认为它可能会占用指令缓存。 - CoffeDeveloper
7
我觉得“代码膨胀”很流行,但我几乎从未看到有人用基准测试来支持这个说法。这很大程度上取决于架构,但每次我看到公布的基准测试(例如http://www.phoronix.com/scan.php?page=article&item=gcc_47_optimizations&num=1),它都显示O3在绝大多数情况下更快。我曾经看到过证明代码膨胀实际上是一个问题所需的分析和仔细剖析,而这通常只发生在那些极端使用模板的人身上。 - Nir Friedman
1
@NirFriedman:当编译器的内联成本模型存在错误或者你优化的目标与实际运行的完全不同时,往往会出现问题。有趣的是,这适用于所有优化级别... - PlasmaHH
1
@PlasmaHH:对于一般情况,解决using-cmov问题可能会很困难。通常情况下,您不仅仅是对数据进行了排序,因此当gcc试图确定分支是否可预测时,寻找调用std::sort函数的静态分析不太可能有所帮助。使用类似于https://dev59.com/h3VD5IYBdhLWcg3wDG_l的东西可能会有所帮助,或者编写源代码以利用排序性:扫描直到看到>=128,然后开始求和。至于臃肿的代码,是的,我打算报告它。 :P - Peter Cordes
显示剩余6条评论

55

在我的经验中,对整个程序应用-O3选项几乎总是会使程序变慢(相对于-O2),因为它开启了激进的循环展开和内联优化,导致程序不再适合放入指令高速缓存中。对于较大的程序,-O2 相对于 -Os 也可能会出现这种情况!

使用-O3的预期模式是,在对程序进行分析之后,将其手动应用于包含关键内部循环的少数文件,这些循环实际上受益于这些激进的以空间换时间的折衷方式。较新版本的 GCC 具有基于分析的优化模式,可以(如果我理解正确)选择性地将-O3优化应用于热函数——有效地自动化此过程。


21
“几乎总是”?把它改成“五五开”,那我们就达成协议了 ;-). - No-Bugs Hare
3
gcc -O3 已经很长时间没有包含 -funroll-loops 了,因为您指出的展开非常热的循环以外的循环的问题。 -O3 仍然包括自动向量化,但 GCC12 将其添加到了 -O2 中。(在处理对齐的循环序言时,几个版本之后的更改通常会产生相当多的膨胀,尽管可能不是向量宽度的倍数的行程计数仍然可能非常膨胀,特别是带有 AVX-512 的 uint8_t 循环。) - Peter Cordes

24

是的,O3存在更多的漏洞。我是一名编译器开发人员,并且在构建自己的软件时发现了明显的gcc漏洞,这些漏洞是由于O3生成有缺陷的SIMD汇编指令引起的。从我看到的情况来看,大多数生产软件都使用O2,这意味着关于测试和漏洞修复方面将会更少关注O3。

这样想吧:O3在O2之上添加更多的转换,而O2在O1之上添加更多的转换。从统计学角度来看,更多的转换意味着更多的漏洞。对于任何编译器都是如此。


3
这太简单了。当-O3完全删除无法到达的代码时,任何在无法到达的代码中进行的-O2或-O1优化都变得无关紧要。这表明优化不会像在此处所假定的那样堆叠。而且,“构建我的软件时”是一个明显的警告信号。-O3将根据正在编译的代码的行为应用优化。代码中的任何未定义行为都可能触发意外的优化。但未定义行为意味着编译器 不可能出错 - MSalters

18

-O3选项开启更多的优化,例如函数内联,除了低级别‘-O2’和‘-O1’中的所有优化。‘-O3’优化等级可能会增加生成的可执行文件的速度,但也可能增加其大小。在某些不利于这些优化的情况下,此选项实际上可能会使程序变慢。


4
我知道有一些“表面上的优化”可能会使程序变慢,但你是否有任何来源声称GCC-O3会使程序变慢呢? - Mooing Duck
2
@MooingDuck:虽然我无法引用来源,但我记得在一些较旧的AMD处理器上遇到过这种情况,它们的L1I缓存非常小(约10k条指令)。我确信谷歌有更多相关信息,但特别是像循环展开这样的选项不属于O3,而且这些选项会大幅增加代码大小。-Os是当您想要使可执行文件最小化时的选择。即使-O2也会增加代码大小。一个很好的工具来尝试不同优化级别的结果是gcc explorer。 - PlasmaHH
2
@PlasmaHH:实际上,一个非常小的缓存大小是编译器可能会出错的地方,这是一个非常好的观点。那真是个很好的例子,请把它放在答案里。 - Mooing Duck
2
@PlasmaHH Pentium III有16KB的代码缓存。AMD的K6及以上实际上有32KB的指令缓存。P4从大约96KB开始。Core I7实际上有32KB的L1代码缓存。指令解码器现在很强大,所以您的L3足以为几乎任何循环提供后备支持。 - doug65536
1
每当在循环中调用函数并且它可以进行重要的公共子表达式消除和提升不必要的重新计算到循环之前时,您将看到巨大的性能提升。 - doug65536
AMD处理器没有L3缓存(据我所知,可能最近有所改变)。 - CoffeDeveloper

3

最近我在使用g++进行优化时遇到了一个问题。这个问题与PCI卡有关,其中寄存器(用于命令和数据)由内存地址表示。我的驱动程序将物理地址映射到应用程序中的指针,并将其提供给被调用的进程,该进程通过以下方式处理它:

unsigned int * pciMemory;
askDriverForMapping( & pciMemory );
...
pciMemory[ 0 ] = someCommandIdx;
pciMemory[ 0 ] = someCommandLength;
for ( int i = 0; i < sizeof( someCommand ); i++ )
    pciMemory[ 0 ] = someCommand[ i ];

这张卡的表现不如预期。当我查看汇编代码时,我发现编译器只在 pciMemory 写入了 someCommand[最后一个],忽略了所有之前的写入。

总之:优化时要准确、细心。


58
这里要表达的意思是,你的程序存在未定义行为;优化器没有做错什么。特别地,你需要将 pciMemory 声明为 volatile - Konrad Rudolph
20
实际上这并不是未定义行为,但编译器有权忽略除最后一次写入"pciMemory"之外的所有写入操作,因为可以证明所有其他写入都没有影响。对于优化器来说,这非常棒,因为它可以删除许多无用和耗时的指令。 - Konrad Rudolph
14
我在标准(10多年后)中找到了这个内容 - 可变声明可用于描述对应于内存映射的输入/输出端口或由异步中断函数访问的对象。对于这样声明的对象的操作不得被实现优化或重新排序,除非符合表达式评估规则的要求。 - borisbn
4
有点跑题,但是你如何知道你的设备在发送新命令之前已经接受了命令? - user877329
5
@user877329 我通过设备的行为看到了它,但这是一个伟大的任务。 - borisbn
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接