编译器优化何时会破坏我的C++代码?

10

编译器优化什么时候会导致我的C++代码表现出错误的行为,如果没有进行这些优化,这些行为将不会出现?例如,在某些情况下不使用volatile可能会导致程序行为不正确(例如,不从内存重新读取变量的值,而是仅读取一次并将其存储在寄存器中)。但是,在开启最激进的优化标志之前是否还有其他需要知道的陷阱,然后想知道为什么程序不再工作?


2
你是在问关于你的程序中的错误还是编译器中的错误? - strager
1
我认为由于优化而导致的错误应该位于程序中,而不是编译器中。除非优化本身完全错误,否则错误将存在于编译器中。因此,换句话说,我的程序中哪些错误可能会因编译器优化而出现? - gablin
无论编译器是否开启优化,都应该按照C++标准生成代码。其他情况都是程序或编译器中的错误。 - Kirill V. Lyadvinsky
4
不算是优化,但在断言语句中应当小心避免使用带有副作用的代码,否则在发布版本中省略(假设在发布版本中确实省略了)这些断言语句时程序行为可能会发生变化。 - jk.
@jk:啊,之前没有想到过那个。不错的点子。 - gablin
编译器对代码进行的优化不会破坏代码,但可能会严重限制您进行源级调试的能力。 - Chubsdad
11个回答

21

编译器的优化不应该影响程序的可观察行为,因此理论上你不需要担心。实际上,如果程序进入了未定义行为,任何事情都可能已经发生了,因此,如果启用优化后程序出现错误,那么你只是暴露了已经存在的错误——并不是因为优化而导致程序错误。

一个常见的优化点是返回值优化(RVO)和命名返回值优化(NRVO),这基本上意味着从函数返回的对象直接在接收它们的对象中构造,而不是复制。这调整了构造函数、复制构造函数和析构函数调用的顺序和数量——但通常情况下,只要这些函数正确地编写了,程序的行为仍然没有可观察的差异。


@gablin:是的。虽然C++0x标准将更清晰地指定这一点,但我相信C++缺乏良好的线程标准。但是优化编译器的设计是不会做任何可观察到的事情,除了使事情更快/使用更少的内存/更有效率。 - Scott Stafford
1
我尽量不对任何人或任何事情提出应该怎样做的要求。如果我的编译器能够在满足特定条件时提供一种不安全但可以显著提高性能的优化,而在不满足这些条件时则不进行优化,那么我希望我的编译器能够提供这些优化。我期望这些不安全的优化默认是关闭的,并且编译器会充分记录“特定条件”,以便我能够做出明智的决策是否开启这些优化。 - bbadour
我认为这个主题现在已经讨论得足够多了。所有的答案都非常有用,谢谢大家!我将把它设置为被接受的答案,因为它得到了最多的投票,而且基本上涵盖了所有核心内容。 - gablin

5
除了您提到的情况,多线程代码的时间可能会发生变化,因此先前有效的代码可能不再有效。局部变量的位置可能会有所变化,例如在调试模式下出现内存缓冲区溢出等有害行为,但在发布、优化或非优化版本中未出现,反之亦然。但所有这些都是已经存在的错误,只是由编译器选项更改暴露出来的。这是假设编译器没有优化器中的错误的情况。

3

我只遇到过浮点数运算时出现的问题。有时,为了提高速度进行的优化会稍微改变答案。当然,在浮点数运算中,“正确”的定义并不总是容易得出的,因此您需要运行一些测试来查看优化是否按照您的预期进行。这些优化不一定会使结果错误,只是不同而已。

除此之外,我从未见过任何优化破坏正确代码的情况。编译器编写者非常聪明,知道自己在做什么。


3
编译器优化引起的Bug如果不是由您的代码中的错误根源造成的,则难以预测和确定(我曾经在检查编译器在优化我的代码某个区域时创建的汇编代码时找到过一个)。通常情况下,如果优化使程序变得不稳定,那么它只是揭示了您程序中的缺陷。

2

不要以为优化器会破坏你的代码,这不是它的作用。如果你发现问题,请自动考虑无意中的UB。

是的,线程可能会对你习惯的假设造成混乱。你既没有语言也没有编译器的帮助,尽管情况正在改变。你应该使用一个好的线程库来处理这个问题,而不是浪费时间在volatile上。在两个或更多线程可以同时访问变量的地方,使用线程库的同步原语。试图走捷径或自己进行优化将是进入线程地狱的单程票。


