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


72

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

我捍卫了这样的观点,有时,编译器优化可能会引入错误或至少会带来不良行为。

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

他一点也不说服我,但是我不得不承认我缺乏现实生活中的例子来加强我的观点。

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


6
这并不常见,但确实会发生。Google搜索codegen bug optimization打开了示例。当然,编译器也存在启用优化的错误,因此不,优化不是编译器中唯一无缺陷的功能。:)
Craig Stuntz 2010年

53
“编译器是由聪明的人建造的,可以做聪明的事情”,因此,永远不会出错-哈哈哈!如果他相信这一点,请给我他的地址-我有一座需要出售的桥梁。

7
@Neil-桥梁是否经过优化?
DVK 2010年

8
@DVK-没关系,我确定它是由聪明的人构建的。
贾斯汀·罗斯巴奇

1
@Justin-我要评论的是,如果他们很聪明,他们会设计桥梁而不是建造桥梁……然后我回想起“ Office Space”的结尾,意识到您是对的:)
DVK

Answers:


44

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

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

更新:正如亚当·罗宾逊(Adam Robinson)在评论中指出的那样,我上面描述的场景更多是编程错误而不是优化程序错误。但是我想说明的一点是,某些正确的程序与某些优化的组合,如果某些组合正常运行,它们可能会在程序中引入错误,而这些优化本来可以正常运行。在某些情况下,语言规范会指出“您必须以这种方式进行操作,因为可能会发生这类优化,并且程序将失败”,在这种情况下,这是代码中的错误。但是有时编译器具有(通常是可选的)优化功能,该功能可能生成错误的代码,因为编译器过于努力地优化代码或无法检测到该优化是不合适的。

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

CERT在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语言规范是开发人员和编译人员之间的合同,但是协议的条款可能会随着时间的推移而更改,并且并非每个人都了解每个细节,或者同意这些细节甚至是明智的。

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


24
关闭优化的一个更常见的原因是,打开优化通常会更困难。
Craig Stuntz 2010年

5
非易失性读取是由编译器(或运行时)优化引起的不当行为的一个很好的例子,尽管我不确定这是否会归类为“错误”,因为这是开发人员的责任对于这样的事情。
亚当·罗宾逊

7
给出的示例是为什么存在“ volatile”关键字的原因。这是一个代码错误。无错误的编译器即使进行了优化也不会引入错误。
phkahler

9
@先生。Shiny已经给出了他相信为什么可以关闭优化的理由,但这并不是毫无争议的,我怀疑大多数编译器文档中都对它进行了解释。当进行调试时,人们通常希望机器代码可以轻松地与原始代码相关联,因此可以在关闭优化的构建中使用,而QA构建(以及最终版本)将启用优化。发现@Mr所描述的问题之类的任何人。有光泽,然后通过关闭优化来“修复”它在我看来是修复了错误的事情。
乔治·霍金斯

3
@ShinyandNew安宇先生您一次又一次地错过了重点。关于您提供的链接中的示例,编译器优化未引入任何错误。该漏洞已经由程序员在不进行NULL检查的情况下进行行struct sock *sk = tun->sk;引用时出现tun。其次,C ++标准指出NULL是指向任何对象的指针,并且不能取消引用。基于此,编译器对代码进行了优化,这完全可以并符合标准。
哈迪·布雷斯

32

当错误通过禁用优化而消失时,大多数情况下这仍然是您的错

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

在那段时间里,我可以确认启用主动式优化后会发生一次代码生成错误。

那我为什么要抱怨呢?因为在同一时间,有许多错误使我怀疑编译器-但是事实证明,这是我对C ++标准的理解不足。该标准为编译器可能会或可能不会使用的优化留有余地。

多年来,在不同的论坛上,我看到很多文章都将责任归咎于编译器,最终导致原始代码中的错误。毫无疑问,它们中的许多模糊的bug需要对标准中使用的概念进行详细的了解,但是源代码的bug仍然如此。

