5.5.1 编译器的结构

5.5.1 编译器的结构
在4.1.7部分中,为了把分析与执行分开,我们已经修改了我们的原来的元循环的解释器。
我们分析每个表达式来生成一个执行程序,它带有一个环境为实际参数并且执行要求的操作。
在我们的编译器中,我们将做必要的相同的分析。代替生成执行程序的是,然而,我们将生成
被我们的寄存器机器运行的指令的序列。

程序compile是在编译器中的最顶层的分发程序。它对应着4.1.1部分中的eval程序,4.1.7部分
中的analyze程序,和在5.4.1部分中的显式控制的解释器的eval-dispatch入口点。编译器像
解释器一样,使用在4.1.2部分中定义的表达式语法的程序。compile执行一个类型分析,根据
是被编译的表达式的语法类型。对于每一个表达式的类型,它都分发一个特定的代码生成器:

(define (compile exp target linkage)
  (cond ((self-evaluating? exp)
         (compile-self-evaluating exp target linkage))
        ((quoted? exp) (compile-quoted exp target linkage))
        ((variable? exp)
         (compile-variable exp target linkage))
        ((assignment? exp)
         (compile-assignment exp target linkage))
        ((definition? exp)
         (compile-definition exp target linkage))
        ((if? exp) (compile-if exp target linkage))
        ((lambda? exp) (compile-lambda exp target linkage))
        ((begin? exp)
         (compile-sequence (begin-actions exp)
                           target
                           linkage))
        ((cond? exp) (compile (cond->if exp) target linkage))
        ((application? exp)
         (compile-application exp target linkage))
        (else
         (error "Unknown expression type -- COMPILE" exp))))

*  目标与连接
compile和代码生成器两个参数再加上要编译的代码。这是一个目标,它指定在编译后的代码中
哪个寄存器是表达式返回的值。也有一个连接的描述符,它描述当它完成它的执行后,从表达式
的编译中得到的结果代码如何继续。连接描述符能要求代码做如下的三件事其中之一:

    .在序列中继续下一条指令(这由连接描述符next指定)
    .从被编译的程序返回(这由连接描述符return指定)
    .跳入一个被命名的入口点(这通过使用目标的标签作为连接描述符来指定)

例如,编译表达式5,(它是自解释的)带有一个val寄存器的目标和一个next的连接描述符,应该生成
指令如下:

(assign val (const 5))

编译相同的表达式带有连接描述符是return应该生成如下的指令

(assign val (const 5))
(goto  (reg continue))

在第一个例子中,执行将继续到序列中的下一条指令。在第二个例子中,
我们将从一个程序调用中返回。在这两个例子,表达式的值将被放在目
标val寄存器中。

* 指令序列与栈的使用
每个代码生成器返回一个指令序列包括了为表达式生成的目标代码。通过组合更简单
的为子表达式的代码生成器的输出完成了复合的表达式的代码生成,正如通过解释子
表达式来完成了复合的表达式的解释。

组合指令序列的最简单的方法是叫做append-instruction-sequence的程序。它以
任意数量的指令的能被顺序的执行的序列为参数;它把它们合并起来并且返回组合
的序列。也就是,如果<seq1>和<seq2>是指令的序列,那么解释

(append-instruction-sequences <seq1> <seq2>)

生成序列

<seq1>
<seq2>

无论何时,寄存器可能需要被保存,编译器的代码生成器使用preserving,它是一个组合
指令序列的更微妙的方法。preserving带有三个实际参数,一个寄存器的集合,两个被
顺序执行的指令序列。它以这样的一种方式来合并序列,就是如果第二个序列的执行需
要寄存器,第一个序列的执行之上保留寄存器集合中的每个寄存器的内容。也就是,如
果第一个序列修改了寄存器,并且第二个序列实际需要寄存器的原来的内容,那么preserving
能在第一个序列与第二个序列合并之前,在第一个序列的前后包装上寄存器的一个保存
与恢复的指令。否则,preserving简单地返回合并的指令序列。因此,例如:

(preserving (list <reg1> <reg2>) <seq1> <seq2>)