3
不要一开始就假设编译器或优化器出了问题,但也不要排除这种可能性。通常情况下,我发现检查某些表现奇怪的代码的编译器输出是有用的。在极少数情况下,它会暴露编译器的错误;在更多的情况下,它会揭示编译器如何解释我所写的内容(例如有符号和无符号类型之间的交互可能会很棘手)。 - supercat
我有些困惑你第一行的意思:你是指我不应该假定编译器总是销毁我的代码,还是永远不会销毁我的代码?"UB"代表什么?完全不同的话题,我认为volatile关键字在我问了那些问题之后将会一直困扰我。^^ 一个更好的名称应该是violated,因为我使用它后感觉就像被违反了一样。 - gablin
@gablin:他的意思是先查找代码中的错误;在您相当确定自己的代码不是问题的原因之前,始终假定编译器是正确的。 - Dennis Zickefoose
@gablin:同意Dennis的观点。UB = 未定义行为。也许下次提到线程就好了,不要再提volatile了。你在追逐一个不稳定的幽灵,很少见到。 - Hans Passant
一个开玩笑的评论,但也很严肃:当我开始在Unix上编程时,编译器毁了我的代码。不,真的!在命令行上输入cc -o program program.c时,Shell的Tab键补全很容易将其变成cc -o program.c program.c。 - Zan Lynx
@Hans Passant:是的,但至少我更了解它了。无论如何,这都是值得的。^^谢谢。 - gablin

2
在声明对易失性内存位置或IO设备的访问时未包括volatile关键字是您代码中的一个错误。即使该错误仅在代码被优化后才显现,也是如此。
您的编译器将记录任何“不安全”的优化,其中记录了打开和关闭它们的命令行开关和编译指示。不安全的优化通常涉及浮点数学(舍入、NAN等边缘情况)或别名问题,正如其他人已经提到的那样。
常量折叠可以创建别名,从而使您代码中的错误出现。因此,例如,如果您有以下代码:
static char *caBuffer = "                                         ";

...

strcpy(caBuffer,...)

你的代码基本上是一种错误,即你在常量(字面值)上涂写。如果没有常量折叠,这个错误实际上不会影响任何东西。但就像你提到的易失性错误一样,当编译器折叠常量以节省空间时,你可能会涂写到另一个字面值中,比如:

printf("%s%s%s",cpName,"   ",cpDescription);

因为编译器可能会将传递给printf调用的文字参数指向用于初始化caBuffer的文字结尾的最后4个字符。


2
只要你的代码没有依赖于未定义/未指定行为的特定形式,并且只要你的代码功能是根据C++程序的可观察行为来定义的,那么C++编译器优化就不可能破坏你的代码功能,只有一种例外:
  • 当创建临时对象仅用于立即复制和销毁时,即使对象的构造函数/析构函数具有影响程序可观察行为的副作用,编译器也可以消除这种临时对象的创建。

在较新版本的C++标准中,该许可已扩展到名为命名返回值优化(NRVO)的命名对象。

这是优化可能破坏符合C++代码功能的唯一方式。如果您的代码以任何其他方式受到优化的影响,则它可能是代码中的错误或编译器中的错误。

尽管可以争论说,依赖此行为实际上等同于依赖于未指定的行为的特定表现形式。这是一个有效的论点,可用于支持上述条件下的优化永远不能破坏程序功能的断言。

您最初的带volatile 的示例不是一个有效的示例。您实际上是在指责编译器打破了根本不存在的保证。如果您的问题应以特定方式被解释(即可能优化器可能破坏的随机虚假不存在的想象保证),则可能的答案数量几乎是无限的。这个问题根本没有多大意义。


1

我最近看到(在C++0x中),编译器允许假设某些类别的循环将始终终止(以允许优化)。我现在找不到参考资料,但如果我能找到,我会尝试链接它。这可能会导致可观察的程序更改。


1
我希望能够看到有人提供一个实际例子,以便了解它如何影响实际系统的行为。 - tenfour
可以假设某个有限类别的循环总是会终止。 - dmckee --- ex-moderator kitten
@tenfour:链接文章中有一些涉及嵌入式系统的示例,其中无限循环是合理有用的,但它们似乎范围相当有限。 - Dennis Zickefoose
@tenfour:这里有一个例子,http://blog.regehr.org/archives/161。基本上是一个程序,用于搜索费马大定理的反例,它的代码并不特别扭曲。大多数人阅读它会认为它永远不会终止:因为定理是正确的。实际上,它具有未定义的行为,因为它包含一个循环,编译器可以假设它会终止,但如果没有标准中那个晦涩的条款,它肯定不会终止。在Windows上使用英特尔编译器时,它会终止并声明费马大定理是错误的。 - Steve Jessop
基本上,编译器允许移动那些在循环终止之后才执行的代码,以便后面的代码可以在循环执行期间或之前执行,前提是——如果循环确实终止——重写不会影响事件的可观察序列(尽管它们的时间可能会受到影响)。 - supercat

1

1
是的 - 我总是不得不为我的虚拟机和其他对内存进行半非典型操作的框架指定-fno-strict-aliasing,否则它们会在Linux和Mac上崩溃。 - AbePralle

1

在元级别上,如果您的代码依赖于基于C++标准未定义方面的行为,符合标准的编译器可以自由地破坏您的C++代码。如果您没有符合标准的编译器,则它也可以做一些非标准的事情,比如无论如何破坏您的代码。

大多数编译器都会发布其符合的C++标准子集,因此您始终可以将代码编写为特定标准,并且大多数情况下可以假设您是安全的。但是,如果没有先遇到编译器中的错误,您实际上仍然无法防范编译器中的错误,因此您仍然没有真正的保障。


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