我们每天都在使用编译器,就像它们的正确性一样,但是编译器也是程序,并且可能包含错误。我一直想知道这种可靠的鲁棒性。您是否曾经在编译器中遇到错误?这是什么,您如何意识到问题出在编译器本身?
...怎么做他们让编译器非常可靠?
我们每天都在使用编译器,就像它们的正确性一样,但是编译器也是程序,并且可能包含错误。我一直想知道这种可靠的鲁棒性。您是否曾经在编译器中遇到错误?这是什么,您如何意识到问题出在编译器本身?
...怎么做他们让编译器非常可靠?
Answers:
随着时间的流逝,成千上万的开发人员对它们进行了彻底的测试。
而且,要解决的问题已经很好地定义了(通过非常详细的技术规范)。任务的性质使其很容易进行单元/系统测试。也就是说,它基本上是将一种非常特定的格式的文本输入转换为另一种定义明确的格式(某种字节码或机器码)的输出。因此,创建和验证测试用例很容易。
此外,这些错误通常也易于复制:除了确切的平台和编译器版本信息之外,通常您所需要的只是一段输入代码。更不用说编译器用户(即开发人员自己)往往提供比任何普通计算机用户都更加精确和详细的错误报告:-)
除了到目前为止所有的好答案:
您有一个“观察者偏见”。您没有观察到错误,因此您假设没有错误。
我曾经想像你一样。然后,我开始专业地编写编译器,让我告诉您,其中存在许多错误!
您看不到错误,因为您编写的代码就像人们编写的所有其他代码中的99.999%一样。您可能编写了完全正常,直接,清晰正确的代码,这些代码调用方法并运行循环,并且没有做任何花哨或怪异的事情,因为您是解决常规业务问题的普通开发人员。
您看不到任何编译器错误,因为这些编译器错误不在易于分析的简单普通代码方案中。这些错误在于您未编写的怪异代码的分析中。
另一方面,我有相反的观察者偏见。我每天都看到疯狂的代码,因此对我而言,编译器似乎充满了错误。
如果您坐下来使用任何一种语言的语言规范,并采用该语言的任何编译器实现,并真的要努力确定编译器是否确实实现了该规范,那么只需专注于晦涩难解的案例,您很快就会发现编译器错误非常频繁。让我举一个例子,这是我在五分钟前发现的一个C#编译器错误。
static void N(ref int x){}
...
N(ref 123);
编译器给出了三个错误。
显然,第一个错误消息是正确的,第三个错误消息是错误。错误生成算法试图找出第一个参数无效的原因,它对其进行了查看,发现它是一个常量,并且不返回源代码来检查它是否被标记为“ ref”;相反,它假定没有人会愚蠢到将一个常量标记为ref,并决定该ref必须丢失。
目前尚不清楚正确的第三条错误消息是什么,但是不是。实际上,也不清楚第二个错误消息是否正确。重载解析是否应该失败,还是应该将“ ref 123”视为正确类型的ref参数?现在,我必须考虑一下,并与分类小组进行讨论,以便我们确定正确的行为。
您从未见过此错误,因为您可能永远也不会做任何愚蠢的事情来尝试通过ref传递123。如果这样做了,您可能甚至不会注意到第三条错误消息是无意义的,因为第一条错误消息是正确的并且足以诊断问题。但是我确实尝试做类似的事情,因为我试图破坏编译器。如果尝试过,您也会看到错误。
你在跟我开玩笑吗?编译器也有错误,可以加载。
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 ++这样的其他语言,我什至不想开始。
如何使编译器可靠?首先,他们承担着单元测试的重担。整个星球都在使用它们,因此没有测试人员短缺。
认真地说,我想相信的编译器开发人员是优秀的程序员,尽管他们并非万无一失,但它们的确给我带来了很大的帮助。
我一天遇到两三个。检测一个的唯一真实方法是查看汇编代码。
尽管出于其他原因指出了编译器的高度可靠性,但我认为编译器的可靠性通常是一种自我实现的评估。程序员倾向于将编译器视为标准。当出现问题时,您可以假设是您的错(因为发生错误的时间为99.999%),然后更改代码以解决编译器问题,而不是相反。例如,在高优化设置下崩溃的代码肯定是编译器错误,但是大多数人只是将其设置得较低,然后继续运行,而不会报告该错误。
我们每天使用编译器
...以及它们如何使编译器如此可靠?
他们没有。我们的确是。由于每个人都一直在使用它们,因此可以迅速发现错误。
这是一个数字游戏。由于编译器是习惯于如此普遍地,这是极有可能的任何错误都会有人被触发,但因为有这么大量的用户,这是非常不可能的是,有人会在你明确。
因此,这取决于您的观点:在所有用户中,编译器都是错误的。但是很可能其他人会在您之前编译过类似的代码,因此,如果他们是一个错误,那么它就会击中他们,而不是您,因此从您个人的角度来看,该错误看起来像是永远不会。
当然,最重要的是,您可以在此处添加所有其他答案:编译器已得到充分研究和理解。有一个神话,他们很难编写,这意味着只有非常聪明,非常优秀的程序员才真正尝试编写一个,并且在编写时要格外小心。它们通常易于测试,也易于压力测试或模糊测试。编译器用户本身就是专家程序员,从而产生高质量的错误报告。反之亦然:编译器编写者往往是他们自己的编译器的用户。
我经常遇到编译器错误。
您可以在测试人员较少的较暗角落找到它们。例如,要查找GCC中的错误,您应该尝试:
他们通常非常擅长-O0。实际上,如果怀疑编译器错误,可以将-O0与尝试使用的级别进行比较。优化级别越高,风险就越大。有些甚至是故意的,并在文档中有这样的标记。我遇到了很多(在我的时间里至少有一百),但是最近变得越来越少了。然而,为了追求良好的规格数字(或其他对营销重要的基准),突破极限的诱惑很大。几年前,我们遇到了一个问题,供应商(不愿透露姓名)决定将括号默认为违规-而不是某些明显标记的特殊编译选项。
与说一个杂散的内存引用相比,可能很难诊断出编译器错误,使用不同选项进行重新编译可能只会使数据对象在内存中的相对位置混乱,因此您不知道它是源代码的Heisenbug还是越野车编译器。同样,许多优化对操作顺序进行合理的更改,甚至对代数进行简化,这些更改在浮点舍入和下溢/上溢方面将具有不同的属性。很难将这些影响与REAL bug分开。出于这个原因,硬核浮点计算很困难,因为错误和数值敏感性通常不容易分解。
编译器错误并不罕见。最常见的情况是编译器报告应接受的代码错误,或编译器接受应拒绝的代码。
是的,昨天我在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个参数(我知道这很疯狂)。
您是否曾经在编译器中遇到错误?这是什么,您如何意识到问题出在编译器本身?
对!
最难忘的两个是我遇到的前两个。它们都在1985-7年前后用于680x0 Mac的Lightspeed C编译器中。
第一个是在某些情况下,整数后递增运算符什么都不做-换句话说,在一段特定的代码中,“ i ++”根本不对“ i”做任何事情。我一直拉着头发,直到我看了一个分解。然后我只是以不同的方式进行了增量,并提交了错误报告。
第二个稍微复杂一些,并且确实是一个考虑不周的“功能”。早期的Mac具有用于执行低级磁盘操作的复杂系统。由于某种原因,我从未理解过-可能与创建较小的可执行文件有关-而不是编译器只是在目标代码中就地生成磁盘操作指令,Lightspeed编译器会调用内部函数,该内部函数在运行时生成磁盘操作指令放在堆栈上并跳转到那里。
在68000个CPU上效果很好,但是当在68020 CPU上运行相同的代码时,它通常会做一些奇怪的事情。事实证明,68020的新功能是原始指令256字节指令高速缓存。这是CPU缓存的早期,它没有缓存是“脏的”并且需要重新填充的概念。我猜摩托罗拉的CPU设计人员没有考虑过自动修改代码。因此,如果您在执行顺序中将两个磁盘操作紧密地结合在一起,并且Lightspeed运行时在堆栈上的同一位置构建了实际指令,则CPU会错误地认为它命中了指令高速缓存,并两次运行了第一个磁盘操作。
同样,弄清楚这一点需要花一些时间来使用反汇编程序,并且在低级调试器中要花很多时间。我的解决方法是在每个磁盘操作之前添加对执行256条“ NOP”指令的函数的调用,从而充斥(并清除了)指令缓存。
从那以后的25年间,随着时间的推移,我看到了越来越少的编译器错误。我认为有以下两个原因:
5.5年前在Turbo Pascal中发现了一个明显的错误。编译器的上一个(5.0)和下一个(6.0)版本中都不存在错误。而且它应该很容易测试,因为它根本不是一个危险的情况(只是一个不常用的调用)。
一般而言,商业编译器制造商(而不是业余项目)当然会具有非常广泛的质量保证和测试程序。他们知道他们的编译器是他们的旗舰项目,并且缺陷对他们而言非常糟糕,比他们对制造大多数其他产品的其他公司的看法更糟。软件开发人员是一群无情的人,我们的工具供应商让我们失望了,我们很可能会寻找替代方案,而不是等待供应商提供修复程序,我们很可能将这一事实传达给可能会跟随我们的同行例。在许多其他行业中并非如此,因此,严重的错误给编译器制造商带来的潜在损失要远远大于视频编辑软件制造商。
当使用-O0和-O2进行编译时,如果软件的行为不同,则说明存在编译器错误。
当您的软件的行为与预期的不同时,则可能是该错误存在于您的代码中。
发生编译器错误,但是您往往会发现它们在奇怪的角落。
在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编译器的链接器中有一个漂亮的错误,它将分段错误并无缘无故地死掉。干净的构建通常会修复它(但并非总是感叹)。
我不久前在C#编译器中发现了一个错误,您可以看到Eric Lippert(属于C#设计团队的人)如何弄清楚这里的错误。
除了已经给出的答案之外,我还要添加一些其他内容。编译器设计师通常是非常优秀的程序员。编译器非常重要:大多数编程都是使用编译器完成的,因此必须保证编译器的质量。因此,让编译器的公司将最好的人放在上面(或者至少是非常好的人:最好的人可能不喜欢编译器设计)符合公司的最大利益。微软非常希望他们的C和C ++编译器能够正常工作,否则公司的其他成员将无法完成工作。
另外,如果您要构建一个非常复杂的编译器,则不能仅将其一起破解。编译器背后的逻辑既高度复杂又易于形式化。因此,这些程序通常将以“健壮”且通用的方式构建,从而往往会减少错误。