在这个演讲中,我先简要讲述四十多年前我作为计算机工业界的一名程序员时的早期经历。我将描述我所看到的编程实践方面的不断改进,特别是在我现在工作的最大的软件公司--微软。这会有助于我们预测以后四十年在智能编译器的进一步改善和编程语言质量的进一步提高。我将强调大学和工业界中高级研究的贡献。

在二十世纪,编译器的定义是就是一段程序,它能读一些用抽象(几乎是数学的)记号表述的用户程序并将其翻译成更晦涩难懂的但能在计算机上直接运行的机器码。一个严重的问题是许多编译好的程序含有错误,一些甚至会导致程序崩溃。检测和消除这些错误就需要艰辛的劳动,即费时耗力的调试过程。

目前,调试问题可以通过其他辅助生成测试条件和测试管理的工具来加速程序缺陷的诊断。但是调试过程的成本和延误仍然是整个程序开发过程的主要部分。

在二十一世纪,编译器和其他程序分析工具会变得更加智能化。它们将更多地理解程序员的意图,能够在程序可能不会实现那些意图时给出警告。程序错误和检测它们的成本将成为过去。卸去了常犯错误的负担,程序员就能够把他们的聪明才智完全地运用于二十一世纪计算机应用中最关键和最激动人心的挑战。我将在此论坛的其他演讲中讲述这些应用。

我从1960年开始编程。那时我从一家很小的英国计算机制造商,位于Borehamwood的伦敦爱略特兄弟公司谋得了第一份工作,当程序员。我的任务是用十进制机器码为公司新的晶体管803计算机编写库程序,就像幻灯片上的那样。它的内存时20k字节左右,工作频率是2000赫兹。它的主要吸引力在于价格低于一百万美元(以今天价值计算)。在五年间它卖出了200多台。它的机器码库有大约100个例程,每个典型的例程都短于一百行。我的特别的贡献在于一个内部排序程序和一个磁带机的设备例程。

我的确喜欢利用机器码的最有智巧的指令优化我的程序的内循环。我也喜欢根据程序的标准给代码写文档,再把它作为我们库程序的一部分交给客户。即使是调试程序也是很有趣味的,跟踪错误就像解决数学难题一样。程序员的薪水也丰厚极了!但世道是公平的,我们还是有一个担忧:程序员本人得承担消除他们的错误的费用。我的错误总是一大堆。

但当某种错误导致我的测试程序崩溃的时候,感觉就不太妙了。错误的程序经常覆盖检测错误起因所需的数据。程序崩溃是可能由于跳入数据区,还是一条指令用一个普通的数字覆盖了另一条指令?这两种错误都容易在机器码层次上解决。找出错误的唯一办法是在程序中加入输出指令,跟踪程序的行为直至崩溃为止。但仅仅是输出的数据量—每秒100字节—就足以添乱了。要知道那时幸运的程序员只需一天接触计算机一次。即使是四十年后的今天,我还必须承认程序崩溃的问题还没有完全解决。

当我干了六个月后,一件更重要的任务交给了我,是设计实现一种新的高级编程语言,使得我们的客户不须用机器码编写他们的程序,如我们做的那样。该语言是新近由一个国际专家委员会设计的语言之一,在Peter Naur编辑的仅21页的《关于ALGOL60的算法语言的报告》中有详细描述。虽然简短,它包含了所有开发人员编译该语言和用户用它编写成功的程序所需的信息。人们根本不需要咨询该语言的最初设计者。当程序提交给编译器时,它能够运行。

ALGOL60成功的原因之一是其极其紧凑然而精确的符号,它能够定义语言的语法或句法,并精确确定值得称为有意义的程序的文本的类。这些符号最初是由伟大的语言学家、心理学家和哲学家诺姆*乔姆斯基发明的。这张幻灯片上是英语语法的例子,表明一个句子如何能够包含一个时间状语从句,而该从句本身又是一个短句。

ALGOL语言的语法定义是我们整个ALGOL编译器结构的模式。它使用了现在知道的递归下降的方法。因此,逻辑上几乎对所有提交的程序中语法上的错写或错误都不可能逃过编译器的检查。如果一段成功编译的程序出错,程序员能够完全自信这不是由缺少或多余的括号,或其他使得程序无意义的错写引起的。

