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

When can optimizations done by the compiler destroy my C++ code?

提问人:gablin 提问时间:9/2/2010 最后编辑:casperOnegablin 更新时间:4/7/2012 访问量:3537

问:

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

C++ 优化

评论

2赞 strager 9/2/2010
你是在问你的程序中的错误还是编译器中的错误?
1赞 gablin 9/2/2010
好吧,我假设由于优化而发生的错误位于程序中,而不是编译器中。除非优化确实完全错误,否则错误将驻留在编译器中。所以,换个说法;在进行编译器优化时,程序中会出现哪些错误?
0赞 Kirill V. Lyadvinsky 9/2/2010
无论优化是打开还是关闭,编译器都应根据 C++ 标准生成代码。所有其他情况都是程序或编译器中的错误。
4赞 jk. 9/2/2010
不是真正的优化,但你应该注意不要在断言中使用带有副作用的代码,否则当断言在发布中被省略时,行为可能会发生变化(假设它们在发布中被省略)
0赞 gablin 9/2/2010
@jk:啊,以前没想过。好一个。

答:

5赞 Steve Townsend 9/2/2010 #1

除了您提到的情况之外,多线程代码中的时序可能会发生变化,因此看起来有效的方法不再起作用。局部变量的位置可能会发生变化,因此在调试中发生内存缓冲区溢出等有害行为,但不会发布、优化或未优化,反之亦然。但所有这些都是已经存在的错误,只是通过编译器选项更改暴露出来。

这是假设编译器在其优化器中没有错误。

3赞 miked 9/2/2010 #2

我只在浮点数学中遇到过它。有时,速度优化可能会稍微改变答案。当然,对于浮点数学,“正确”的定义并不总是那么容易提出,所以你必须运行一些测试,看看优化是否符合你的预期。优化不一定会使结果出错,只是不同而已。

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

21赞 AshleysBrain 9/2/2010 #3

编译器优化不应影响程序的可观察行为,因此从理论上讲,您无需担心。在实践中,如果你的程序误入了未定义的行为,任何事情都可能已经发生,所以如果你的程序在启用优化时中断,你只是暴露了现有的错误——不是优化破坏了它。

一个常见的优化点是返回值优化 (RVO) 和命名返回值优化 (NRVO),这基本上意味着由函数中的值返回的对象直接在接收它们的对象中构造,而不是复制。这会调整构造函数、复制构造函数和析构函数调用的顺序和数量 - 但通常正确编写这些函数后,行为仍然没有明显的差异。

评论

0赞 Scott Stafford 9/2/2010
@gablin:是的。虽然 C++0x 会在标准中更清楚地指定这一点,但我相信。C++缺乏良好的线程标准。但是优化编译器被设计为除了使事情更快/使用更少的内存/更有效率之外,不做任何可观察的事情。
1赞 bbadour 9/2/2010
我尽量不对任何人或任何事情进行攻击。如果我的编译器可以在满足某些条件时提供不安全的优化,但在不满足这些条件时可以显着提高性能,我希望我的编译器提供这些优化。我希望默认情况下会关闭不安全的优化,并且编译器将充分记录“某些条件”,以便我可以就打开优化做出明智的决定。
0赞 gablin 9/3/2010
我认为这个主题现在已经足够多了。所有的答案都派上了用场,谢谢大家!我会将其设置为接受,因为它获得了最多的选票,并且几乎涵盖了所有的核心。
1赞 Mark B 9/2/2010 #4

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

评论

1赞 tenfour 9/2/2010
我很想看看是否有人能提供一个实际的例子,说明这将影响真实系统中的行为。
0赞 dmckee --- ex-moderator kitten 9/2/2010
...允许假设某个有限的循环类将始终终止...
0赞 Dennis Zickefoose 9/2/2010
stackoverflow.com/questions/3592557/......@tenfour:在链接的文章中,有一些涉及嵌入式系统的例子,其中无限循环是合法的,但它们的范围似乎非常有限。
0赞 Steve Jessop 9/2/2010
@tenfour:这里的例子,blog.regehr.org/archives/161。基本上是一个搜索费马大定理反例的程序,它不是特别扭曲的代码。大多数阅读它的人会认为它永远不会终止:定理是正确的。事实上,它具有未定义的行为,因为它包含一个循环,编译器被允许假设终止,但如果不是标准中那个晦涩的子句,它肯定不会终止。使用英特尔的编译器,在 Windows 上,它会终止,指出费马大定理是错误的。
0赞 supercat 9/2/2010
基本上,编译器可以移动在循环终止后才会执行的代码,以便后一个代码将在循环执行之前或期间执行,前提是 - 如果循环终止 - 可观察的事件序列不会受到重写的影响(即使它们的时间可能受到影响)。
0赞 decimus phostle 9/2/2010 #5

我没有确切的细节(也许其他人可以插话),但我听说如果循环计数器变量是 char/uint8_t 类型(在 gcc 上下文中),则循环展开/优化会导致错误(即)。

1赞 5ound 9/2/2010 #6

严格别名是您在使用 gcc 时可能会遇到的问题。据我了解,在某些版本的 gcc (gcc 4.4) 中,它会自动启用优化。这个网站 http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html 很好地解释了严格的混叠规则。

评论