为什么我这么晚答复:在确认这实际上是编译器的错误之前,不要责怪编译器。


13

编译器(和运行时)优化肯定会引入不希望的行为-但至少仅在您依赖未指定的行为(或确实对正确指定的行为做出错误假设)时才发生。

现在,除此之外,编译器当然可以包含错误。其中一些可能是周围的优化,以及影响可能是非常微妙的-事实上他们可能是,如明显的错误是更容易被固定。

假设您将JIT作为编译器,我已经看到.NET JIT和Hotspot JVM的发行版中的错误(不幸的是,目前我没有详细信息),这些错误在特别奇怪的情况下是可以重现的。我不知道它们是否是由于特定的优化。


我在下面的文章中指出了一个众所周知的问题,C ++的优化器将使用双重检查的锁定模式引入一个错误。就当前的C ++规范而言,这是正确的行为,它不是编译器错误,它是经过良好指定的行为,可以在关闭优化的情况下很好地工作,但是在打开优化时会中断。
tloach 2010年

@Jon Skeet:我从不投票(很好,几乎没有),但是在172k时,您甚至注意到了吗?:-)
Mike Dunlavey 2010年

11

合并其他帖子:

  1. 像大多数软件一样,编译器偶尔也会在代码中出现错误。“聪明人”的论点与此完全无关,因为由聪明人建造的NASA卫星和其他应用程序也存在错误。执行优化的代码与未执行优化的代码不同,因此,如果错误恰好在优化器中,则实际上您的优化代码可能包含错误,而未优化的代码则不会。

  2. 正如Shiny和New先生所指出的那样,对于并​​发和/或时序问题很幼稚的代码在没有优化的情况下可以令人满意地运行,而在优化过程中却失败了,因为这可能会改变执行时间。您可以将这样的问题归咎于源代码,但是如果仅在优化后才出现,那么有些人可能会归咎于优化。


8

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

此后显然已解决


1
显然,这是编译器中的错误。当然,编译器可以(确实!)存在错误:它们是软件。我对优化代码中并非由编译器错误引起的错误(异常/意外行为)示例更感兴趣。
R. Martinho Fernandes

8

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

而且最近我遇到了一个编译器指令“删除”了一个错误的情况。当然,该错误确实仍然存在,但是在我正确修复程序之前,我有一个临时解决方法。


++特别是在Fortran中。我们一直在遇到Fortran可以正常工作的情况,但前提是您使用特定的选择级别。并在IDE下运行Fortran?优化器(即使您不要求这样做)在哪里自由地完全加扰代码并改组变量?全部以“优化”为名?给我一个怪胎
Mike Dunlavey 2010年

1
@Mike-我很痛苦,我最近的情况是我正在从一个群集移植到另一个“相同”的Fortran程序。IDE?当然,您的意思是Emacs :-)
高性能Mark

抱歉,我陷入了Windows世界。Fortran是个热门土豆-MS,DEC,Compaq,现在是Intel。我想念养父母吗?它可以在.net下运行,但前提是您的老板不断为升级付费。再加上GCC,那里有可爱的GDB。有人告诉我,不管喜欢与否,“ Fortran就像摇滚乐一样,它永远不会死!”
Mike Dunlavey 2010年

7

是。一个很好的例子是经过仔细检查的锁定模式。在C ++中,没有办法安全地执行双重检查锁定,因为编译器可以按在单线程系统中有意义的方式对指令进行重新排序,而在多线程系统中则不然。可以在http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf上找到完整的讨论


正如您似乎指出的那样,这是C ++的缺点,并不是优化编译器的问题。即使没有优化,这也是一个问题。
phkahler

1
一点也不。如果没有优化,则双重检查锁定就可以了。这是一种痛苦的错误,因为它可以在调试版本中正常工作。仅仅是因为允许优化器进行与单线程系统中等效的逻辑更改,才有问题。您可以声称这是优化程序在C ++中允许的问题,但是仍然是在打开优化的情况下进行编译可能导致问题的领域。
tloach 2010年