与机器码相比,高级语言的另一个巨大优势是引入了程序操作的数据的类型。例如,一个名为i的数据项可以被声明成取整数值,另一个变量b被声明成布尔型,它只能取两个不同的值,即真值—真和假。比较两个整数显然是有意义的,比如3比5小。但比较一个整数和一个布尔真值就毫无意义。一个有判断力的编译器就会拒绝编译这样没有意义的程序,而且程序员也能够依赖这种保证。

不幸的是,有许多程序错误不能用六十年代编译器的这种简单方法检查出来,而且检查必须推迟到运行时进行。在设计我们的爱略特ALGOL编译器输出的机器码时,我的至高无上的原则是从高级语言编译的程序绝不会崩溃。我们的客户不得不因此接受机器性能上的重要折扣,因为运行时所有数组的下标都要被检查是否越界。而且我们发现程序经常通不过检查。

由于有了这些强制检查,用ALGOL编写的程序从不崩溃。调试也变得相对简单,因为每个程序的效果能从程序自身的源代码推断,而不需知道关于编译器和所运行机器的任何信息。不幸的是那个原则被上个世纪的许多编译器和编程语言所抛弃,包括C语言,而大量有价值的软件遗产就是用它写的。

ALGOL编译器于1962年发布,我关于ALGOL编译器的早期经验简单说明了我说的智能编译是什么意思。智能编译器能足够理解程序员的意图并检查程序的执行是否会实现这些意图,至少保证能在意图不能实现时警告程序员。最有用的警告是在程序编译时、执行前,即使是试用版。这种方式定义的编译器能极大地降低程序调试的成本并提高调试的有效性。

现在让我很快地转到目前状况,解释编译器和它们连带的程序生成工具是如何在目前软件业的编程实践中大展身手的。

当今计算机硬件对四十年前的优势是不可想象的,无论是能力、容量还是价格。我们现在每天使用的工作站几乎比我为之写程序的第一台计算机快一百万倍,内存多一万倍,而价格却便宜一千倍。上百万台的工作站连接在延伸到世界各地的网络上。软件的大小也随之增长,但只增长了一千倍,而不是一百万倍。

只有一件事从未改变。软件崩溃仍然是大家公认的问题。虽然每天软件测试的次数也大大增加了,软件测试仍然是成本和延误的主要因素。而且程序仍然有崩溃的危险。

不但测试次数增加,而且测试的效率也大大提高了。一个主要的进展是使用断言使计算机自己检查程序是否通过测试。断言是一个布尔表达式,由程序在执行到时求其真值。例如,这是一个测试下标i是否在数组A允许的范围内。总的来说,在微软的软件产品中,大约百分之一的代码行是这样的断言,但是某些产品中会占到百分之十。

程序员的意图是所有这样的断言在每次遇到时都必须是真的。在测试阶段,如果计算机发现断言的值是假,计算的状态就被自动保存,以便程序员日后分析失败的原因。这差不多能完全消除程序失败从而导致无法分析的崩溃的风险。代码彻底地测试后,断言被自动删除,使得它们交付给客户时不再浪费系统资源。

断言能使错误在运行时在尽可能靠近导致它们的命令的位置被检测出来,但如我前述,错误能够在程序文本中检测出来,甚至在程序被检测之前。这样消除错误通常费用更低。这是程序分析工具,如PCLint,的目标。它识别并报告流行的C语言中任何熟知的编程陷阱和格式缺陷的发生。这种工具的主要问题是它会有大量的误报—即把不是错误的报成错误的。每个误报和真正的错误要浪费人类程序员几乎同样多的思虑。

近来智能编译方面的主要突破的代表是高级程序分析工具PREfix。它由微软研究院开发,它使程序运行的全局分析成为整个系统的行为,包括方法和方法调用。在这种分析的基础上,它能识别没有被适当检验的下标,报告未被初始化的变量,警告使用空指针,等等。然而它仍然不能保证找出每个错误,并发出太多的警告。即使如此,微软有许多程序员要求自己的代码必须完全通过PREfix,然后再把程序写得更清晰,使得PREfix不再给出任何警告,即使是误报。

这种复杂的全局程序分析的问题在于分析长程序需要很长时间。对2000万行程序,它一开始花了三个星期才查了个遍。幸运的是,对它的改进使它现在只需三天三夜。

