提问人:gablin 提问时间:9/2/2010 最后编辑:casperOnegablin 更新时间:4/7/2012 访问量:3537
编译器完成的优化何时会破坏我的 C++ 代码?
When can optimizations done by the compiler destroy my C++ code?
问:
编译器完成的优化何时会导致我的 C++ 代码表现出错误的行为,如果没有执行这些优化,这些行为就不会出现?例如,在某些情况下不使用可能会导致程序行为不正确(例如,不从内存中重新读取变量的值,而只读取一次并将其存储在寄存器中)。但是,在打开最激进的优化标志之前,是否还有其他陷阱应该了解,然后想知道为什么该程序不再起作用?volatile
答:
除了您提到的情况之外,多线程代码中的时序可能会发生变化,因此看起来有效的方法不再起作用。局部变量的位置可能会发生变化,因此在调试中发生内存缓冲区溢出等有害行为,但不会发布、优化或未优化,反之亦然。但所有这些都是已经存在的错误,只是通过编译器选项更改暴露出来。
这是假设编译器在其优化器中没有错误。
我只在浮点数学中遇到过它。有时,速度优化可能会稍微改变答案。当然,对于浮点数学,“正确”的定义并不总是那么容易提出,所以你必须运行一些测试,看看优化是否符合你的预期。优化不一定会使结果出错,只是不同而已。
除此之外,我从未见过任何优化破坏正确的代码。编译器编写者非常聪明,知道他们在做什么。
编译器优化不应影响程序的可观察行为,因此从理论上讲,您无需担心。在实践中,如果你的程序误入了未定义的行为,任何事情都可能已经发生,所以如果你的程序在启用优化时中断,你只是暴露了现有的错误——不是优化破坏了它。
一个常见的优化点是返回值优化 (RVO) 和命名返回值优化 (NRVO),这基本上意味着由函数中的值返回的对象直接在接收它们的对象中构造,而不是复制。这会调整构造函数、复制构造函数和析构函数调用的顺序和数量 - 但通常正确编写这些函数后,行为仍然没有明显的差异。
评论
我最近刚刚看到(在 C++0x 中)编译器被允许假设某些类的循环将始终终止(以允许优化)。我现在找不到参考资料,但如果能找到它,我会尝试链接它。这可能会导致可观察到的程序更改。
评论
我没有确切的细节(也许其他人可以插话),但我听说如果循环计数器变量是 char/uint8_t 类型(在 gcc 上下文中),则循环展开/优化会导致错误(即)。
严格别名是您在使用 gcc 时可能会遇到的问题。据我了解,在某些版本的 gcc (gcc 4.4) 中,它会自动启用优化。这个网站 http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html 很好地解释了严格的混叠规则。
评论
只是不要假设优化器会破坏你的代码。这不是它本来就要做的。如果您确实观察到问题,则自动考虑无意的 UB。
是的,线程可能会破坏您习惯的那种假设。你从语言或编译器中得不到任何帮助,尽管这种情况正在改变。你所做的不是用 volatile 来惹恼,你使用了一个好的线程库。在两个或多个线程都可以接触变量的地方,您可以使用它的同步原语之一。试图走捷径或自己优化它是一张进入线程地狱的单程票。
评论
volatile
violated
在元级别上,如果你的代码使用依赖于基于 C++ 标准的未定义方面的行为,那么符合标准的编译器可以自由地销毁你的 C++ 代码(正如你所说)。如果你没有一个符合标准的编译器,那么它也可以做一些非标准的事情,比如无论如何都会销毁你的代码。
大多数编译器发布它们遵循的 C++ 标准的子集,因此您始终可以按照该特定标准编写代码,并且大多数情况下假设您是安全的。但是,如果不首先遇到编译器中的错误,就无法真正防范它们,因此您仍然无法真正保证任何事情。
在声明对易失性内存位置或 IO 设备的访问权限时,未能包含 volatile 关键字是代码中的一个错误;即使错误只有在代码得到优化时才明显。
编译器将记录任何“不安全”的优化,其中记录了打开和关闭它们的命令行开关和编译指示。不安全的优化通常与浮点数学(舍入、NAN 等边缘情况)或混叠的假设有关,正如其他人已经提到的那样。
持续折叠会产生别名,从而在代码中出现 bug。因此,例如,如果您有如下代码:
static char *caBuffer = " ";
...
strcpy(caBuffer,...)
你的代码基本上是一个错误,你在常量(文字)上乱涂乱画。如果没有不断折叠,错误不会真正影响任何事情。但就像你提到的易失性错误一样,当你的编译器折叠常量以节省空间时,你可能会在另一个文字上乱涂乱画,比如下面的空格:
printf("%s%s%s",cpName," ",cpDescription);
因为编译器可能会将文本参数指向用于初始化 caBuffer 的文本的最后 4 个字符处的 printf 调用。
由编译器优化引起的错误不是基于代码中的错误,这些错误是不可预测的,也很难确定(我曾经在优化代码中的某个区域时检查编译器创建的汇编代码时设法找到了一个错误)。常见的情况是,如果优化使程序不稳定,它只会暴露程序中的缺陷。
只要您的代码不依赖于未定义/未指定行为的特定表现形式,并且只要代码的功能是根据 C++ 程序的可观察行为定义的,C++ 编译器优化就不可能破坏代码的功能,只有一个例外:
- 当创建临时对象的唯一目的是立即复制和销毁时,即使该对象的构造函数/析构函数具有影响程序可观察行为的副作用,编译器也可以消除此类临时对象的创建。
在较新版本的 C++ 标准中,该权限扩展到涵盖所谓的命名返回值优化 (NRVO) 中的命名对象。
这是优化破坏符合 C++ 代码的功能的唯一方法。如果您的代码以任何其他方式受到优化的影响,则要么是代码中的错误,要么是编译器中的错误。
不过,人们可以争辩说,依赖这种行为实际上只不过是依赖于未指定行为的特定表现形式。这是一个有效的参数,可用于支持在上述条件下优化永远不会破坏程序功能的断言。
您的原始示例不是有效示例。你基本上是在指责编译器破坏了从一开始就不存在的保证。如果你的问题应该以这种特定的方式解释(即优化器可能会破坏哪些随机的假的不存在的假想保证),那么可能的答案的数量几乎是无限的。这个问题根本没有多大意义。volatile
评论