William Clinger:
符号程序设计语言解释器的优化
William D. Clinger博士师从Carle Hewitt,在MIT取得了博士学位。他的博士研究课题是用于并行计算的Actor模型的指称语义(Denotational Semantics)。Clinger博士是R^2RS~R^5RS标准的编辑之一, 同时也是MacScheme和Larceny两种语言的实现者。本片文章,即是对其电视课程《符号化程序设计语言解释器的优化》的摘录。
在深入讨论针对符号化语言的编译器优化技术之前,有必要先了解一下什么是“符号化程序语言”。与其用形式化的定义来指明什么是“符号化程序设计语言”,不如用具体实例来说明。通常来说,符号化程序设计语言是指与数值化程序设计语言如Fortran相对的程序设计语言,如Smalltalk、Scheme、ML和Common Lisp。
Scheme是Lisp族系中的一种简单的方言,也是类Lisp语言编译器优化的研究热点。ML有点像Scheme,但是却是静态类型的,并且能够类型推导。ML也向指明了符号化程序语言不必是动态类型的。Common Lisp是商业化符号计算的领军程序设计语言。
符号化程序设计语言有很多共同点:
- 第一级对象(First Class Objects)
- 废料收集(Garbage Collection)
- 存储分配(Storage Allocation)
- 运行时检查(Runtime Checks)
所谓“第一级对象”是指语言中的所有符号对象都可以像其它语言中任何数值那样方便地操作,也就说符号对象可以:
- 可以被存放在变量和数据结构中
- 可以当做参数传递给子过程
- 可以作为返回值从子过程中返回
这是某样东西是语言中的“第一级公民”,那么他就必须具备这些条件。还有一点显而易见,使用第一级对象时,不需要显式地给他命名,即第一级对象可以是匿名的。对第一类对象的存储分配也导致了废料收集的产生。由于符号化程序设计语言的动态类型特性,这也就导致其运行时系统有大量的运行时检查。符号化程序设计语言的另一大特点是过程调用或者像Smalltalk中的消息传递(Message Sends)会相对的多。
现在,让我们来深入讨论一下“第一级对象”。Lisp是一门使用链表(Linked List)作为第一级对象的重要程序设计语言,运行时系统负责分配及管理这些数据。在大多数现代Lisp方言中,过程(Procedure)也是第一级对象。Smalltalk把数据和过程发展成了一种新的叫做对象(Object)的概念。还有一些东西也是第一级对象,在Scheme和Smalltalk中,“继续(Continuation)”就也是第一级对象。
导致运行时检查的因素有下诸几点:
- 链表类型是一种联合类型,一个链表可以是一个含有指向下一个结点指针的结点或者空表。系统为了确定给定的链表是否为空,需要对这类联合类型做运行时检查。这是不可避免的,必须做这些检查。
- 符号化程序设计语言通常做通用型算术运算(Generic Arithmetic),也就是他并不在2^32大小的整数域做计算。符号化程序设计语言做更好的近似,他尝试在实数域(Real Number)甚至复数域(Complex Number)上做计算,在某些系统中,这些数值都是一致的。系统要负责处理这些数值的表示,例如,数值精度溢出了单个寄存器时,他应该能做出相应处理。这都是运行时系统该做的事情。
- 越界检查(下标边界,Subscript Bound)也是非常重要的,在其他语言中,下标越界错误会引发核心转储(Core Dump),而在符号化程序设计语言中,越界则会触发一个Trap,程序员可以在此检查计算状态。
- 符号化程序设计语言通常是动态类型的,运行时系统需要动态的确定对象类型。
为了加速符号化程序设计语言,运行时系统需要在下面四方面提速:
- 高效地存储分配与管理。
- 高效地实现过程调用。在某些运行时系统里面,过程调用是“昂贵的”(费时的)。例如,Smalltalk里面过程调用可能会造成动态的方法查找。
- 高效地标签抽取(用于类型检查)。
- 高效地通用型算术。
这些对于运行时系统都是必须的,因为编译器无法完成这些任务。但是编译器可以:
- 减少废料的产生。
- 消除过程调用。
- 消除标签检查。
- 将算术限制在特定域。
你如果编写过Pascal的编译器,或者任何其他的可以将函数作为参数传递的结构化程序设计语言的编译器,那么你一定了解闭包(Closure)。通常来说,闭包由两部分构成:
- 指向代码的指针;
- 指向一个被称为环境的数据结构的指针;
其中,环境是含有过程体代码所引用的变量,也就是自由变量。在Pascal中,当你将一个函数作为参数传递时,你需要同时传递这两样东西。通常来说,闭包必须在由废料收集器所管理的堆上分配。比如在下面这段Scheme代码中,make-counter
过程返回了一个匿名lambda
函数,由于函数体中引用了参数n
,我们必须记住n
的值,所以不能够在栈上分配,而需要在堆上分配存储空间。