编译器最佳化会引入虫子吗?

今天我和一个朋友讨论了几个小时关于“编译器最佳化”的问题。

我为编译器最佳化可能引入 bug 或者至少是不希望出现的行为的观点进行了辩护。

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

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

谁在这里?如果我是,你有没有一个现实生活中的例子,一个编译器最佳化在生成的软件中产生了一个 bug?如果我弄错了,我应该停止编程而学习钓鱼吗?

26027 次浏览

理论上是有可能的。但是,如果你不相信这些工具能够完成它们应该完成的工作,为什么还要使用它们呢?但是现在,任何一个站在

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

正在做一个愚蠢的论点。

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

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

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

更新: 正如 Adam Robinson 在评论中指出的,我上面描述的场景更像是一个编程错误,而不是优化器错误。但是,我想说明的是,一些程序,在其他方面是正确的,结合一些优化,在其他方面是正确的,可以在程序中引入错误,当它们结合在一起时。在某些情况下,语言规范说“您必须这样做,因为这些类型的优化可能会发生,您的程序将会失败”,在这种情况下,这是代码中的一个 bug。但是有时编译器有一个(通常是可选的)优化特性,这个特性可能会生成不正确的代码,因为编译器过于努力地优化代码,或者无法检测到优化是不正确的。在这种情况下,程序员必须知道什么时候可以安全地打开所讨论的优化。

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

CERT 有一个题为“危险的优化和因果关系的丧失”的演示文稿,作者是 Robert C. Seacord,其中列出了许多在程序中引入(或暴露) bug 的优化。它讨论了各种可能的优化,从“做硬件所做的”到“捕获所有可能的未定义行为”到“做任何不被禁止的事情”。

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

  • 检查是否溢出

    // fails because the overflow test gets removed
    if (ptr + len < ptr || ptr + len > max) return EINVAL;
    
  • Using overflow artithmetic at all:

    // The compiler optimizes this to an infinite loop
    for (i = 1; i > 0; i += i) ++j;
    
  • Clearing memory of sensitive information:

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

The problem here is that compilers have, for decades, been less aggressive in optimization, and so generations of C programmers learn and understand things like fixed-size twos complement addition and how it overflows. Then the C language standard is amended by compiler developers, and the subtle rules change, despite the hardware not changing. The C language spec is a contract between the developers and compilers, but the terms of the agreement are subject to change over time and not everyone understands every detail, or agrees that the details are even sensible.

This is why most compilers offer flags to turn off (or turn on) optimizations. Is your program written with the understanding that integers might overflow? Then you should turn off overflow optimizations, because they can introduce bugs. Does your program strictly avoid aliasing pointers? Then you can turn on the optimizations that assume pointers are never aliased. Does your program try to clear memory to avoid leaking information? Oh, in that case you're out of luck: you either need to turn off dead-code-removal or you need to know, ahead of time, that your compiler is going to eliminate your "dead" code, and use some work-around for it.

有可能吗?虽然不是主流产品,但也可能是 当然。编译器优化是生成的代码; 无论代码来自哪里(您编写它或由其他东西生成它) ,它都可能包含错误。

仅举一个例子: 几天前,有人使用 -foptimize-sibling-calls选项(-O2暗示了这一点)生成了一个 Emacs 可执行文件,该文件在启动时出现了 Segfault 错误。

这是 显然已经修好了

编译器(和运行时)优化当然可以引入 不受欢迎行为——但是至少只有当你依赖于未指定的行为(或者确实对指定的行为做出不正确的假设)时,应该才会发生。

除此之外,编译器当然也可能存在 bug。其中一些可能与优化有关,其影响可能非常微妙——实际上它们是 很有可能,因为明显的 bug 更有可能被修复。

假设您将 JIT 作为编译器包含在内,我已经在两个。NET JIT 和 Hotspot JVM (不幸的是,我现在还没有详细的资料) ,它们在特别奇怪的情况下是可重现的。我不知道它们是否是由于特殊的优化。

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

我最近遇到过这样的情况: 编译器指令“移除”了一个 bug。当然,bug 仍然存在,但是我有一个临时的解决方案,直到我正确地修复了程序。

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

合并其他职位:

  1. 和大多数软件一样,编译器的代码中偶尔也会有 bug。“聪明人”的论点与此完全无关,因为 NASA 的卫星和其他由聪明人开发的应用程序也有漏洞。进行优化的代码与不进行优化的代码是不同的,所以如果错误恰好出现在优化器中,那么优化的代码确实可能包含错误,而非优化的代码则不会包含错误。

  2. 正如 Shiny 先生和 New 先生指出的那样,对于并发性和/或计时问题来说很幼稚的代码有可能在没有优化的情况下运行得令人满意,但是在优化的情况下却失败了,因为这可能会改变执行的时间。您可以将这样的问题归咎于源代码,但是如果它只在优化时才显示出来,那么有些人可能会将其归咎于优化。

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

打开生成的程序集很明显,编译器已经删除了该函数80% 的功能。重新编写代码以避免使用未定义行为。

更明显的例子: VS2008 vs GCC

声明:

Function foo( const type & tp );

名称:

foo( foo2() );

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

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

有可能发生,甚至影响了 Linux

是的,编译器优化可能是危险的。通常硬实时软件项目禁止优化正是出于这个原因。不管怎样,你知道有没有没有漏洞的软件吗?