微软程序员的工作和我1960年开始编程时的经验很不一样。为了给新计算机写库程序,开始每个新程序时我优先写出个大概。计算机专业的大部分学生在他们初期的编程练习中也有同样的习惯。但今天,在象微软这样的软件公司开发产品,绝大部分专业程序员把大部分时间花在扩展、修改和改进已有的庞大程序上,真是非常大。

因此我们需要一个程序分析工具,它能够独立工作并快得多,注意力集中在新近修改的局部小范围代码上。PREfast就是这样的局部分析工具,在微软广泛使用。当然,它是靠牺牲一定准确度来赢得速度的,会检测到较少的错误和较多的误报。研究编程原则、发展编程规范及语言的主要目的之一就是保证局部分析和昂贵得多的全局分析一样好。

仍然还会有许多程序缺陷不能被静态程序分析所诊断,它们还必须通过系统和详尽的测试。有一系列专门工具可供使用。它们基于这样的原则:测试必须比正常的产品运行更严格和苛刻,使得能通过整个一系列测试的程序也很可能在为客户服务时运行正常。于是一种称为应用验证者的工具在程序运行时插入额外的故障,如额外页故障或甚至内存用完故障和其他例外情形。其他工具基于预测的(或意外的)用户使用该程序时的行为的描述,自动生成随机的测试进度表。这些工具正被扩充为模拟或检查当修改后的代码嵌入时余下系统的行为,其目的是能对新近修改的代码进行单独的且更严格的单元测试。

另一个提高程序开发效率的巨大贡献来自编程语言质量的提高。我曾提到C语言作为可信赖的生成正确程序的工具比不上ALGOL60。一个巨大的进展是C++带来的。它能极大地扩充关于内部数据操作有效性的类型检查技术。不幸的是,源代码兼容的要求需要重现原来C语言的所有陷阱和不安全因素。

兼容的负担在设计Java语言时被抛弃了。它回到ALGOL60的传统,检查所有的下标。更进一步的时更新的语言C#,它引入了“控制的代码”的概念。控制的代码是中间代码,即使在装载时也能进行完全的类型检查。我预计这样的改进在二十一世纪还会继续。但我相信这些新语言的好处并不能完全代替程序分析工具,后者能提高软件遗产的质量,即使它是用早期的语言编写的,如C。

到这里,我已经给你们介绍了上一世纪最后四十年里智能编译的状况的两个剪影。它们给我以坚实的基础,使我能够预测这个世纪头四十年里的情形。超过四十年,我不敢妄言。

最容易的预测是计算机硬件将继续它令人惊异的进步改进速度和容量并降低价格。我预计芯片生产的经济性将使未来每台工作站拥有一百或更多个处理器,足以挑战当今最大的多处理器服务器。一个典型的应用程序会在诸如一百个工作站上执行代码,而这些工作站会位于不同的建筑物里。而且支持这些应用的软件基础架构和服务将不再以千万行代码度量,而是上亿行。

现存的绝大部分软件遗产建构时是基于这样的假定:即程序只会在单处理器上执行。当代码在将来集群多处理器上运行时,就会有三个严重的新问题必须避免。第一个就是非确定性,产生于不能控制并行过程的执行顺序。结果,同一程序在相同环境中的两个完全一样的执行却给出不同的结果。于是,程序在测试时虽然完美无缺,但还会在客户端出现异常。第二个问题是死锁,产生于一组程序中所有的并行程序都在等待同组其他程序释放资源。因为它们都在等待,这种释放永远不会发生。事实上,什么都不会再发生。第三个问题是活锁,产生于一组并行过程都把时间完全花在相互间的通讯上而没有精力理会客户的需求。活锁是现在所有的病毒达到“拒绝服务”的攻击目的的方法。它们利用了支持服务软件中隐藏的程序错误。

要克服这些新问题,还有那些现在仍然折磨着串行程序开发的问题,我们将会看到在这个世纪智能编译器设计上的巨大进步。

编译器的智能将基于对程序意图和用户需求的远为深入的理解。经常一个变量的意图可从它的类型的更有表达力的声明中得到预示。类型的概念将扩展到允许一般的量纲分析,使得每一个值都与它的单位紧密联系。例如,在不同的环境中,距离以英尺或米度量,而速度以米/秒或英里/秒度量。智能编译器能保证在用不同度量标准的国家执行相同的代码时不会产生混乱的结果。

表达变量意图的更广泛的方法是通过不变量断言。例如,变量i可以被声明成仅取397以下的整数。这种断言在程序执行的整个过程中都被认为是永远正确的,不论在该变量操作之前或之后。有时一个不变量表达的是两个或多个变量之间关系。例如,可能变量i总是小于变量j.

