ere*_*eOn 68 compiler-construction optimization compiler-optimization
今天我和我的一个朋友进行了讨论,我们就"编译器优化"进行了几个小时的讨论.
我辩护说,有时,编译器优化可能会引入错误或至少是不良行为.
我的朋友完全不同意,他说"编译器是由聪明人构建并做聪明的东西",因此永远不会出错.
他根本没有说服我,但我不得不承认我缺乏现实生活中的例子来强化我的观点.
谁在这?如果是的话,您是否有任何现实生活中的例子,编译器优化会在生成的软件中产生错误?如果我错了,我应该停止编程并学习钓鱼吗?
Mr.*_* 安宇 41
编译器优化可能会引入错误或不良行为.这就是为什么你可以关闭它们.
例如:编译器可以优化对内存位置的读/写访问,例如消除重复读取或重复写入,或重新排序某些操作.如果所讨论的内存位置仅由单个线程使用并且实际上是内存,则可能没问题.但如果内存位置是硬件设备IO寄存器,那么重新排序或消除写入可能是完全错误的.在这种情况下,您通常必须编写代码,知道编译器可能会"优化"它,因此知道天真的方法不起作用.
更新:正如Adam Robinson在评论中指出的那样,我上面描述的场景更多的是编程错误,而不是优化器错误.但我试图说明的一点是,一些程序本来是正确的,加上一些优化,否则它们可以正常工作,当它们组合在一起时会在程序中引入错误.在某些情况下,语言规范说"你必须这样做,因为可能会发生这种类型的优化,你的程序也会失败",在这种情况下,这是代码中的错误.但有时编译器有一个(通常是可选的)优化功能,可以生成错误的代码,因为编译器太难以优化代码或无法检测到优化是不合适的.在这种情况下,程序员必须知道何时打开有问题的优化是安全的.
另一个例子:linux内核有一个错误,在该指针的测试为空之前,可能会取消引用一个可能的NULL指针.但是,在某些情况下,可以将内存映射到零地址,从而允许取消引用成功.在注意到指针被解除引用时,编译器假定它不能为NULL,然后删除后面的NULL测试和该分支中的所有代码.这在代码中引入了一个安全漏洞,因为该函数将继续使用包含攻击者提供的数据的无效指针.对于指针合法为空并且内存未映射到地址零的情况,内核仍然像以前一样OOPS.所以在优化之前,代码包含一个bug; 在它包含两个之后,其中一个允许本地root利用.
CERT在 Robert C. Seacord上发表了一篇名为"危险优化和因果关系丢失" 的演讲,其中列出了许多在程序中引入(或暴露)错误的优化.它讨论了各种可能的优化,从"做硬件做什么"到"捕获所有可能的未定义行为"到"做任何不被禁止的事情".
一些代码的例子在完全优化的编译器得到它之前是完全正常的:
检查溢出
// fails because the overflow test gets removed
if (ptr + len < ptr || ptr + len > max) return EINVAL;
Run Code Online (Sandbox Code Playgroud)完全使用溢出算法:
// The compiler optimizes this to an infinite loop
for (i = 1; i > 0; i += i) ++j;
Run Code Online (Sandbox Code Playgroud)清除敏感信息的记忆:
// the compiler can remove these "useless writes"
memset(password_buffer, 0, sizeof(password_buffer));
Run Code Online (Sandbox Code Playgroud)这里的问题是几十年来编译器在优化方面的积极性较低,因此几代C程序员学习和理解固定大小的二进制补码以及它如何溢出等问题.然后,编译器开发人员修改了C语言标准,尽管硬件没有变化,但细微的规则也会发生变化.C语言规范是开发人员和编译人员之间的合同,但协议的条款可能会随着时间的推移而变化,并不是每个人都了解每个细节,或者同意细节甚至是明智的.
这就是大多数编译器提供关闭(或打开)优化的标志的原因.你的程序写的是理解整数可能会溢出吗?然后你应该关闭溢出优化,因为它们可以引入错误.你的程序是否严格避免别名指针?然后,您可以打开假设指针永远不会出现别名的优化.您的程序是否尝试清除内存以避免泄露信息?哦,在这种情况下,你运气不好:你需要关闭死代码删除或者你需要提前知道你的编译器将消除你的"死"代码,并使用一些工作为此而言.
pet*_*hen 30
当一个错误通过禁用优化而消失时,大部分时间它仍然是你的错
我负责一个主要用C++编写的商业应用程序 - 从VC5开始,早期移植到VC6,现在成功移植到VC2008.它在过去10年中增长到超过100万行.
在那个时候,我可以确认在启用了激进优化时发生的单个代码生成错误.
那我为什么抱怨?因为在同一时间,有几十个错误使我怀疑编译器 - 但结果证明我对C++标准的理解不足.该标准为编译器可能使用或不使用的优化提供了空间.
多年来在不同的论坛上,我看到许多帖子都指责编译器,最终证明是原始代码中的错误.毫无疑问,其中许多模糊的bug需要详细了解标准中使用的概念,但源代码仍然存在错误.
为什么我这么晚才回复:在确认编译器确实是编译器的错误之前停止指责编译器.
Jon*_*eet 12
编译器(和运行时)优化肯定会带来不期望的行为-但它至少应该如果你依靠明确的行为(或者实际上作出有关精心指定的行为不正确的假设)才会发生.
除此之外,当然编译器可能会有错误.其中一些可能是围绕优化,其影响可能非常微妙 - 实际上它们很可能是,因为明显的错误更容易被修复.
假设你将JIT作为编译器包含在内,我已经看到了.NET JIT和Hotspot JVM的发布版本中的错误(不幸的是,目前我没有详细信息),这些错误在特别奇怪的情况下是可重现的.我不知道他们是否因特殊优化而受到影响.
Car*_*icz 10
要结合其他帖子:
编译器偶尔会在代码中出现错误,就像大多数软件一样."智能人"的论点与此完全无关,因为美国宇航局的卫星和智能人员制造的其他应用也存在漏洞.执行优化的编码与不编码的编码不同,因此如果错误恰好位于优化器中,那么您的优化代码确实可能包含错误,而非优化代码则不会.
正如Shiny和New先生所指出的那样,对于并发和/或时序问题而言天真的代码可以在没有优化的情况下令人满意地运行但是优化失败,因为这可能会改变执行的时间.您可以在源代码上归咎于这样的问题,但如果它只在优化时才会显示,有些人可能会责怪优化.
我从未听说过或使用过编译器,其指令无法改变程序的行为.通常这是一件好事,但它确实需要您阅读手册.
我有一个最近的情况,编译器指令"删除"了一个错误.当然,这个bug确实还在那里,但我有一个临时的解决方法,直到我正确修复程序.
是.一个很好的例子是双重检查锁定模式.在C++中,没有办法安全地实现双重检查锁定,因为编译器可以在单线程系统中以有意义的方式重新排序指令,但不能在多线程系统中重新排序.有关完整的讨论,请访问http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
我使用更新的编译器构建旧代码时遇到过这种情况.旧代码可以工作,但在某些情况下依赖于未定义的行为,例如不正确定义/强制转换运算符.它可以在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做了一些优化来解决这个问题,它可能会起作用.
别名可能会导致某些优化出现问题,这就是编译器可以选择禁用这些优化的原因。来自维基百科:
为了以可预测的方式启用此类优化,C 编程语言(包括其较新的 C99 版本)的 ISO 标准规定,不同类型的指针引用相同的内存位置是非法的(有一些例外)。这条规则被称为“严格别名”,可以显着提高性能[需要引用],但已知会破坏一些其他有效的代码。一些软件项目故意违反 C99 标准的这一部分。例如,Python 2.x 这样做是为了实现引用计数,[1] 并且需要对 Python 3 中的基本对象结构进行更改以启用此优化。Linux 内核这样做是因为严格的别名会导致内联代码优化出现问题。 [2] 在这种情况下,当用 gcc 编译时,