生成了如下的四种指令序列之一,依赖于<seq1> 和<seq2> 如何 使用 <reg1>和<reg2>:

<seq1>     |   (save  <reg1>)  |   (save  <reg2>)  |  (save  <reg2>)  |
<seq2>     |     <seq1>           |   <seq1>             |  (save  <reg1>)  |
                 | (restore  <reg1>)|(restore  <reg2>) |    <seq1>           |
                 |     <seq2>           |    <seq2 >            |(restore  <reg1>)|  
                 |                            |                              |(restore  <reg2>) |
                 |                            |                              |    <seq2>            |

通过使用preserving来组合指令序列,编译器避免了不必要的栈操作。用preserving
也分离了是否生成保存与恢复指令的细节,把它们从写每个独立的代码生成器的关注
中分离出来。在事实上,没有保存与恢复的指令是由代码生成器显式地生成的。

在原则上,我们能把一个指令序列简单地表示为一个指令的列表。通过执行普通的
列表的append操作append-instruction-sequence能合并指令序列。然而,
preserving将成为一个复杂的操作,因为它可能不得不分析每个指令序列,来确定
序列如何使用它的寄存器。Preserving像它的复杂一样而没有效率,因为它不得不
分析每个指令序列的实际参数,即使这些序列可能它们本身通过调用preserving而
组装的,而它的每个部分可能已经被分析过了。为了避免这种重复的分析,我们将
把每个指令序列与它的寄存器的使用的某些信息关联起来。当我们组装一个基本的
指令序列时我们将显式地提供这种信息,并且组合指令序列的程序将为了组合的序列
从子序列中关联的信息进行推导寄存器的使用的信息。

一个指令序列将包括了三个方面的信息:

       . 在序列中的指令被执行之前,寄存器的集合必须被初始化(这些寄存器被序列所需要)
       . 在序列的指令修改了寄存器的值,这些值所属的寄存器的集合
       . 在序列中的实际的指令(也叫做语句)
 
我们将表示一个指令序列作为一个有三个部分的列表。指令序列的组装子如下:

(define (make-instruction-sequence needs modifies statements)
  (list needs modifies statements))

例如,两个指令的序列是在当前的环境中,查找变量x的值,把结果赋给val,并且然后返回,
要求寄存器env 和continue已经被初始化,并且修改了寄存器val.因此,这个序列被组装成:

(make-instruction-sequence '(env continue) '(val)
 '((assign val
          (op lookup-variable-value) (const x) (reg env))
   (goto (reg continue))))

我们有时需要组装一个没有语句的空的指令序列:

(define (empty-instruction-sequence)
  (make-instruction-sequence '() '() '()))

组合指令序列的程序被显示在5.5.4部分中。
 
练习5.31
在解释一个程序应用中,显式控制的解释器在操作符的解释的前后
总是保存与恢复env寄存器,在每个操作数的解释前后,除了最后一个操作数,
在每个操作数的解释前后,保存与恢复arg1寄存器, 在操作数的序列的解释前后,
保存与恢复proc寄存器。对于如下的组合中的每一个,说出这些保存与恢复的操作,
哪个是多余的,并且因此能被编译器的preserving机制给消除的:

(f 'x 'y)
((f) 'x 'y)
(f (g 'x) y)
(f (g 'x) 'y)

练习5.32
使用preserving机制,编译器将避免了在操作符中一个符号的情况下,在组合的操作符
解释前后 加上保存与恢复寄存器env.我们也把这样的优化构建进了解释器。的确,5.4
部分中的显式控制的解释器已经准备好了执行一个相似的优化,通过处理没有操作数
的组合作为一个特殊的例子。

a.扩展显式控制的解释器,让它把表达式的组合中的操作符是符号的识别为一个单独的类别,
在解释这样的表达式时利用这个事实。
 
b. 阿丽莎建议通过扩展解释器来识别越来越多的特例,我们能集成所有的编译器的优化,
并且这将削弱了编译的优势,你如何看待这种想法?

猜你喜欢

转载自blog.csdn.net/gggwfn1982/article/details/83540016