编译器优化会引入错误吗?

时间:2010-04-27 15:01:34

标签: optimization compiler-construction compiler-optimization

今天我与我的一位朋友进行了讨论,我们就“编译器优化”进行了几个小时的辩论。

我为有时这一点辩护,编译器优化可能会引入错误或至少是不良行为。

我的朋友完全不同意,他说“编译器是由聪明人构建并做聪明的事情”,因此,永远不会出错。

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

谁在这?如果是的话,您是否有任何现实生活中的例子,编译器优化会在生成的软件中产生错误?如果我错了,我应该停止编程并学习钓鱼吗?

23 个答案:

答案 0 :(得分:40)

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

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

更新:正如Adam Robinson在评论中指出的那样,我上面描述的场景更多的是编程错误,而不是优化器错误。但我试图说明的一点是,某些程序本来是正确的,加上一些优化,否则它们可以正常工作,当它们组合在一起时会在程序中引入错误。在某些情况下,语言规范说“你必须这样做,因为可能会发生这种类型的优化,你的程序也会失败”,在这种情况下,这是代码中的错误。但有时编译器有一个(通常是可选的)优化功能,可以生成错误的代码,因为编译器太难以优化代码或无法检测到优化是不合适的。在这种情况下,程序员必须知道何时打开有问题的优化是安全的。

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

CERT has a presentation由Robert C. Seacord称为“危险优化和因果关系丢失”,列出了许多在程序中引入(或暴露)错误的优化。它讨论了各种可能的优化,从“做硬件做什么”到“捕获所有可能的未定义行为”到“做任何不被禁止的事情”。

一些代码的例子,在积极优化的编译器得到它之前完全没问题:

  • 检查溢出

    // 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语言标准,尽管硬件没有变化,但细微的规则也会发生变化。 C语言规范是开发人员和编译人员之间的合同,但协议的条款可能会随着时间的推移而变化,并不是每个人都了解每个细节,或者同意细节甚至是明智的。

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

答案 1 :(得分:26)

当通过禁用优化来消除错误时,大部分时间它仍然是您的错误

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

在那段时间里,我可以确认在启用激进优化时发生的单个代码生成错误。

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

多年来,在不同的论坛上,我看到很多帖子都指责编译器,最终证明是原始代码中的错误。毫无疑问,其中许多模糊的bug需要详细了解标准中使用的概念,但源代码仍然存在错误。

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

答案 2 :(得分:25)

绝对是的。
请参阅herehere(仍然存在 - “按设计”!?!),herehereherehere。 ..

答案 3 :(得分:11)

编译器(和运行时)优化当然可以引入不受欢迎的行为 - 但至少只有应该只有在你依赖于未指明的行为时(或者实际上做出错误的假设)才会发生关于明确的行为)。

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

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

答案 4 :(得分:9)

结合其他帖子:

  1. 编译器偶尔会在代码中出现错误,就像大多数软件一样。 “智能人”的论点与此完全无关,因为美国宇航局的卫星和智能人员制造的其他应用也存在漏洞。执行优化的编码与不编码的编码不同,因此如果错误发生在优化器中,那么确实优化的代码可能包含错误,而非优化代码则不会。

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

答案 5 :(得分:7)

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

自{。}以来有apparently been fixed

答案 6 :(得分:7)

我从未听说过或使用过编译器,其指令无法改变程序的行为。通常这是好东西,但它确实需要您阅读手册。

我有一个最近的情况,编译器指令'删除'了一个bug。当然,这个bug确实仍然存在,但我有一个临时的解决方法,直到我正确修复程序。

答案 7 :(得分:6)

是。一个很好的例子是双重检查锁定模式。在C ++中,没有办法安全地实现双重检查锁定,因为编译器可以在单线程系统中以有意义的方式重新排序指令,但不能在多线程系统中重新排序。可以在http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf

找到完整的讨论

答案 8 :(得分:5)

可能吗?不是主要产品,但肯定可能。编译器优化是生成代码;无论代码来自何处(您编写代码或生成代码),都可能包含错误。

答案 9 :(得分:5)

我使用更新的编译器构建旧代码时遇到过这种情况。旧代码可以工作,但在某些情况下依赖于未定义的行为,例如不正确定义/强制转换运算符。它可以在VS2003或VS2005调试版本中运行,但在发布时它会崩溃。

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

更明显的例子:VS2008 vs GCC

宣称:

Function foo( const type & tp ); 

调用:

foo( foo2() );

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

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

答案 10 :(得分:4)

别名可能会导致某些优化问题,这就是编译器可以选择禁用这些优化的原因。来自Wikipedia

  

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

答案 11 :(得分:3)

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

积极的优化可能会缓存甚至对您的变量做出奇怪的假设。问题不仅在于代码的稳定性,而且还会欺骗您的调试器。我已经多次看到调试器无法表示内存内容,因为一些优化在微控制器的寄存器中保留了一个变量值

你的代码可能会发生同样的事情。优化将变量放入寄存器,并且在变量完成之前不写入变量。现在想象一下,如果你的代码指向堆栈中的变量并且它有多个线程

,那么事情会有多么不同

答案 12 :(得分:2)