积极的优化可能会缓存变量,甚至对变量进行奇怪的假设。问题不仅在于代码的稳定性,还在于它们会欺骗调试器。我曾多次看到一个调试器无法表示内存内容,因为一些优化在 micro寄存器中保留了一个变量值

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

我当然同意这样说是愚蠢的,因为编译器是由“聪明人”编写的,因此他们是绝对正确的。兴登堡号和塔科马海崃吊桥也是聪明人设计的。即使编译器编写者确实是最聪明的程序员,但编译器也确实是最复杂的程序之一。他们当然有虫子。

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

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

任何你能想象到的对程序所做的事情都会引入 bug。

别名可能会导致某些优化问题,这就是为什么编译器可以选择禁用这些优化:

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

据我回忆,早期的 Delphi 1有一个 bug,其中 Min 和 Max 的结果是相反的。只有在 dll 中使用浮点值时,才会出现一个含有一些浮点值的模糊错误。不可否认,已经过去十多年了,所以我的记忆可能有点模糊。

当一个 bug 通过禁用优化而消失时,大多数情况下仍然是你的错

我负责一个商业应用程序,大部分是用 C + + 编写的——从 VC5开始,早期移植到 VC6,现在成功移植到 VC2008。在过去的10年里,它增长到超过100万条。

在这段时间里,我可以确认一个单独的代码生成错误,这个错误是在启用了主动优化的情况下发生的。

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

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

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

由于 详尽无遗测试和实际 C + + 代码相对简单(C + + 有低于100个关键字/运算符) ,编译器的错误相对较少。糟糕的编程风格往往是唯一遇到的问题。通常编译器会崩溃或者产生一个内部编译器错误。这条规则的唯一例外是海湾合作委员会。GCC,特别是旧版本,在 O3中启用了许多实验性优化,有时甚至在其他 O 级别也启用了这些优化。GCC 还针对如此多的后端,以至于在它们的中间表示中留下了更多的 bug 空间。

我有一个问题。NET 3.5如果你使用优化构建,添加另一个变量到一个方法中,这个方法的命名类似于同一作用域中同一类型的现有变量,那么两个变量中的一个(新变量或旧变量)在运行时将无效,所有对无效变量的引用将被替换为对另一个变量的引用。

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

我想我曾经遇到过几次这样的问题。NET 4.0和 C # 当做 Silverlight 应用程序也。在我上一份工作中,我们经常在 C + + 中遇到这个问题。这可能是因为编译需要15分钟,所以我们只能构建我们需要的库,但有时优化的代码与前一个构建完全一样,即使添加了新代码并且没有报告构建错误。

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

如果您编译的程序具有良好的测试套件,则可以启用更多、更积极的优化。然后就可以运行这个套件,并且更加确信程序运行正确。此外,您还可以准备自己的测试,这些测试与您计划在生产环境中进行的测试非常匹配。

任何大型程序都可能独立地存在(而且很可能确实存在)一些 bug,您使用哪些开关来编译它。

我昨天在.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。在你的 C + + 代码中可能有一个你不知道的 bug,你只是没有看到它。在这种情况下,这是一个隐藏的或休眠的错误,因为该分支的代码没有执行[足够的次数]。

代码中出现错误的可能性比编译器代码中出现错误的可能性大得多(几千倍) : 因为编译器经过了广泛的测试。由 TDD 加上实际上所有的人谁已经使用它们,因为他们的发布!).因此,一个 bug 被你发现而不被其他人发现几十万次的可能性是微乎其微的。

休眠的虫子或隐藏的 bug 只是一个还没有向程序员显示的 bug。能够声称自己的 C + + 代码没有(隐藏的) bug 的人非常少见。它需要 C + + 的知识(很少有人可以声称自己有这方面的知识)和对代码的广泛测试。这不仅仅关系到程序员,还关系到代码本身(开发的风格)。容易出错是代码的特点(测试的严格程度)或者/和程序员的特点(测试的纪律性以及对 C + + 和编程的了解程度)。

安全性 + 并发性错误: 如果我们将并发性和安全性作为错误包括在内,情况会更糟。但毕竟,这些“是”虫子。编写一个首先在并发性和安全性方面没有 bug 的代码几乎是不可能的。这就是为什么在代码中总是存在一个 bug,这个 bug 可以在编译器最佳化中被发现(或者被遗忘)。

我在一个大型工程应用程序上工作,有时候我们只看到发布崩溃和客户报告的其他问题。我们的代码有37个文件 6000)我们在文件的顶部有这个,关闭优化来修复这样的崩溃:

#pragma optimize( "", off)

(我们使用的是 Microsoft Visual C + + 原生版,2015年,但对于几乎任何编译器都是如此,除了 Intel Fortran 2016更新2,我们还没有进行任何优化。)

如果您搜索 MicrosoftVisualStudio 反馈站点,也可以在那里找到一些优化错误。我们偶尔会记录一些我们的日志(如果你可以用一小段代码很容易地复制它,而且你愿意花时间) ,它们确实得到了修复,但遗憾的是其他的又被引入了。微笑

编译器是由人编写的程序,任何大程序都有 bug,相信我。编译器最佳化选项肯定存在 bug,开启优化肯定会在程序中引入 bug。