5.5 编译

5.5 编译
5.4部分中的显式控制的解释器是一个寄存器机器,它的控制器解释scheme程序。在这部分中,
我们将看看在一个寄存器机器上如何运行一个scheme程序,并且这个机器上的控制器不是一个
scheme的解释器。

显式控制的解释器的机器是通用的,它能执行任何的计算过程,只要这个过程能够被描述成scheme程序。
解释器的控制器精心策划它的数据路径的使用来执行目标的计算。因此,解释器的数据路径也是通用的:
给出一个合适的控制器,执行我们期待的任何计算都是足够的。

商业化的通用目的的计算机是寄存器机器,它由寄存器的集合和组成一个高效的和方便通用的数据路径的集合
的操作。一个通用的目的的机器的控制器是一个寄存器机器语言(像我们已经使用过的那个一样)的解释器。
这种语言被叫做机器的原生的语言,或者是简单的机器语言。用这种机器语言写的程序是使用机器的数据路径
的指令的序列。例如,显式控制的解释器的指令序列能被认为是一个通用的目的的计算机的机器语言的程序
而不是作为一个特定的解释器机器的控制器。

这有两个通常的策略来弥补高级语言和寄存器机器语言之间的鸿沟。显式控制的解释器演示了解释的策略。
一个解释器以一个机器的原生的语言写成,配置机器,来执行以某一种语言(这种语言被叫做源语言)
写成的程序。这种语言可能不同于执行解释的机器的原生的语言。源语言的原生程序被实现为子程序的库。
这个子程序以给定的机器的原生语言写成。被解释的一个程序(调用源程序)被表示为一个数据结构。解释
器遍历这个数据结构,分析源程序。正如它所做的,它模拟源程序的意图的行为,通过从库中调用合适的
原生的子程序。

在这部分中,我们探索另一种可行的策略,即编译。一个给定的源语言的编译器和机器翻译源程序
成为一个等价的程序(叫做目标程序),这个等价的程序是以机器的原生的语言写成的。在这部分中,
我们实现的编译器把用scheme语言写成的程序翻译成使用显式控制的解释器的机器的数据路径能执行
的指令序列。

相对于解释,编译在程序执行的效率方面,能提供一个巨大的提升,正如我们将在下面的编译器的
概述中的阐述。另一个方面,一个解释器对于交互性的程序的开发与调试,提供了一个更强有力的环境,
因为被执行的源程序在运行时期是可用的,可以被检查和修改。此外,因为原生程序的整个库是可表示的,
在调试期间,新的程序能够被组装并且添加到系统中来。

在编译与解释的互补的优势的视角来看,现代的程序开发的环境追求一个混合的策略。Lisp的解释器被
通常设计为解释性的程序和编译性的程序能够调用彼此。这让一个程序员能编译一个正要被调试的程序
的那些部分,因此得到了编译的效率优势,而保留了处于交互式的开发与调试当中的程序的那些部分
的执行的解释模式。在5.5.7部分中,我们已经实现了编译器之后,我们将显示如何对我们的解释器
作接口,让它生成一个集成的解释器与编译器的开发系统。

*  编译器的概述
在结构上与执行的功能上,我们的编译器非常像我们的解释器。对应的,编译器分析表达式
所使用的机制是与解释器所使用的很相似。进而,为了使编译的与解释的代码的接口更容易,
我们将设计编译器生成的代码符合解释器的寄存器的使用的相同的规范:环境变量被保存在
envp寄存器中,实际参数列表将被累加在arg1寄存器中,被应用的程序将放在proc寄存器
中,程序返回的答案放在val寄存器中,一个程序应该返回的位置放在continue寄存器中。
总之,编译器把一个源程序翻译成一个目标程序,这个目标程序务必执行相同的寄存器操作,
如同解释器在解释相同的源程序。

这个描述建议了一个实现一个基本的编译器的策略:我们遍历表达式与解释器的方式相同。
当我们遇到了一个寄存器指令,解释器在解释表达式时执行,我们不执行指令而是把它累
加到一个序列。指令的结果序列是目标代码。注意的是编译比解释有效率上的优势。解释
器每次解释一个表达式,例如(f  84 96)它执行分类表达式的工作(例如发现这是一个
程序应用)并且测试操作数列表的结尾处(例如发现有两个操作数)。在编译器中,表达
式的分析仅有一次,当指令序列在编译期被生成时。编译器生成的目标代码仅包括解释
操作符与两个操作数的指令,汇编的实际参数的列表,应用程序(在proc)到实际参数
(在arg1)

这是与我们在4.1.7部分中的分析性的解释器的实现有相同类型的优化。但是在编译性的
代码中,为了得到效率有更进一步的机会。在解释器运行时,它遵循的过程是对语言中的
任何表达式都必须是可应用的。相反的是,编译的代码的给定的片段意味着执行某些特定
的表达式。这能成为一个很大的区别,例如为了保存寄存器的栈的使用上。当解释器解释
一个表达式时,它必须为任何的 偶然性 作准备。在解释一个子表达式之前,解释器要保存
所有的稍后会用到的寄存器,因为子表达式可能会需要一个随意的解释。一个编译器,
在另一个方面,能探索特定的表达式的结构,它处理生成的代码,避免了不必要的栈的操作。

在某一点上作为一个例子,考虑一下组合体(f   84 96).在解释器解释组合体的操作符之前,
它准备这个解释器通过保存包括了操作数和环境的寄存器,这些值在稍后将被需要用到。解释
器然后解释操作符,得到了结果放入val,恢复保存的寄存器,并且最后把结果从val放入proc.
然而,在我们处理的特殊的表达式中,操作符是符号f,它的解释通过机器操作lookup-variable-value
来完成,它没有改变任何的寄存器。在这部分中我们实现的编译器将利用这个事实,生成代码,
使用如下的指令解释操作符:

(assign proc (op lookup-variable-value) (const f) (reg env))

这个代码不仅避免了不必要的保存与恢复而且直接把查找得到的值赋给了proc,然而,
解释器把结果放在val,再移动到proc.

一个编译器也能优化了对环境的读取。分析了代码,编译器能在许多的情况下知道了
一个特定的变量位于哪个帧,直接读取那个帧,而不是执行Lookup-variable-value的搜索。
在5.5.6部分中,我们将讨论如何实现这样的变量读取。直到这,然而,我们将聚焦于如上
描述的寄存器和栈的优化上。这还有许多的其它方面的优化能被一个编译器执行,例如编码
原生的操作“内联”代替了使用一个通知的应用的机制(见练习5.38);但是在这我们不
强调这些。在这部分中,我们的主要的目标是在一个简化的(但是仍然是有趣的)氛围中,
演示编译的过程。

猜你喜欢

转载自blog.csdn.net/gggwfn1982/article/details/83539998
5.5