我当然同意,因为编制者是由“聪明人”编写的,因此它们是绝对可靠的,这是愚蠢的。聪明人也设计了兴登堡和塔科马海峡大桥。即使编译器编写者是最聪明的程序员之一,编译器也是最复杂的程序之一。当然他们有虫子。

另一方面,经验告诉我们商业编译器的可靠性非常高。我有很多次有人告诉我程序不起作用的原因必须是因为编译器中的一个错误,因为他非常仔细地检查了它,并且他确信它是100%正确的...然后我们发现实际上程序有错误而不是编译器。我想到的是,我个人遇到的事情,我确信这是编译器中的一个错误,我只记得一个例子。

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

答案 13 :(得分:2)

理论上可行,当然。但如果你不相信这些工具可以做他们应该做的事情,为什么要使用它们?但是,任何人都可以从

的立场出发
  

“编译器是由聪明人构建的   做聪明的事情“因此,可以   永远不会出错。

是一个愚蠢的论点。

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

答案 14 :(得分:2)

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

答案 15 :(得分:1)

我记得,早期的Delphi 1有一个错误,其中Min和Max的结果被颠倒了。只有在dll中使用浮点值时,才会出现一些带有浮点值的模糊错误。不可否认,已经十多年了,所以我的记忆可能有点模糊。

答案 16 :(得分:1)

如果使用优化构建,我在.NET 3.5中遇到了问题,将另一个变量添加到一个方法中,该方法的命名方式类似于同一范围内相同类型的现有变量,然后是两个中的一个(新旧变量) )在运行时无效,对无效变量的所有引用都被替换为对另一个的引用。

因此,例如,如果我有MycustomClass类型的abcd并且我有MycustomClass类型的abdc并且我设置了abcd.a = 5和abdc.a = 7那么两个变量将具有属性a = 7。为了解决这个问题,应该删除这两个变量,编译程序(希望没有错误),然后重新添加它们。

我认为在执行Silverlight应用程序时,我已经使用.NET 4.0和C#多次遇到此问题。在我上一份工作中,我们经常在C ++中遇到这个问题。这可能是因为编译需要15分钟,因此我们只会构建我们需要的库,但有时优化的代码与之前的版本完全相同,即使添加了新代码并且没有报告构建错误。

是的,代码优化器是由聪明人构建的。它们也非常复杂,因此容易出错。我建议全面测试大型产品的任何优化版本。通常限制使用的产品不值得完整发布,但仍应对它们进行一般性测试,以确保它们正确执行常见任务。

答案 17 :(得分:1)

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

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

休眠错误或隐藏的错误只是一个尚未向程序员透露的错误。可以声称他们的C ++代码没有(隐藏)错误的人是非常罕见的。它需要C ++知识(很少有人可以声称)和广泛的代码测试。它不仅仅是关于程序员,而是关于代码本身(开发风格)。容易出错的是代码的特性(测试的严格程度)或/和程序员(测试中的规范性以及对C ++和编程的了解程度如何)。

安全性+并发错误:如果我们将并发性和安全性作为错误包含在内,那就更糟了。但毕竟,这些'都是'错误。编写一个在并发性和安全性方面首先无错误的代码几乎是不可能的。这就是为什么代码中总会存在一个错误,可以在编译器优化中显示(或遗忘)。

答案 18 :(得分:0)

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

答案 19 :(得分:0)

如果您编译的程序具有良好的测试套件,则可以启用更积极的优化。然后就可以运行该套件,并且可以更加确定程序是否正常运行。此外,您可以准备自己的测试,这些测试与您计划在生产中密切配合。

任何大型程序都可能(或可能确实存在)一些错误,您可以使用哪些开关来编译它们。

答案 20 :(得分:0)

我在大型工程应用程序上工作,时不时地看到发布仅崩溃和客户报告的其他问题。我们的代码有37个文件(约占  6000),我们将其放在文件顶部,以关闭优化以修复此类崩溃:

#pragma optimize( "", off)

(我们使用的是Microsoft Visual C ++ native,2015,但几乎对于任何编译器都是如此,除了可能尚未进行任何优化的Intel Fortran 2016 update 2。)

如果您通过Microsoft Visual Studio反馈网站进行搜索,则也可以在其中找到一些优化错误。我们有时会记录一些记录(如果您可以用一小段代码就可以很容易地重现它,并且您愿意花时间),它们确实会得到修复,但遗憾的是会再次引入它们。 微笑

编译器是人编写的程序,任何大程序都有错误,请相信我。编译器优化选项肯定有错误,打开优化肯定会在程序中引入错误。

答案 21 :(得分:-1)

由于详尽的测试以及实际C ++代码的相对简单性(C ++有不到100个关键字/运算符),编译器错误相对较少。糟糕的编程风格通常是遇到它们的唯一方法。通常编译器会崩溃或产生内部编译器错误。这条规则唯一的例外是GCC。 GCC,尤其是旧版本,在O3中有时启用了大量实验性优化,有时甚至是其他O级别。 GCC还针对如此多的后端,这为其中间代表中的错误留下了更多空间。

答案 22 :(得分:-2)

我昨天遇到了.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(); }

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