这就是C11 / C ++ 11引入线程感知内存模型和的原因std::atomic。C ++ 11之前的无锁原子和无所不能的锁始终高度依赖于关于编译器如何工作(并且经常需要inline-asm)和滥用的许多假设volatile
彼得·科德斯

6

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


6

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

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

更明显的例子:VS2008 vs GCC

声明:

Function foo( const type & tp ); 

称为:

foo( foo2() );

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

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


5

混叠会导致某些优化出现问题,这就是为什么编译器可以选择禁用这些优化。从维基百科

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


4

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

积极的优化可能会缓存您的变量,甚至可能做出奇怪的假设。问题不仅在于代码的稳定性,还在于它们可能使调试器蒙骗。我见过几次调试器无法表示内存内容,因为某些优化在微控制器的寄存器中保留了一个变量值

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


3
我过去写过一些无错误的程序。“ Hello world”之类的。:P
R. Martinho Fernandes

3
我编写对安全性至关重要的硬实时软件,并且始终以最大的优化来进行构建。如果有一个编译器错误,我想早点找到它,不要等到我们用完CPU时间和日历时间并且有人说“ -O3”可以帮助您之后再发布错误代码,因为我们没有进行足够的测试。如果您不信任自己的工具,请不要使用它们。
phkahler

@Martinho Fernandes:是的,甚至有一句话:是否有可能编写一个完全没有错误的程序?是的,但这将毫无用处。
sharptooth 2010年

@phkahler我见过DO-178b认证的项目,其中有些DAL-A级别的设计委员会禁止任何类型的编译器优化。通常甚至禁止线程。我已经对编译器进行了一些研究,遇到了许多人,他们同意C ++和Java语言在设计上存在非常严重的缺陷(因此,您似乎在DAL-A软件中看不到它们),尽管人们尝试使用它或忽略它们。我信任的工具很少,而我的工作大部分压力是因为我被迫使用越野车库和/或工具
SystematicFrank 2010年