智能类型检查可以扩充以解决病毒问题,且一般能防止与朋友合作时受到干扰和防止敌人的恶意侵入。当然,这需要你的朋友的合作,对他们合作的网络应用代码进行类型检查。显然但不幸的是,无法强制敌人进行类型检查。

令人惊异的是,这没什么关系。有可能为朋友间的通讯设计类型检查协议,使得它不受没有经过同样检查的敌人的攻击。这令人惊讶的结果在最近的研究中得到了数学上的证明。它是由我在微软剑桥研究院的一个同事和芝加哥dePaul大学的一个学者共同完成的。这个工作是工业界和学术界合作的典型。它保证了我们不用把折磨上一个世纪的问题带到这个世纪。

断言给程序员为程序表达想要的性质提供了更加广泛的技术。考虑一个想要把一个数组A按升序排列的程序。这是一个表达这个意图的断言:对所有的指标i和j,如果i小于j,那么A的第i个元素就小于或等于第j个元素。这是一个排序程序的规范的重要方面:排序程序的用户需要知道它的效果。这样的断言称为一个例程或方法的后条件。

现在我已经描述了为了帮助检测错误,目前像这样的断言是如何在每次测试时得到检查的。未来几年,我们会做得好得多。智能编译器将会通过数学计算或证明一劳永逸地检查断言和程序的正确性,而且根本不再需要通过测试检查断言是否成立。这种编译器称为验证编译器。而且,智能编译器将包含智能优化器,它能够利用断言提供的冗余信息使得生成的机器码更有效。我希望能说服程序员们在他们的代码里多些断言,使得大型软件里的断言的平均密度接近目前的最好实践。总体的效果将是程序更可靠、更有效,额外的收获将是维护更容易。

但即使用验证编译器,大规模计算机编程也绝不是轻而易举的。使用智能编译器的仍将对人类程序员的智慧提出挑战。而且人类程序员将继续出错。他们还会写出不符合他们意图的程序。在这情况下,验证编译器将依赖于拒绝接受他们的程序。

但直接拒绝值不够的。为了改正程序,智能编译器必须提供更多的帮助。它必须生成一系列典型的测试条件以准确暴露错误发生的时间、位置和原因。这些情形应当是足够短而简单,使得人类程序员能够理解。而且需要编译器和程序员双方的智慧。

终极解决方案将是编译器足够聪明,它们能充分理解程序声明的意图,以至于程序员根本不需要编程。它们能自动生成符合规范的高效程序。这样,程序和规范之间的不一致就是不可能的。那是编程原则和工具的研究的长期目标。这些研究正在全世界的学术界和工业界的研究实验室如火如荼地开展着。

但我的预测是即使这种研究能成功的转移到实践,程序员的意图也必须伴以如何有效实现的提示。最终,我们将逐渐从程序伴随着意图转移到意图伴随着程序梗概。智能编译器的成功运用将继续需要聪明和受过良好教育的程序员,甚至比现在还更依赖。

在我这简短的报告中,我描述了我关于智能编译器的一些想法,和它将如何解决现在和未来大规模编程中的许多问题。我回顾了我所经历过的那些想法逐渐演变的部分历史。另外,我也解释了导致不能立即成功运用这些想法的困难。我想以感谢那些提出原始想法的先驱们作为结束。

使用断言的想法和它在大程序中的作用最早由阿兰*图灵1950年在英格兰曼切斯特大学工作时提出。他当然是存储程序计算机和计算机科学本身的奠基人。使用验证编译器证明程序正确性的想法来源于鲍勃*弗洛伊德,他在位于加利福尼亚的斯坦福大学工作。利用程序的规范通过构造生成正确程序的想法来自我的老朋友和同事爱德斯格*迪克斯特拉,他在荷兰的Eindhoven大学工作。

把这些想法的在实践中实现的研究正在世界各地的大学和工业界的研究实验室里不断取得进步。我很高兴我现在在微软剑桥研究院工作,和我在剑桥、雷德蒙以及北京的同事们并肩作战。他们在发展和运用这些计算领域里才华横溢的先驱们的思想方面起着急先锋的作用。

posted on 2013-02-07 07:49  CodingPenguin  阅读(402)  评论(0编辑  收藏  举报