We have not yet explained how to load compiled code into the evaluator machine
or how to run it. We will assume that the explicit-control-evaluator machine
has been defined as in 5.4.4, with the additional operations
specified in Footnote 323. We will implement a procedure
compile-and-go that compiles a Scheme expression, loads the resulting
object code into the evaluator machine, and causes the machine to run the code
in the evaluator global environment, print the result, and enter the
evaluator’s driver loop. We will also modify the evaluator so that interpreted
expressions can call compiled procedures as well as interpreted ones. We can
then put a compiled procedure into the machine and use the evaluator to call
it:
我们尚未解释如何将编译后的代码载入求值器机器以及如何运行它。我们假定显式控制求值器机器已按 5.4.4 节中的方式定义,并附加了脚注 323 中所述的额外操作。我们将实现一个名为 `compile-and-go` 的过程,它编译一个 Scheme 表达式,将所得目标代码载入求值器机器,并让机器在求值器的全局环境中运行该代码,打印结果,然后进入求值器的驱动循环。我们还将修改求值器,使得被解释的表达式既能调用已编译过程,也能调用被解释的过程。这样就可以将一个已编译过程置入机器,并用求值器来调用它:
To allow the evaluator to handle compiled procedures (for example, to evaluate
the call to factorial above), we need to change the code at
apply-dispatch (5.4.1) so that it recognizes compiled
procedures (as distinct from compound or primitive procedures) and transfers
control directly to the entry point of the compiled code:
为了使求值器能处理已编译过程(例如对上面 factorial 的调用求值),我们需要修改 `apply-dispatch`(5.4.1 节)处的代码,使其能识别已编译过程(有别于复合过程或基本过程),并将控制直接转移到已编译代码的入口点:
Note the restore of continue at compiled-apply. Recall that the
evaluator was arranged so that at apply-dispatch, the continuation would
be at the top of the stack. The compiled code entry point, on the other hand,
expects the continuation to be in continue, so continue must be
restored before the compiled code is executed.
注意 `compiled-apply` 处对 continue 的恢复。回顾一下,求值器被安排为:在 `apply-dispatch` 处,后续步骤(continuation)位于栈顶。而已编译代码的入口点则期望后续步骤位于 continue 中,因此必须在执行已编译代码之前先恢复 continue。
To enable us to run some compiled code when we start the evaluator machine, we
add a branch instruction at the beginning of the evaluator machine,
which causes the machine to go to a new entry point if the flag register
is set.
为了能在启动求值器机器时运行某些已编译代码,我们在求值器机器的开头添加一条分支指令,当 flag 寄存器被置位时,机器将跳转到一个新的入口点。
External-entry assumes that the machine is started with val
containing the location of an instruction sequence that puts a result into
val and ends with (goto (reg continue)). Starting at this entry
point jumps to the location designated by val, but first assigns
continue so that execution will return to print-result, which
prints the value in val and then goes to the beginning of the
evaluator’s read-eval-print loop.
`external-entry` 假定机器启动时 val 中存放着一个指令序列的起始位置,该序列将结果置于 val 中并以 `(goto (reg continue))` 结束。从该入口点开始执行会跳转到 val 所指示的位置,但首先会为 continue 赋值,使得执行最终返回到 `print-result`——它打印 val 中的值,然后跳转到求值器读入-求值-打印循环的起始处。
Now we can use the following procedure to compile a procedure definition,
execute the compiled code, and run the read-eval-print loop so we can try the
procedure. Because we want the compiled code to return to the location in
continue with its result in val, we compile the expression with a
target of val and a linkage of return. In order to transform the
object code produced by the compiler into executable instructions for the
evaluator register machine, we use the procedure assemble from the
register-machine simulator (5.2.2). We then initialize the
val register to point to the list of instructions, set the flag
so that the evaluator will go to external-entry, and start the
evaluator.
现在我们可以使用下面的过程来编译一个过程定义,执行编译后的代码,并运行读入-求值-打印循环以便试用该过程。因为我们希望已编译代码以结果在 val 中的方式返回到 continue 所指示的位置,所以我们以目标 val、连接 return 来编译该表达式。为了将编译器产生的目标代码转化为求值器寄存器机器的可执行指令,我们使用寄存器机器模拟器(5.2.2 节)中的 `assemble` 过程。然后我们将 val 寄存器初始化为指向指令表的起始处,置位 flag 以使求值器转到 `external-entry`,并启动求值器。
If we have set up stack monitoring, as at the end of 5.4.4, we
can examine the stack usage of compiled code:
如果我们已按 5.4.4 节末尾的方式设置了栈监控,就可以查看已编译代码的栈使用情况:
Compare this example with the evaluation of (factorial 5) using the
interpreted version of the same procedure, shown at the end of
5.4.4. The interpreted version required 144 pushes and a maximum stack
depth of 28. This illustrates the optimization that results from our
compilation strategy.
Subheading: Interpretation and compilation
将本例与 5.4.4 节末尾所示的用同一过程的解释版本对 `(factorial 5)` 求值时的情况相比较。解释版本需要 144 次压栈操作,最大栈深度为 28。这说明了我们的编译策略所带来的优化效果。
小节标题:解释与编译
With the programs in this section, we can now experiment with the alternative
execution strategies of interpretation and compilation. An interpreter raises the machine to
the level of the user program; a compiler lowers the user program to the level
of the machine language. We can regard the Scheme language (or any programming
language) as a coherent family of abstractions erected on the machine language.
Interpreters are good for interactive program development and debugging because
the steps of program execution are organized in terms of these abstractions,
and are therefore more intelligible to the programmer. Compiled code can
execute faster, because the steps of program execution are organized in terms
of the machine language, and the compiler is free to make optimizations that
cut across the higher-level abstractions.
有了本节的这些程序,我们现在可以实验解释和编译这两种不同的执行策略。解释器将机器提升到用户程序的层次;编译器则将用户程序降低到机器语言的层次。我们可以把 Scheme 语言(或任何编程语言)视为建立在机器语言之上的一组连贯的抽象体系。解释器有利于交互式程序开发和调试,因为程序执行的各个步骤以这些抽象为单位来组织,因而对程序员更加易于理解。编译后的代码执行速度可以更快,因为程序执行的步骤以机器语言为单位来组织,编译器可以自由地进行跨越更高层次抽象的优化。
The alternatives of interpretation and compilation also lead to different
strategies for porting languages to new computers. Suppose that we wish to
implement Lisp for a new machine. One strategy is to begin with the
explicit-control evaluator of 5.4 and translate its instructions
to instructions for the new machine. A different strategy is to begin with the
compiler and change the code generators so that they generate code for the new
machine. The second strategy allows us to run any Lisp program on the new
machine by first compiling it with the compiler running on our original Lisp
system, and linking it with a compiled version of the run-time
library. Better yet, we can
compile the compiler itself, and run this on the new machine to compile other
Lisp programs. Or we can compile one of the interpreters of
4.1 to produce an interpreter that runs on the new machine.
解释和编译这两种策略还引出了将语言移植到新计算机上的不同方案。假设我们希望在一台新机器上实现 Lisp。一种策略是从 5.4 节的显式控制求值器出发,将其指令翻译成新机器的指令。另一种策略是从编译器出发,修改代码生成器,使其生成适用于新机器的代码。第二种策略使我们能够先用运行于原有 Lisp 系统上的编译器对任意 Lisp 程序进行编译,再将其与运行时库的编译版本链接,从而在新机器上运行该程序。更进一步,我们可以编译编译器本身,并在新机器上运行以编译其他 Lisp 程序;或者我们可以编译 4.1 节中的某个解释器,得到一个能在新机器上运行的解释器。
(compile-and-go
'(define (factorial n)
(if (= n 1)
1
(* (factorial (- n 1)) n))))
;;; EC-Eval value:
ok
;;; EC-Eval input:
(factorial 5)
;;; EC-Eval value:
120 apply-dispatch
(test (op primitive-procedure?) (reg proc))
(branch (label primitive-apply))
(test (op compound-procedure?) (reg proc))
(branch (label compound-apply))
(test (op compiled-procedure?) (reg proc))
(branch (label compiled-apply))
(goto (label unknown-procedure-type))
compiled-apply
(restore continue)
(assign val
(op compiled-procedure-entry)
(reg proc))
(goto (reg val)) ;; branches if flag is set:
(branch (label external-entry))
read-eval-print-loop
(perform (op initialize-stack))
… external-entry
(perform (op initialize-stack))
(assign env (op get-global-environment))
(assign continue (label print-result))
(goto (reg val)) (define (compile-and-go expression)
(let ((instructions
(assemble
(statements
(compile
expression 'val 'return))
eceval)))
(set! the-global-environment
(setup-environment))
(set-register-contents!
eceval 'val instructions)
(set-register-contents!
eceval 'flag true)
(start eceval))) (compile-and-go
'(define (factorial n)
(if (= n 1)
1
(* (factorial (- n 1)) n))))
(total-pushes = 0, maximum-depth = 0)
;;; EC-Eval value:
ok
;;; EC-Eval input:
(factorial 5)
(total-pushes = 31, maximum-depth = 14)
;;; EC-Eval value:
120