1赞 AbePralle 3/6/2021
是的 - 我总是必须为我的 VM 和其他框架指定 -fno-strict-aliasing,这些框架使用内存执行半非典型操作,否则它们会在 Linux 和 Mac 上损坏。
2赞 Hans Passant 9/2/2010 #7

只是不要假设优化器破坏你的代码。这不是它本来就要做的。如果您确实观察到问题,则自动考虑无意的 UB。

是的,线程可能会破坏您习惯的那种假设。你从语言或编译器中得不到任何帮助,尽管这种情况正在改变。你所做的不是用 volatile 来惹恼,你使用了一个好的线程库。在两个或多个线程都可以接触变量的地方,您可以使用它的同步原语之一。试图走捷径或自己优化它是一张进入线程地狱的单程票。

评论

3赞 supercat 9/2/2010
不要一开始就假设编译器或优化器坏了,但不要总是忽略这种可能性。很多时候,我发现检查编译器输出中是否有某些行为异常的代码很有用。在极少数情况下,它会暴露编译器错误;在更多情况下,它揭示了编译器如何解释我所写的内容(诸如有符号和无符号类型之间的交互之类的事情可能很棘手)。
0赞 gablin 9/2/2010
我很难理解你的第一行:你的意思是我不应该假设编译器总是破坏我的代码,还是它永远不会破坏我的代码?“UB”代表什么?从完全不同的角度来说,我想在问了我之前的那些问题之后,会永远困扰着我。^^ 该关键字的更好名称应该是 ,因为这就是我使用它后的感受。volatileviolated
0赞 Dennis Zickefoose 9/2/2010
@gablin:他说首先要查看代码中的错误;始终假设编译器是正确的,直到您相当确定自己的代码不是您目睹的问题的原因。
0赞 Hans Passant 9/2/2010
@gablin:同意丹尼斯的观点。UB = 未定义的行为。是的,提出 volatile 可能不是一个好主意,我猜下次再提线程。你正在追逐一个不稳定的幽灵,它很少见。
0赞 Zan Lynx 9/2/2010
一个笑话评论,但也很严肃:当我开始在Unix上编程时,编译器破坏了我的代码。不,真的是这样!在命令行上键入 cc -o program program.c 时,shell 制表符补全非常容易使 cc -o program.c program.c
1赞 MSN 9/2/2010 #8

在元级别上,如果你的代码使用依赖于基于 C++ 标准的未定义方面的行为,那么符合标准的编译器可以自由地销毁你的 C++ 代码(正如你所说)。如果你没有一个符合标准的编译器,那么它也可以做一些非标准的事情,比如无论如何都会销毁你的代码。

大多数编译器发布它们遵循的 C++ 标准的子集,因此您始终可以按照该特定标准编写代码,并且大多数情况下假设您是安全的。但是,如果不首先遇到编译器中的错误,就无法真正防范它们,因此您仍然无法真正保证任何事情。

2赞 bbadour 9/2/2010 #9

在声明对易失性内存位置或 IO 设备的访问权限时,未能包含 volatile 关键字是代码中的一个错误;即使错误只有在代码得到优化时才明显。

编译器将记录任何“不安全”的优化,其中记录了打开和关闭它们的命令行开关和编译指示。不安全的优化通常与浮点数学(舍入、NAN 等边缘情况)或混叠的假设有关,正如其他人已经提到的那样。

持续折叠会产生别名,从而在代码中出现 bug。因此,例如,如果您有如下代码:

static char *caBuffer = "                                         ";

...

strcpy(caBuffer,...)

你的代码基本上是一个错误,你在常量(文字)上乱涂乱画。如果没有不断折叠,错误不会真正影响任何事情。但就像你提到的易失性错误一样,当你的编译器折叠常量以节省空间时,你可能会在另一个文字上乱涂乱画,比如下面的空格:

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

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

3赞 Razzupaltuff 9/2/2010 #10

由编译器优化引起的错误不是基于代码中的错误,这些错误是不可预测的,也很难确定(我曾经在优化代码中的某个区域时检查编译器创建的汇编代码时设法找到了一个错误)。常见的情况是,如果优化使程序不稳定,它只会暴露程序中的缺陷。

2赞 AnT stands with Russia 9/2/2010 #11

只要您的代码不依赖于未定义/未指定行为的特定表现形式,并且只要代码的功能是根据 C++ 程序的可观察行为定义的,C++ 编译器优化不可能破坏代码的功能,只有一个例外

  • 当创建临时对象的唯一目的是立即复制和销毁时,即使该对象的构造函数/析构函数具有影响程序可观察行为的副作用,编译器也可以消除此类临时对象的创建。

在较新版本的 C++ 标准中,该权限扩展到涵盖所谓的命名返回值优化 (NRVO) 中的命名对象。

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

不过,人们可以争辩说,依赖这种行为实际上只不过是依赖于未指定行为的特定表现形式。这是一个有效的参数,可用于支持在上述条件下优化永远不会破坏程序功能的断言。

您的原始示例不是有效示例。你基本上是在指责编译器破坏了从一开始就不存在的保证。如果你的问题应该以这种特定的方式解释(即优化器可能会破坏哪些随机的假的不存在的假想保证),那么可能的答案的数量几乎是无限的。这个问题根本没有多大意义。volatile