编译器为何如此可靠?


62

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

...怎么他们让编译器非常可靠?


16
好吧,他们在其中编译了编译器……
Michael K

31
他们不是万无一失的。有编译器错误-只是它们非常罕见。
克里斯·

5
随着代码堆栈的下降,错误变得越来越罕见:应用程序错误比编译器错误更为常见。编译器错误比CPU(微码)错误更常见。这实际上是个好消息:您能想象这是否相反吗?
Fixee 2011年

您可能会观察到确实存在很多错误(例如sdcc!)的编译器与gcc这样的编译器(后者更加健壮和可靠)有何不同,从而学到了一些东西。
本杰克逊

Answers:


96

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

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

此外,这些错误通常也易于复制:除了确切的平台和编译器版本信息之外,通常您所需要的只是一段输入代码。更不用说编译器用户(即开发人员自己)往往提供比任何普通计算机用户都更加精确和详细的错误报告:-)


32
再加上许多编译器代码可能被证明是正确的。
biziclop 2011年

@biziclop,很好,这是任务特殊性质的另一个结果。
彼得Török

John Backus在1957年为FORTRAN语言编写了第一个完整的编译器。因此,您可以看到,编译器技术已有50多年的历史了。尽管有其他人指出,编译器确实存在错误,但我们已经花了很多时间来解决问题。
leed25d 2011年

@biziclop,实际上,甚至可以从语法自动生成诸如词法分析器和解析器之类的某些组件,这再次降低了发生错误的风险(假设词法分析器/解析器生成器很健壮-出于与上述相同的原因,它们通常是可靠的) 。
彼得Török

2
@Péter:在更广泛使用的编译器中,Lexer /解析器生成器似乎很少见-出于各种原因,大多数人手动编写lexer和解析器,包括速度和缺乏针对所讨论语言的足够聪明的解析器/ lexer生成器(例如C )。

61

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

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

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

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

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

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

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

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

编译器给出了三个错误。

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

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

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

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


4
在第一个错误消息之后发送好的错误消息非常困难。

Sureöy必须花更多的精力才能使编译器完全“傻瓜式” :)
Homde 2011年

2
@MKO:当然。许多错误无法修复。有时,修复程序是如此昂贵,而情况却如此晦涩,以至于成本不能由收益来证明。有时,足够多的人开始依赖于您必须保持这种“越野车”行为。
埃里克·利珀特

mmm ...最终在错误消息中出现的错误是“正常的”。总是可以对代码进行一些修改以使其正常工作。那些编译器接受源代码并产生“错误的”汇编输出的错误呢?太可怕了
Gianluca Ghettini '16

7
@aij:在“显然合法的C#代码”意义上正确。例如,您是否编写过一个包含一个接口的程序,该接口继承了两个接口,其中一个接口具有一个属性,另一个接口具有与该属性同名的方法?快速,无需查看规格:这合法吗?现在假设您已经调用了该方法。是am昧的吗?等等。人们写的代码一直都没有做到他们的意思。但是,他们很少会编写代码,而您必须成为规范专家才能说出它是否甚至是合法的C#。
埃里克·利珀特

51

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

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 ++这样的其他语言,我什至不想开始。

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

认真地说,我想相信的编译器开发人员是优秀的程序员,尽管他们并非万无一失,但它们的确给我带来了很大的帮助。


19

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

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


6
+1表示“将编译器视为标准”。长期以来,我一直坚持认为有两件事真正定义了一种语言:编译器和标准库。标准文档只是文档。
梅森惠勒

8
@Mason:这对于具有一种实现的语言非常有效。对于许多语言,该标准至关重要。现实生活中的影响是,如果您抱怨某事,那么如果这是标准问题,供应商将认真对待您,如果这是未定义的行为或类似行为,则将您拒之门外。
David Thornley,

2
@Mason-这仅仅是因为很少有语言遵守和/或遵守标准。顺便说一句,恕我直言,这不是一件好事-对于任何认真的开发,预计将持续多代OS。
Rook

