it-swarm.cn

编译器为何如此可靠?

我们每天都在使用编译器,就像它们的正确性一样,但是编译器也是程序,并且可能包含错误。我一直想知道这种可靠的鲁棒性。您是否曾经在编译器中遇到错误?这是什么,您如何意识到问题出在编译器本身?

...以及do如何使编译器如此可靠?

67
EpsilonVector

随着时间的流逝,成千上万的开发人员对它们进行了彻底的测试。

而且,要解决的问题已经很好地定义了(通过非常详细的技术规范)。任务的性质使其很容易进行单元/系统测试。即它基本上是将一种非常特定的格式的文本输入转换为另一种定义明确的格式(某种字节码或机器码)的输出。因此,创建和验证测试用例很容易。

而且,这些错误通常也很容易重现:除了确切的平台和编译器版本信息之外,通常您所需要的只是一段输入代码。更不用说编译器用户(即开发人员自己)提供的错误报告要比任何普通计算机用户都多得多:

101
Péter Török

除了到目前为止所有的好答案:

您有一个“观察者偏见”。您没有观察到错误,因此您假设没有错误。

我曾经想像你一样。然后,我开始专业地编写编译器,让我告诉您,其中存在许多错误!

您看不到错误,因为您编写的代码就像人们编写的所有其他代码中的99.999%一样。您可能编写了完全正常,简单,清晰正确的代码,这些代码调用方法并运行循环,并且没有任何花哨或怪异的事情,因为您是解决常规业务问题的普通开发人员。

您看不到任何编译器错误,因为这些编译器错误不在易于分析的简单普通代码方案中。这些错误在于您未编写的怪异代码的分析中。

另一方面,我有相反的观察者偏见。我每天都看到疯狂的代码,因此对我而言,编译器似乎充满了错误。

如果您坐下来使用任何一种语言的语言规范,并采用该语言的任何编译器实现,并真的要努力确定编译器是否确实实现了该规范,那么只需专注于晦涩难解的案例,您很快就会发现编译器错误非常频繁。让我举一个例子,这是我在五分钟前发现的一个C#编译器错误。

static void N(ref int x){}
...
N(ref 123);

编译器给出了三个错误。

  • Ref或out参数必须是可分配的变量。
  • N(ref int x)的最佳匹配具有无效参数。
  • 参数1缺少“ ref”。

显然,第一个错误消息是正确的,第三个错误消息是错误。错误生成算法试图找出第一个参数无效的原因,它对其进行了查看,发现它是一个常量,并且不返回源代码来检查它是否被标记为“ ref”;相反,它假定没有人会愚蠢到将常量标记为ref,并决定ref必须丢失。

目前尚不清楚正确的第三条错误消息是什么,但是不是。实际上,也不清楚second错误消息是否正确。重载解析是否应该失败,还是应该将“ ref 123”视为正确类型的ref参数?我现在必须考虑一下,并与分类小组进行讨论,以便我们可以确定正确的行为。

您从未见过此错误,因为您可能永远不会做任何愚蠢的事情来尝试通过ref传递123。如果这样做,您可能甚至不会注意到第三个错误消息是无意义的,因为第一个错误消息是正确的并且足以诊断问题。但是我确实尝试做这样的事情,因为我trying破坏了编译器。如果尝试过,您也会看到错误。

66
Eric Lippert

你在跟我开玩笑吗?编译器也有错误,可以加载。

GCC可能是地球上最著名的开源编译器,并查看其错误数据库: http://gcc.gnu.org/bugzilla/buglist.cgi?product=gcc&component=c%2B% 2B&resolution = ---

在GCC 3.2和GCC 3.2.3之间,看看有多少个错误已得到修复: http://gcc.gnu.org/gcc-3.2/changes.html

至于像Visual C++这样的其他语言,我什至不想开始。

如何使编译器可靠?首先,他们要承担单元测试的重担。整个星球都在使用它们,因此没有测试人员短缺。

认真地说,我想相信的编译器开发人员是优秀的程序员,尽管他们并非万无一失,但它们确实包装得相当不错。

54
Fanatic23

我一天遇到两三个。检测一个的唯一真实方法是查看汇编代码。

尽管出于其他原因指出了编译器的高度可靠性,但我认为编译器的可靠性通常是一种自我实现的评估。程序员倾向于将编译器视为标准。当出现问题时,您可以假设是您的错(因为发生错误的时间为99.999%),然后更改代码以解决编译器问题,而不是相反。例如,在高优化设置下崩溃的代码肯定是编译器错误,但是大多数人只是将其设置得较低,然后继续运行,而不会报告该错误。

21
Karl Bielefeldt

编译器具有若干个导致其正确性的属性:

  • 该领域是众所周知的,并且已经过研究。这个问题定义明确,提供的解决方案定义明确。
  • 自动化测试足以证明编译器正常工作
  • 编译器具有非常广泛的,通常是公共的,自动化的和单元测试,并且随着时间的推移,它们不断积累,以涵盖比大多数其他程序更多的错误空间
  • 编译器有大量的目光在注视着它们的结果
15
blueberryfields

我们每天使用编译器

