编译器优化能否引入错误?

今天我和我的一个朋友进行了一次讨论,我们就“编译器优化”问题进行了几个小时的讨论。

我曾经辩解过, 有时编译器优化可能会引入错误或者至less是不希望的行为。

我的朋友完全不同意,说“编译器是聪明人做的,做聪明的事情”,所以不会出错。

他根本没有说服我,但我不得不承认我缺乏现实的例子来强化我的观点。

谁在这里? 如果是,那么你是否有一个真实的例子,编译器优化在结果软件中产生了一个bug? 如果我错了,我应该停止编程,学习捕鱼吗?

编译器优化可能会引入错误或不良行为。 这就是为什么你可以closures它们。

举一个例子:编译器可以优化对内存位置的读/写访问,如消除重复读取或重复写入,或重新sorting某些操作。 如果有问题的内存位置只能由一个线程使用,并且实际上是内存,那可能是正确的。 但是,如果内存位置是硬件设备IO寄存器,那么重新sorting或消除写入可能是完全错误的。 在这种情况下,您通常必须编写代码,因为知道编译器可能会“优化”它,因此知道这种简单的方法不起作用。

更新:正如亚当·罗宾逊(Adam Robinson)在评论中指出的那样,我上面描述的场景更多的是编程错误,而不是优化器错误。 但是我想说明的一点是,一些正确的程序,再加上一些正常工作的优化,可以在程序结合在一起的时候在程序中引入错误。 在某些情况下,语言规范说“你必须这样做,因为这种优化可能会发生,你的程序将会失败”,在这种情况下,这是代码中的一个错误。 但有时编译器有一个(通常是可选的)优化function,可能会产生不正确的代码,因为编译器正在努力优化代码或无法检测到优化是不适当的。 在这种情况下,程序员必须知道什么时候可以安全地打开所讨论的优化。

另一个例子: linux内核有一个错误 ,其中一个潜在的NULL指针在该指针的testing为空之前被解引用。 但是,在某些情况下,可以将内存映射到地址为零,从而使解引用成功。 编译器在注意到指针已解除引用时,假定它不能为NULL,那么稍后删除NULLtesting以及该分支中的所有代码。 这在代码中引入了一个安全漏洞 ,因为函数会继续使用包含攻击者提供数据的无效指针。 对于指针合法为空且内存未映射到地址0的情况,内核仍然会像以前那样使用OOPS。 所以在优化之前,代码中包含一个bug。 之后它包含了两个,其中一个允许本地根用户利用。

securecoding.cert.org网站上有一个名为“危险优化和因果关系损失” 的文档 ,罗伯特·西科德列举了很多引入(或暴露)程序错误的优化。 Googlecaching链接 。 它讨论了可能的各种优化,从“做什么硬件”到“把所有可能的未定义的行为”,“做任何不被禁止的事情”。

代码的一些例子是非常好的,直到一个积极优化的编译器才得以实现:

  • 检查溢出

     // fails because the overflow test gets removed if (ptr + len < ptr || ptr + len > max) return EINVAL; 
  • 使用溢出算术:

     // The compiler optimizes this to an infinite loop for (i = 1; i > 0; i += i) ++j; 
  • 清除敏感信息的记忆:

     // the compiler can remove these "useless writes" memset(password_buffer, 0, sizeof(password_buffer)); 

这里的问题在于编译器几十年来在优化方面的积极性不高,所以C程序员的一代人学习和理解了固定大小的补充加法以及溢出的方式。 然后C语言标准被编译器开发者修改,微妙的规则改变了,尽pipe硬件没有改变。 C语言规范是开发人员和编译人员之间的合同,但是协议的条款随时间而变化,并不是每个人都能理解每一个细节,或者同意细节甚至是明智的。

这就是大多数编译器提供closures(或打开)优化的标志的原因。 你的程序写的理解是整数可能会溢出吗? 那么你应该closures溢出优化,因为它们可以引入错误。 你的程序是否严格避免别名指针? 然后,您可以打开假设指针永不别名的优化。 您的程序是否尝试清除内存以避免泄漏信息? 哦,在这种情况下,你运气不好:你需要closures死代码,或者你需要提前知道你的编译器将会消除你的“死”代码,并使用一些工作 – 为此。

是的,一点没错。
看到这里 ,(这里仍然存在 – “按devise”!?!), 在这里 , 在这里 , 在这里 ,…

当一个错误消失,通过禁用优化,大部分时间,这仍然是你的错

我负责一个主要用C ++编写的商业应用程序 – 从VC5开始,早期移植到VC6,现在已成功移植到VC2008。 在过去的10年里,它已经增长到100多万行。

在那个时候,我可以确认一个代码生成的问题,当发生激进的优化,启用。

那为什么我在抱怨? 因为在同一时间,有几十个bug让我怀疑编译器 – 但结果是我对C ++标准的理解不足。 该标准为编译器可能或不可以使用的优化提供了空间。

