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

ere*_*eOn 68 compiler-construction optimization compiler-optimization

今天我和我的一个朋友进行了讨论,我们就"编译器优化"进行了几个小时的讨论.

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

我的朋友完全不同意,他说"编译器是由聪明人构建并做聪明的东西",因此永远不会出错.

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

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

Mr.*_* 安宇 41

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

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

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

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

CERT在 Robert C. Seacord上发表了一篇名为"危险优化和因果关系丢失" 的演讲,其中列出了许多在程序中引入(或暴露)错误的优化.它讨论了各种可能的优化,从"做硬件做什么"到"捕获所有可能的未定义行为"到"做任何不被禁止的事情".

一些代码的例子在完全优化的编译器得到它之前是完全正常的:

这里的问题是几十年来编译器在优化方面的积极性较低,因此几代C程序员学习和理解固定大小的二进制补码以及它如何溢出等问题.然后,编译器开发人员修改了C语言标准,尽管硬件没有变化,但细微的规则也会发生变化.C语言规范是开发人员和编译人员之间的合同,但协议的条款可能会随着时间的推移而变化,并不是每个人都了解每个细节,或者同意细节甚至是明智的.

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

  • 关闭优化的一个更常见的原因是调试通常更难. (22认同)
  • @先生.Shiny已经让他有理由相信为什么可以关闭优化,但这远远不是_​​indisputable_而且我怀疑它在大多数编译器文档中都是这样解释的.调试时,人们通常需要能够轻松与原始代码相关的机器代码,因此可以使用关闭优化的构建,而QA构建(以及最终版本)将启用优化.任何人,在发现像@Mr描述的那个问题时.闪亮,然后通过关闭优化来"修复"它在我看来修正了错误的东西. (9认同)
  • 给出的示例是"volatile"关键字存在的原因.这是一个代码错误.即使有优化,无错误的编译器也不会引入错误. (7认同)
  • 非易失性读取是由编译器(或运行时)优化引起的*错误行为*的一个很好的例子,尽管我不确定这是否会被归类为"错误",因为它是开发人员的责任解释这些事情. (5认同)
  • @ Mr.ShinyandNew安宇你一次又一次地忽略了这一点.关于您提供的链接示例,编译器优化没有引入任何错误.程序员在`struct sock*sk = tun-> sk;`dereferencing`tun`没有进行NULL检查时就已经出现了这个错误.其次,C++标准规定NULL是指向无对象的指针,不能解除引用.基于此,编译器优化了代码,完全可以并且符合标准. (3认同)
  • @ Mr.ShinyandNew安宇空指针是否有效无关紧要.重要的是标准说它无效,编译器只需要符合标准.当程序员编写违反标准的代码或者他不理解标准时,他或她必须对后果承担全部责任.我已经阅读了这个帖子中的所有"例子",但由于同样的原因,它们都是无效的. (2认同)
  • @ Mr.ShinyandNew安宇Specs可能不合逻辑或含糊不清,编译器可能会出错.我没有声称否则.但是,编译器优化只有在存在已触发的错误时才会引入错误.这就是我在第一次和第二次评论中所说的两次.但是你提供的例子和你的答案表明编译器优化可能会引入错误,即使它们没有错误,这是你错误的,因为我已经多次解释过了. (2认同)
  • @darkestkhan 这也是一个无效的例子,因为标准*允许*编译器优化内存读取和写入*当单线程可观察行为没有改变并且没有未定义的行为时*。如果程序员误解了语言并认为`memset`不会被优化掉,那么*是程序员有错*,是程序员在代码中引入了错误,而不是编译器。程序员应该使用诸如 `memset_s` 或 `SecureZeroMemory` 之类的安全函数,这些函数不会被优化掉...... (2认同)
  • ...这个答案本质上是说“当程序员误解语言或期望语言不能保证的事情时,优化可能会引入错误”,这是没有意义的,“当编译器有错误时,优化会引入错误”,这是非常明显,并且 * 不是 * OP 问题是关于什么的。乔恩斯基特的答案是正确的答案。 (2认同)

pet*_*hen 30

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

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

在那个时候,我可以确认在启用了激进优化时发生的单个代码生成错误.

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

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

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


Jon*_*eet 12

编译器(和运行时)优化肯定会带来不期望的行为-但它至少应该如果你依靠明确的行为(或者实际上作出有关精心指定的行为不正确的假设)才会发生.

除此之外,当然编译器可能会有错误.其中一些可能是围绕优化,其影响可能非常微妙 - 实际上它们很可能是,因为明显的错误更容易被修复.

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


Car*_*icz 10

要结合其他帖子:

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

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


leg*_*cia 7

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

从那以后,这显然已得到修复.


Hig*_*ark 7

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

我有一个最近的情况,编译器指令"删除"了一个错误.当然,这个bug确实还在那里,但我有一个临时的解决方法,直到我正确修复程序.


tlo*_*ach 6

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


Ada*_*son 5

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


pet*_*sev 5

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

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

更明显的例子:VS2008 vs GCC

声明:

Function foo( const type & tp ); 
Run Code Online (Sandbox Code Playgroud)

所谓的:

foo( foo2() );
Run Code Online (Sandbox Code Playgroud)

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

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


Mar*_*som 5

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

为了以可预测的方式启用此类优化,C 编程语言(包括其较新的 C99 版本)的 ISO 标准规定,不同类型的指针引用相同的内存位置是非法的(有一些例外)。这条规则被称为“严格别名”,可以显着提高性能[需要引用],但已知会破坏一些其他有效的代码。一些软件项目故意违反 C99 标准的这一部分。例如,Python 2.x 这样做是为了实现引用计数,[1] 并且需要对 Python 3 中的基本对象结构进行更改以启用此优化。Linux 内核这样做是因为严格的别名会导致内联代码优化出现问题。 [2] 在这种情况下,当用 gcc 编译时,