1
@phkahler:就像您说的那样,您必须信任您的工具或寻找其他人来替换它们。我碰巧知道编译器和优化器是由一些非常聪明的人(虽然不是我(:())编写的,并且由独立团队维护的前端,中间和后端代码和优化器质量应该提高。规则:根据通用规则编写优化器并产生一些可能称为错误的错误(易失性示例),而不是尝试为规则指定每个单独的异常以及难以维护的风险肿的代码。+1
Olof Forshell

3

从理论上讲,这是肯定的。但是,如果您不信任工具可以执行应做的事情,为什么要使用它们呢?但是马上,有人在争论

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

愚蠢的争论。

因此,除非您有理由相信编译器正在这样做,否则为什么要摆姿势呢?


1
这更像是一种直觉。我信任该工具,但是我敢肯定,编译器优化有时可以完成您无法做的事情。我只是不能举一个例子。也许我错了,没有这样的例子。
ereOn 2010年

好了,有一些示例(在上面和评论中提供),但是看起来像个愚蠢的论点(特别是您的朋友“支持论点”),直到它成为一个真正的问题为止。但是您确实是正确的,这是绝对可能的。
Daniel DiPaolo 2010年

当代码未达到预期的性能时,检查编译器输出的功能通常很有用。在某些情况下,这种检查会导致C规范中发现一个奇怪的极端情况,这意味着实际上不能保证代码可以按预期工作。在其他情况下,它会导致发现真正的编译器错误(例如,使用专用的CPU寄存器来保存变量,但随后将其用于专用目的而不先保存它)。
supercat 2014年

3

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

另一方面,经验告诉我们,商业编译器的可靠性很高。我有很多次有人告诉我,程序无法正常工作的原因一定是由于编译器中的错误,因为他已经非常仔细地检查了程序,并且他确定程序是100%正确的...然后我们发现实际上程序有错误,而不是编译器。我试图考虑的是我个人遇到的事情,我确实确定这是编译器中的错误,我只能想起一个例子。

因此,通常来说:信任您的编译器。但是他们曾经做错吗?当然。



2

我记得,早期的Delphi 1有一个错误,使Min和Max的结果相反。仅当在dll中使用浮点值时,才会出现带有一些浮点值的模糊错误。诚然,已经有十多年了,所以我的记忆可能有点模糊。


2

我在.NET 3.5中遇到了问题,如果您进行优化构建,则将另一个变量添加到方法中,该方法的命名与在相同范围内相同类型的现有变量的名称类似,那么这两个变量(新变量或旧变量)将不会在运行时有效,并且对无效变量的所有引用都替换为对另一个变量的引用。

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

我想在做Silverlight应用程序时,.NET 4.0和C#也遇到了这个问题。在我的上一份工作中,我们经常在C ++中遇到这个问题。可能是因为编译花费了15分钟的时间,所以我们只会构建所需的库,但是有时即使添加了新代码并且未报告任何构建错误,优化后的代码也与以前的构建完全相同。

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


2

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

与编译器代码中的错误相比,您的代码中的错误的可能性要大得多(数千倍):因为对编译器进行了广泛的测试。由TDD加上几乎所有自发布以来使用过的人!因此,您几乎不可能发现错误,而几乎没有其他人使用它数十万次才发现错误。

一个休眠的错误或隐藏的错误就是这样不透露自己的程序员又一个错误。可以声称自己的C ++代码没有(隐藏)错误的人非常罕见。它需要C ++知识(很少有人可以声称这一点)和广泛的代码测试。它不仅与程序员有关,还与代码本身(开发风格)有关。易于出错的是代码(测试的严格程度)或/和程序员(测试的纪律如何以及对C ++和编程的了解程度)的特征。

安全性+并发性错误:如果我们将并发性和安全性包含为错误,那就更糟了。但是毕竟,这些“是”错误。首先,就并发性和安全性而言,编写无缺陷的代码几乎是不可能的。这就是代码中始终存在错误的原因,可以在编译器优化中发现(或忽略)错误。


1

如果您编译的程序具有良好的测试套件,则可以启用更多且更积极的优化。然后可以运行该套件,并可以更确定地确保程序正常运行。另外,您可以准备自己的测试,这些测试与计划在生产中进行的匹配非常接近。

的确,任何大型程序都可能(实际上可能确实)有一些错误,这些错误与您使用哪个开关进行编译无关。


1

我从事大型工程应用程序的工作,时不时地看到仅发行崩溃和客户报告的其他问题。我们的代码包含37个文件(约6000个文件),该文件位于文件顶部,以关闭优化以修复此类崩溃:

#pragma optimize( "", off)

(我们使用的是Microsoft Visual C ++ native,2015,但几乎对于任何编译器都是如此,但也许英特尔Fortran 2016 update 2尚未进行任何优化。)

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

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


0

您可能想像到的对程序执行的所有操作都会引入错误。


1
我个人不认为ereOn想要开个玩笑的答案。不过,也许您可​​以将“将”更改为“可以”,以便您的答案至少在一定程度上是正确/合理的?
2012年

@Trisped我的回答异想天开,但我认为是正确的。编译器优化有时会引入错误吗?是。说不,那绝对是不可能的,这意味着编译器作者是绝对的编程之神,这当然是错误的。编译器优化经常会引入错误吗?不。它们通常都经过良好测试。但是,我们当然必须说出哪个编译器。
杰伊

0

由于详尽的测试和实际C ++代码的相对简单性(C ++的关键字/运算符少于100个),编译器错误很少见。糟糕的编程风格通常是遇到它们的唯一途径。通常,编译器会崩溃或产生内部编译器错误。此规则的唯一例外是GCC。GCC,尤其是较旧的版本,在O3甚至其他O级别启用了许多实验性优化。GCC还针对这么多的后端,从而在中间表示中留出了更多的漏洞。


-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(); }

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.