...以及它们如何使编译器如此可靠?

他们没有。我们的确是。 因为每个人都一直使用它们,所以很快就发现了错误。

这是一个数字游戏。由于编译器的使用如此普遍,因此任何错误will很有可能被某人触发,但是由于用户数量众多,因此不太可能有人会专门为你。

因此,这取决于您的观点:在所有用户中,编译器都是错误的。但是很可能其他人会在您之前编译过类似的代码,因此,如果他们的was一个错误,它将击中他们,而不是您,所以从您的个体观点,似乎该错误从未出现过。

当然,最重要的是,您可以在此处添加所有其他答案:编译器已得到充分研究和理解。有一个神话,那就是它们很难编写,这意味着只有非常聪明,非常优秀的程序员才真正尝试编写一个,并且在编写时要格外小心。它们通常易于测试,也易于压力测试或模糊测试。编译器用户本身就是专家程序员,从而产生高质量的错误报告。反之亦然:编译器编写者往往是他们自己的编译器的用户。

14
Jörg W Mittag

除了所有答案之外,我还要添加:

相信很多时候,摊贩正在吃自己的狗食。意思是,他们正在自己编写编译器。

12
DevSolo

我经常遇到编译器错误。

您可以在测试人员较少的较暗角落找到它们。例如,要查找GCC中的错误,您应该尝试:

  • 建立一个交叉编译器。您会在GCC的configure和build脚本中发现数十个的错误。有些导致在GCC编译期间生成失败,而另一些导致交叉编译器无法生成有效的可执行文件。
  • 使用profile-bootstrap构建Itanium版本的GCC。最近几次我在GCC 4.4和4.5上尝试了此操作,但它未能产生有效的C++异常处理程序。未优化的版本运行良好。似乎没有人对修复我报告的错误感兴趣,在尝试挖掘GCC asm内存规范中的漏洞后,我自己放弃了对其进行修复。
  • 尝试使用最新的内容构建自己的工作GCJ,而不遵循发行版的构建脚本。我赌你。
8
Zan Lynx

几个原因:

  • 编译器作者“ 吃自己的狗粮”。
  • 编译器基于CS的众所周知的原理
  • 编译器的构建非常严格明确规范
  • 编译器得到经过测试
  • 编译器是并非总是很可靠
6
Kramii

他们通常非常擅长-O0。实际上,如果我们怀疑编译器错误,可以将-O0与尝试使用的级别进行比较。优化级别越高,风险就越大。有些甚至是故意的,并在文档中有这样的标记。我遇到了很多(在我的时间里至少有一百),但是最近变得越来越少了。然而,为了追求良好的规格数字(或其他对营销重要的基准),推动极限的诱惑很大。几年前,我们遇到了一个问题,一个供应商(不愿透露姓名)决定将括号默认为违背-而不是一些明确标记的特殊编译选项。

与说一个杂散的内存引用相比,可能很难诊断出编译器错误,使用不同选项进行重新编译可能只会扰乱数据对象在内存中的相对位置,因此您不知道它是源代码的Heisenbug还是越野车编译器。同样,许多优化对操作顺序进行合理的更改,甚至对代数进行简化,这些更改在浮点舍入和下溢/上溢方面将具有不同的属性。很难将这些影响与REAL bug分开。出于这个原因,硬核浮点计算很困难,因为错误和数值敏感性通常不容易分解。

5
Omega Centauri

编译器错误并不罕见。最常见的情况是编译器报告应接受的代码错误,或编译器接受应拒绝的代码。

5
kevin cline

您是否曾经在编译器中遇到错误?这是什么,您如何意识到问题出在编译器本身?

是的!

最难忘的两个是我遇到的前两个。它们都在1985-7年前后用于680x0 Mac的Lightspeed C编译器中。

第一个是在某些情况下,整数后递增运算符什么都不做-换句话说,在一段特定的代码中,“ i ++”根本不对“ i”做任何事情。我一直拉着头发,直到我看了一个分解。然后我只是以不同的方式进行了增量,并提交了错误报告。

第二个稍微复杂一些,并且确实是一个考虑不周的“功能”。早期的Mac具有用于执行低级磁盘操作的复杂系统。由于某种原因,我从未理解过-可能与创建较小的可执行文件有关-而不是编译器只是在目标代码中就地生成磁盘操作指令,Lightspeed编译器会调用内部函数,该内部函数在运行时生成磁盘操作指令放在堆栈上并跳转到那里。

在68000个CPU上效果很好,但是当在68020 CPU上运行相同的代码时,它通常会做一些奇怪的事情。事实证明,68020的新功能是原始指令256字节指令高速缓存。这是CPU缓存的早期,它没有缓存是“脏的”并且需要重新填充的概念。我猜摩托罗拉的CPU设计人员没有考虑过自动修改代码。因此,如果您在执行顺序中将两个磁盘操作紧密地结合在一起,并且Lightspeed运行时在堆栈的同一位置构建了实际指令,则CPU会错误地认为它命中了指令高速缓存,并两次运行了第一个磁盘操作。