1
@David:或更准确地说,一个主要的实现。无论ANSI和ECMA怎么说,Borland定义Pascal,Microsoft定义C#。
dan04 2011年

4
在高度优化下C,C ++或Fortran代码崩溃比编译器错误更经常是错误的输入代码。我经常使用最新的和预发行版的编译器进行工作,并且经常使用非常新的硬件,并且经常看到与优化相关的失败。由于这些语言具有未定义行为的概念,并且未指定不符合标准程序的处理方式,因此必须非常仔细地检查崩溃,最终针对程序集进行检查。在80-90%的情况下,应用程序代码错误,而不是编译器错误。
菲尔·米勒

14

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

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

2
同样,在许多情况下,代码很旧,GCC与其他代码一样已有20多年的历史了,因此许多错误已在很长的时间内解决。
Zachary K

13

我们每天使用编译器

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

他们没有。我们的确是。由于每个人都一直在使用它们,因此可以迅速发现错误。

这是一个数字游戏。由于编译器是习惯于如此普遍地,这是极有可能的任何错误都会有人被触发,但因为有这么大量的用户,这是非常不可能的是,有人会在你明确。

因此,这取决于您的观点:在所有用户中,编译器都是错误的。但是很可能其他人会在您之前编译过类似的代码,因此,如果他们一个错误,那么它就会击中他们,而不是您,因此从您个人的角度来看,该错误看起来像是永远不会。

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


11

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

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


7

我经常遇到编译器错误。

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

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

我们发现IA64(Itanium)有很多问题。该平台我们的客户并不多,因此减少优化级别是我们通常的错误修正。这又回到了其他答案,用于流行架构的流行语言的编译器通常具有足够的用户曝光度和足够好的支持,但是当您使用不太流行的架构和/或语言时,应该期望可靠性受到损害。
欧米茄半人马座

@Omega:削减优化似乎是每个人都要做的。不幸的是,Itanium 需要高度优化的编译器才能运行良好。噢...
Zan Lynx

我听到你了 坦率地说,该架构在问世时已经过时了,幸运的是AMD用x86-64强迫了Intel的手(尽管它有很多缺点也还算不错)。如果您可以分解源文件,则可以找出问题所在,并找到解决方法。如果这是一个重要的平台,那就是我们要做的,但对于IA64,不是。
欧米茄半人马座

@Omega:不幸的是,我真的很喜欢Itanium。这是一个很棒的架构。我认为x86和x86-64已过时,但是它们永远不会消失。
Zan Lynx

x86有点奇怪。他们不断向其中添加新的东西,因此一次增加了一个疣。但是,乱序执行引擎运行得很好,新的SSE => AVX东西为那些愿意为其编写代码的人提供了一些真正的功能。诚然,有很多晶体管致力于做半陈旧的东西,但这就是传统兼容性的价格。
欧米茄半人马座

5

几个原因:

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

4

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

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


4

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


不幸的是,我们看不到第二类错误:代码可以编译=一切都很好。因此,大概一半的错误(假设两个错误类之间的比率为50-50)不是人为发现的,而是通过编译器单元测试发现的
Gianluca Ghettini '16

3

是的,昨天我在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

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

对!

最难忘的两个是我遇到的前两个。它们都在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为目标),另一部分将其转换为实际目标硬件的指令。在多平台编译器中,第一部分无处不在,因此需要进行大量的实际测试。

避免自我修改代码的原因之一。
Technophile

3

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

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


2

当使用-O0和-O2进行编译时,如果软件的行为不同,则说明存在编译器错误。

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


8
不必要。在C和C ++中,存在大量令人烦恼的未指定和未定义的行为,并且可以根据月亮的优化级别或相位或道琼斯指数的移动来合理地变化。该测试确实可以使用更严格定义的语言。
David Thornley,

2

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

在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编译器的链接器中有一个漂亮的错误,它将分段错误并无缘无故地死掉。干净的构建通常会修复它(但并非总是感叹)。


2

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


1

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

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

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

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


1

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

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

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

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.