多年来,在不同的论坛上,我看到许多指责编译器的post,最终成为原代码中的错误。 毫无疑问,它们中的许多掩盖了需要对标准中使用的概念的详细理解的错误,但是源代码错误仍然存​​在。

为什么我这么晚回复:在确认实际上是编译器的错误之前停止指责编译器。

编译器(和运行时)优化肯定会引入不希望的行为 – 但是至less应该只在你依赖未指定的行为时(或者对错误的行为做出错误的假设)才会发生。

除此之外,编译器当然可以有错误。 其中一些可能是围绕优化,其影响可能是非常微妙的 – 事实上它们可能是,因为明显的错误更可能被修复。

假设你将JIT作为编译器,我已经看到了.NET JIT和Hotspot JVM的发布版本中的错误(不幸的是,目前我没有细节),这在特别奇怪的情况下是可重现的。 不pipe他们是否由于特别的优化,我不知道。

结合其他post:

  1. 与大多数软件一样,编译器偶尔会在代码中出现错误。 “聪明人”的说法与此完全无关,因为美国宇航局的卫星和其他由聪明人build造的应用程序也有缺陷。 优化的代码与不优化的代码不同,所以如果错误发生在优化器中,那么优化代码可能包含错误,而非优化代码则不会。

  2. 正如Shiny和New先生所指出的那样,对于并​​发性和/或时序问题来说,天真的代码在没有优化的情况下能够令人满意地运行,却可能会因优化而失败,因为这可能会改变执行的时间。 你可以把这样的问题归咎于源代码,但是如果它只会在优化时出现,有些人可能会责怪优化。

只是一个例子:几天前,有人发现 gcc 4.5带有选项-foptimize-sibling-calls (由-O2暗示)会产生一个Emacs可执行文件,在启动时会出现段错误。

这显然已经被固定 。

我从来没有听说过或使用过编译器的指令不能改变程序的行为。 通常这是一件好事 ,但它确实需要你阅读手册。

最近有一个编译器指令“删除”了一个错误。 当然,这个错误还真的存在,但是我有一个临时的解决方法,直到我正确地修复程序。

是。 一个很好的例子是双重检查locking模式。 在C ++中,没有办法安全地实现双重检查locking,因为编译器可以在单线程系统中重新sorting指令,而不是在multithreading系统中重新sorting。 完整的讨论可以在http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdffind。;

可能吗? 不是主要的产品,但肯定是可能的。 编译器优化是生成的代码; 不pipe代码来自哪里(你写它或者产生它),它都可能包含错误。

我用一个更新的编译器构build旧代码时遇到了这个问题。 旧的代码可以工作,但在某些情况下依赖于未定义的行为,例如不正确定义的/ cast操作符超载。 它将在VS2003或VS2005debugging版本中工作,但在发布时会崩溃。

打开程序集生成很明显,编译器刚刚删除了有问题的function的80%的function。 重写代码不使用未定义的行为清除了它。

比较明显的例子:VS2008 vs GCC

声明:

 Function foo( const type & tp ); 

所谓的:

 foo( foo2() ); 

其中foo2()返回类type的对象;

在GCC中会崩溃,因为在这种情况下对象没有被分配到堆栈上,但是VS做了一些优化来解决这个问题,它可能会工作。

是的,编译器优化可能是危险的。 通常,硬实时软件项目由于这个原因而禁止优化。 无论如何,你知道有没有错误的软件吗?

积极的优化可能会caching甚至对variables进行奇怪的假设。 问题不仅在于代码的稳定性,还会欺骗debugging器。 我曾多次看到debugging器无法表示内存内容,因为某些优化在微处理器的寄存器中保留了一个variables值

同样的事情可能发生在你的代码上。 优化将一个variables放入一个寄存器中,不要写入variables,直到完成。 现在想象一下,如果你的代码有指向你的堆栈中的variables的指针,并且它有多个线程,那么情况会有多不同

别名可能会导致某些优化问题,这就是编译器可以select禁用这些优化的原因。 维基百科 :

为了以可预测的方式启用这种优化,C编程语言(包括其更新的C99版本)的ISO标准规定,不同types的指针指向相同的存储器位置是非法的(有一些例外)。 这个被称为“严格别名”的规则允许令人印象深刻的性能提升[需要的引证],但已经知道打破一些有效的代码。 有几个软件项目故意违反了C99标准的这一部分。 例如,Python 2.x这样做是为了实现引用计数[1],并且需要对Python 3中的基本对象结构进行更改以启用此优化。 Linux内核是这样做的,因为严格的别名会导致优化内联代码的问题[2]。 在这种情况下,使用gcc进行编译时,会调用选项-fno-strict-aliasing以防止可能产生不正确代码的不必要的或无效的优化。

当然,这在理论上是可能的。 但是如果你不相信这些工具去做他们应该做的事情,为什么要使用它们呢? 但是,马上就有人争论

“编译人员是聪明人做的,做聪明的事情”,因此永远不会出错。

