灯下 登录
计算机科学 / SICP / subsection 中英对照

5.5.7 Interfacing Compiled Code to the Evaluator

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 节中的某个解释器,得到一个能在新机器上运行的解释器。

Racket #lang sicp
(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
Racket #lang sicp
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))
Racket #lang sicp
;; branches if flag is set:
(branch (label external-entry))
read-eval-print-loop
 (perform (op initialize-stack))
 …
Racket #lang sicp
external-entry
 (perform (op initialize-stack))
 (assign env (op get-global-environment))
 (assign continue (label print-result))
 (goto (reg val))
Racket #lang sicp
(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)))
Racket #lang sicp
(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