同样,弄清楚这个问题需要花一些时间来进行反汇编程序的研究,并在低级调试器中进行很多单步调试。我的解决方法是在每个磁盘操作之前添加对执行256条“ NOP”指令的函数的调用,从而充斥(并清除了)指令缓存。

从那以后的25年间,随着时间的推移,我看到了越来越少的编译器错误。我认为有以下两个原因:

  • 编译器的验证测试越来越多。
  • 现代编译器通常分为两部分或更多部分,其中一部分生成平台无关的代码(例如LLVM以您可能认为的虚拟CPU为目标),另一部分将其转换为实际目标硬件的指令。在多平台编译器中,第一部分无处不在,因此需要进行大量的实际测试。
4
Bob Murphy

5.5年前在Turbo Pascal中发现了一个明显的错误。编译器的上一个(5.0)和下一个(6.0)版本中都不存在错误。而且它应该很容易测试,因为它根本不是一个危险的情况(只是一个不常用的调用)。

一般而言,商业编译器制造商(而不是业余项目)当然会具有非常广泛的质量保证和测试程序。他们知道他们的编译器是他们的旗舰项目,并且缺陷对他们而言非常糟糕,比他们对制造大多数其他产品的其他公司的看法更糟。软件开发人员是一群无情的人,我们的工具供应商让我们失望了,我们很可能会寻找替代方案,而不是等待供应商提供解决方案,并且我们很可能将这一事实传达给可能会跟随我们的同行例。在其他许多行业并非如此,因此,严重的错误给编译器制造商带来的潜在损失要远远大于视频编辑软件制造商。

4
jwenting

是的,昨天我在ASP.NET编译器中遇到一个错误:

在视图中使用强类型模型时,可以包含多少个参数模板是有限制的。显然,它不能接受超过4个模板参数,因此下面的两个示例使编译器无法处理的过多事情:

ViewUserControl<System.Tuple<type1, type2, type3, type4, type5>>

不会按原样编译,但如果type5 已移除。

ViewUserControl<System.Tuple<MyModel, System.Func<type1, type2, type3, type4>>>

如果type4 已移除。

注意 System.Tuple有很多重载,最多可以包含16个参数(我知道这很疯狂)。

3
user8685

发生编译器错误,但是您往往会发现它们在奇怪的角落。

在1990年代,Digital Equipment Corporation VAX VMS C编译器中出现一个奇怪的错误

(我当时腰带上戴着洋葱,当时很流行)

For循环之前任何位置的无关分号将被编译为for循环的主体。

f(){...}
;
g(){...}

void test(){
  int i;
  for ( i=0; i < 10; i++){
     puts("hello");
  }
}

在有问题的编译器上,循环仅执行一次。

看到

f(){...}
g(){...}

void test(){
  int i;
  for ( i=0; i < 10; i++) ;  /* empty statement for fun */

  {
     puts("hello");
  }
}

那花了我很多时间。

我们曾经(曾经)对工作经验强的学生使用的PIC C编译器的较旧版本无法生成正确使用高优先级中断的代码。您必须等待2-3年才能升级。

MSVC 6编译器的链接器中有一个漂亮的错误,它将分段错误并无缘无故地死掉。干净的构建通常会修复它(但sigh并非总是如此)。

3
Tim Williscroft

当使用-O0和-O2进行编译时,如果软件的行为不同,那么您会发现一个编译器错误。

当您的软件的行为与预期的行为完全不同时,则可能是该错误存在于您的代码中。

2
mouviciel

在某些领域,例如航空电子软件,对代码,硬件以及编译器的认证要求非常高。关于最后一部分,有一个项目旨在创建一个经过正式验证的C编译器,称为 Compcert 。从理论上讲,这种编译器与它们一样可靠。

2
Axel

我已经看到了几个编译器错误,并报告了一些自己的错误(特别是在F#中)。

就是说,我认为编译器错误很少见,因为编写编译器的人通常对计算机科学的严格概念非常熟悉,这些概念使他们真正意识到代码的数学含义。

他们中的大多数人大概对lambda演算,形式验证,指称语义等非常熟悉-像我这样的普通程序员几乎无法理解的东西。

而且,在编译器中通常存在从输入到输出的相当直接的映射,因此调试编程语言可能比调试博客引擎要容易得多。

2
Rei Miyasaka

我不久前在C#编译器中发现了一个错误,您可以看到Eric Lippert(属于C#设计团队的人)如何弄清楚该错误是 在这里

除了已经给出的答案之外,我还要添加一些其他内容。编译器设计师通常是非常优秀的程序员。编译器非常重要:大多数编程都是使用编译器完成的,因此必须保证编译器的质量。因此,使编译器的公司将最好的人放在上面(或者至少是非常好的人:最好的人可能不喜欢编译器设计)符合公司的最大利益。微软非常希望他们的C和C++编译器能够正常工作,否则公司的其余部分将无法完成工作。

另外,如果您要构建一个非常复杂的编译器,则不能仅将其一起破解。编译器背后的逻辑既高度复杂又易于形式化。因此,这些程序通常将以“健壮”且通用的方式构建,从而往往会减少错误。

2
Alex ten Brink