正在做一个愚蠢的论点。

所以,直到你有理由相信编译器正在这样做,为什么要这样做呢?

这有可能发生。 它甚至影响了Linux 。

我当然同意说这是愚蠢的,因为编译器是由“聪明人”编写的,因此它们是无误的。 聪明的人也devise了欣登堡和塔科马海峡大桥。 即使编译器作者是最聪明的程序员之一,但编译器也是最复杂的程序之一。 当然他们有错误。

另一方面,经验告诉我们商业编译器的可靠性非常高。 我有很多次,有人告诉我,程序的原因不起作用,必须是因为编译器中的一个错误,因为他已经非常仔细地检查了它,并且确信它是100%正确的。然后我们发现其实程序有错误而不是编译器。 我试图想到我曾经亲身经历过的一些事情,我真的确定是编译器中的一个错误,我只能回想一个例子。

所以一般来说:相信你的编译器。 但他们错了吗? 当然。

我记得早期的Delphi 1有一个错误,即Min和Max的结果相反。 只有在dll中使用浮点值的时候,也有一些浮点值的问题。 无可否认,这已经十多年了,所以我的记忆可能有些模糊。

如果您使用优化构build,则在.NET 3.5中遇到了问题,将另一个variables添加到名称与在同一范围内的同一types的现有variables类似的方法,然后两个variables之一(新variables或旧variables)将不会在运行时有效,并且所有对无效variables的引用都被引用replace为另一个引用。

所以,例如,如果我有abcd的MyCustomClasstypes,我有abc的MyCustomClasstypes,我设置abcd.a = 5和abdc.a = 7,那么这两个variables将具有属性a = 7。 要解决这个问题,这两个variables应该被删除,程序编译(希望没有错误),那么他们应该重新添加。

我觉得在使用Silverlight应用程序的时候,我也遇到了几次.NET 4.0和C#的问题。 在我上一份工作中,我们经常在C ++中遇到这个问题。 这可能是因为编译花费了15分钟,所以我们只build立我们需要的库,但是有时候优化的代码和以前的版本完全一样,即使已经添加了新的代码,并且没有报告生成错误。

是的,代码优化器是由聪明人build立的。 他们也很复杂,所以有bug是常见的。 我build议全面testing大型产品的任何优化版本。 通常有限使用的产品不值得完全发布,但是他们仍然应该通常进行testing,以确保他们正确地执行他们的共同任务。

编译器优化可以显示(或激活)代码中的隐藏(或隐藏)错误。 您可能在C ++代码中存在一个您不知道的错误,您只是看不到它。 在这种情况下,这是一个隐藏或hibernate的错误,因为代码的分支没有执行[足够的次数]。

代码中出现错误的可能性比编译器代码中的错误要大得多(数千倍):因为编译器已被广泛testing。 通过TDD加上几乎所有使用它们的人都可以使用它们!)。 所以,一个错误几乎不可能被你发现,而且几乎不会被其他人使用的数十万次发现。

一个潜伏的bug或隐藏的bug只是一个程序员没有向自己显示的bug。 可以声称他们的C ++代码没有(隐藏)错误的人是非常罕见的。 它需要C ++的知识(很less有人可以要求这么做),并且需要大量的代码testing。 这不仅仅是程序员,而是关于代码本身(开发风格)。 易出错的是代码的性质(testing的严格程度)还是程序员(如何受到严格的testing以及如何知道C ++和编程)。

安全性+并发性错误:如果将并发性和安全性作为缺陷包括在内,则会更糟糕。 但毕竟,这些“是”错误。 编写一个在并发性和安全性方面无缺陷的代码几乎是不可能的。 这就是为什么代码中总是存在一个bug,在编译器优化中可能会被泄露(或遗忘)。

由于彻底的testing和相对简单的实际C ++代码(C ++有100个关键字/运算符),编译器错误相对较less。 糟糕的编程风格往往是唯一遇到的问题。 通常编译器会崩溃或产生一个内部编译器错误。 这个规则的唯一例外是GCC。 海湾合作委员会,特别是旧版本,有很多实验优化启用O3 ,有时甚至其他O级别。 海湾合作委员会也瞄准如此之多的后端,这为他们的中间表示留下了更多的空间。

如果您编译的程序具有良好的testing套件,则可以启用更多,更积极的优化。 然后有可能运行该套件,并确保程序运行正确。 此外,你可以准备你自己的testing,密切配合,你打算在生产中做。

任何大型程序都可能(并且可能确实有)独立于您使用哪些开关进行编译的错误也是事实。

我昨天遇到了.net 4的问题,看起来像…

 double x=0.4; if(x<0.5) { below5(); } else { above5(); } 

它会调用above5(); 但是如果我真的在某个地方使用x ,它会调用below5();

 double x=0.4; if(x<0.5) { below5(); } else { System.Console.Write(x); above5(); } 

不完全相同的代码,但类似。

所有你可能想象的事情都会引入bug。