# 5.5.7将编译后的代码与评估器接口

> 译者： [https://sicp.comp.nus.edu.sg/chapters/121](https://sicp.comp.nus.edu.sg/chapters/121)

我们尚未说明如何将编译后的代码加载到评估器计算机中或如何运行它。 我们将假定已在 [5.4.4](113)**中指定了其他操作 适用于：compiler-ops** 。 我们将实现一个函数`compile-and-go`，该函数编译Scheme表达式，将结果目标代码加载到评估器计算机中，并使该计算机在评估器全局环境中运行代码，打印结果，并进入评估器的驱动程序循环。 我们还将修改评估程序，以便解释表达式可以调用编译函数以及解释函数。 然后，我们可以将已编译的函数放入计算机中，并使用求值器对其进行调用：

```js
compile_and_go(
    parse(
        "function factorial(n) {
             return n === 1 
                 ? 1
                 : n * factorial(n - 1);
         }"));

;;; EC-Eval value:
ok

;;; EC-Eval input:
(factorial 5)
;;; EC-Eval value:
120
```

为了允许评估者处理已编译的函数（例如，评估上面对`factorial`的调用），我们需要在`apply_dispatch`处更改代码（ [5.4.1](110) ），以便它识别已编译的函数（不同于复合函数或原始函数），并将控制权直接转移到已编译的代码的入口点： [[1]](121#footnote-1)

```js
"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")),
    go_to(label("unknown_procedure_type")),

"compiled_apply",
    restore("continue"),
    assign("val", list(op("compiled_procedure_entry"), reg("proc"))),
    go_to(reg("val")),
```

请注意`compiled_apply`处`continue`的还原。 回想一下，评估器的排列方式是使`apply_dispatch`处的延续在堆栈的顶部。 另一方面，编译代码入口点希望继续在`continue`中，因此必须在执行编译代码之前恢复`continue`。

为了使我们能够在启动评估器计算机时运行一些编译后的代码，我们在评估器计算机的开头添加了`branch`指令，如果设置了`flag`寄存器，它将使计算机进入新的入口点。 [[2]](121#footnote-2) `branch(label("external_entry")), // branches if flag is set "read_eval_print_loop", perform(op("initialize-stack")), ![%5Clangle%5Cldots%5Crangle](img/5558496ce03717375325b4b03700d8f4.jpg)`

`External_entry`假定机器从`val`开始，该机器包含将结果放入`val`并以`go_to(reg("continue"))`结尾的指令序列的位置。 从此入口点开始，跳到`val`指定的位置，但首先分配`continue`，以便执行返回`print_result`，后者会在`val`中打印值，然后转到评估程序读取的开始处。 评估打印循环。 [[3]](121#footnote-3)

```js
"external_entry",
    perform(op("initialize_stack")),
    assign("env", op("get_global_environment")),
    assign("continue", label("print_result")),
    go_to(reg("val")),
```

现在，我们可以使用以下函数来编译函数定义，执行已编译的代码并运行read-eval-print循环，以便我们可以尝试使用该函数。 因为我们希望编译后的代码返回到`continue`中的位置，并将其结果返回到`val`中，所以我们使用目标`val`和`return`的链接来编译表达式。 为了将编译器生成的目标代码转换为评估器注册机的可执行指令，我们使用了注册机模拟器中的功能`assemble`（ [5.2.2](103) 节） ）。 然后，我们初始化`val`寄存器以指向指令列表，设置`flag`，以便评估器转到`external_entry`并启动评估器。

```js
function compile_and_go(expression) {
    const instructions = assemble(statements(compile(expression, "val", "return")), "eceval");
    the-global-environment = setup-environment();
    set_register_contents("eceval", "val", instructions);
    set_register_contents("eceval", "flag", true);
    return start("eceval");
}
```

如果我们已经设置了堆栈监视，那么在 [5.4.4](113) 部分的结尾处，我们可以检查编译代码的堆栈使用情况：

```js
compile_and_go(
    parse(
        "function factorial(n) {
             return n === 1 
                 ? 1
                 : n * factorial(n - 1);
         }"));

(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
```

将本示例与使用相同功能的解释版本的`factorial(5)`评估进行比较，如 [5.4.4](113) 一节的末尾所示。 解释版本需要144次推送，最大堆栈深度为28次。这说明了我们的编译策略所产生的优化。

## 解释与汇编

通过本节中的程序，我们现在可以尝试使用解释和编译的替代执行策略。 [[4]](121#footnote-4) 解释器将机器提升到用户程序的级别； 编译器将用户程序降低到机器语言级别。 我们可以将Scheme语言（或任何编程语言）视为机器语言上建立的连贯的抽象族。 解释器对于交互式程序的开发和调试很有用，因为程序执行的步骤是根据这些抽象来组织的，因此对于程序员来说更容易理解。 编译后的代码可以更快地执行，这是因为程序的执行步骤是根据机器语言来组织的，并且编译器可以自由地进行跨高层抽象的优化。 [[5]](121#footnote-5)

用于流行语言（例如C和C ++）的编译器几乎没有将任何错误检查操作放入正在运行的代码中，从而使事情尽可能快地运行。 结果，程序员必须明确提供错误检查。 不幸的是，即使在速度不受限制的关键应用中，人们也常常忽略了这样做。 他们的计划过着快速而危险的生活。 例如，臭名昭著的&lt;quote&gt;蠕虫&lt;/quote&gt;在1988年瘫痪了互联网，利用UNIX ![%5E%7B%5Ctextrm%7BTM%7D%7D](img/fe56a43a66550c6d751ed4dbf720bd67.jpg)操作系统的失败来检查输入缓冲区在手指守护程序中是否溢出。 （请参见 &lt;citation&gt;Spafford 1989&lt;/citation&gt; 。）

解释和编译的替代方法也导致了将语言移植到新计算机的不同策略。 假设我们希望为新机器实现JavaScript。 一种策略是从 [5.4](109)[4.1](73) 部分的解释器之一，以生成在新计算机上运行的解释器。

<exercise>By comparing the stack operations used by compiled code to the stack operations used by the evaluator for the same computation, we can determine the extent to which the compiler optimizes use of the stack, both in speed (reducing the total number of stack operations) and in space (reducing the maximum stack depth). Comparing this optimized stack use to the performance of a special-purpose machine for the same computation gives some indication of the quality of the compiler.

1.  练习 [5.27](113#ex_5.27)[ 5.14 ](105#ex_5.14)[ 5.11 ](99#fig_5.11) 中所示的专用析乘机进行相同的测量。 现在，使用编译后的`factorial`函数执行相同的分析。 取编译版本中的推入次数与解释版本中的推入次数之比，并对最大堆栈深度执行相同的操作。 由于在![n](img/493731e423d5db62086d0b8705dda0c8.jpg)中用于计算![n!](img/9dc6259785da3af5bb9d517f330964af.jpg)的操作数和堆栈深度是线性的，因此![n](img/493731e423d5db62086d0b8705dda0c8.jpg)变大时，这些比率应接近常数。 这些常数是什么？ 同样，找到专用计算机中堆栈使用率与解释版本中使用率的比率。 将专用代码与解释代码的比率与已编译与解释代码的比率进行比较。 您应该发现专用计算机的性能要比编译后的代码好得多，因为手工定制的控制器代码应该比原始的通用编译器要好得多。
2.  您是否可以建议对编译器进行改进，以帮助其生成与手工定制版本性能更接近的代码？

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_121_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>Carry out an analysis like the one in exercise <ref name="ex:measure-factorial-ratio">[5.44](121#ex_5.44)</ref> to determine the effectiveness of compiling the tree-recursive Fibonacci function

```js
function fib(n) {
    return n < 2 
        ? n
        : fib(n - 1) + fib(n - 2);
}
```

compared to the effectiveness of using the special-purpose Fibonacci machine of figure <ref name="fig:fib-machine">[5.12](99#fig_5.12)</ref>. (For measurement of the interpreted performance, see exercise <ref name="ex:rec-fib">[5.28](113#ex_5.28)</ref>.) For Fibonacci, the time resource used is not linear in ![n](img/493731e423d5db62086d0b8705dda0c8.jpg); hence the ratios of stack operations will not approach a limiting value that is independent of ![n](img/493731e423d5db62086d0b8705dda0c8.jpg).<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_121_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>This section described how to modify the explicit-control evaluator so that interpreted code can call compiled functions. Show how to modify the compiler so that compiled functions can call not only primitive functions and compiled functions, but interpreted functions as well. This requires modifying `compile_procedure_call` to handle the case of compound (interpreted) functions. Be sure to handle all the same `target` and `linkage` combinations as in `compile_proc_appl`. To do the actual function application, the code needs to jump to the evaluator's `compound_apply` entry point. This label cannot be directly referenced in object code (since the assembler requires that all labels referenced by the code it is assembling be defined there), so we will add a register called `compapp` to the evaluator machine to hold this entry point, and add an instruction to initialize it: `assign("compapp", label("compound_apply")), branch(label("external_entry")), _; branches if `flag` is set_ "read_eval_print_loop" ![%5Cldots](img/9f57fa7487fb5cc87b777f74773ffb74.jpg)` To test your code, start by defining a function `f` that calls a function `g`. Use `compile_and_go` to compile the definition of `f` and start the evaluator. Now, typing at the evaluator, define `g` and try to call `f`.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_121_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>The `compile_and_go` interface implemented in this section is awkward, since the compiler can be called only once (when the evaluator machine is started). Augment the compiler-interpreter interface by providing a `compile_and_run` primitive that can be called from within the explicit-control evaluator as follows:

```js
compile_and_run(
    parse(
        "function factorial(n) {
             return n === 1 
                 ? 1
                 : n * factorial(n - 1);
         }"));

;;; EC-Eval value:
ok

;;; EC-Eval input:
(factorial 5)
;;; EC-Eval value:
120
```

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_121_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>As an alternative to using the explicit-control evaluator's read-eval-print loop, design a register machine that performs a read-compile-execute-print loop. That is, the machine should run a loop that reads an expression, compiles it, assembles and executes the resulting code, and prints the result. This is easy to run in our simulated setup, since we can arrange to call the functions `compile` and `assemble` as <quote>register-machine operations.</quote><button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_121_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>Use the compiler to compile the metacircular evaluator of section <ref name="sec:mc-eval">[4.1](73)</ref> and run this program using the register-machine simulator. (To compile more than one definition at a time, you can package the definitions in a sequence.) The resulting interpreter will run very slowly because of the multiple levels of interpretation, but getting all the details to work is an instructive exercise.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_121_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>Develop a rudimentary implementation of JavaScript in C (or some other low-level language of your choice) by translating the explicit-control evaluator of section <ref name="sec:eceval">[5.4](109)</ref> into C. In order to run this code you will need to also provide appropriate storage-allocation routines and other run-time support.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_121_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>As a counterpoint to exercise <ref name="ex:interp-in-C">[5.50](121#ex_5.50)</ref>, modify the compiler so that it compiles JavaScript functions into sequences of C instructions. Compile the metacircular evaluator of section <ref name="sec:mc-eval">[4.1](73)</ref> to produce a JavaScript interpreter written in C.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_121_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise>

* * *

[[1]](121#footnote-link-1) Of course, compiled functions as well as interpreted functions are compound (nonprimitive). For compatibility with the terminology used in the explicit-control evaluator, in this section we will use <quote>compound</quote> to mean interpreted (as opposed to compiled).

[[2]](121#footnote-link-2) Now that the evaluator machine starts with a `branch`, we must always initialize the `flag` register before starting the evaluator machine. To start the machine at its ordinary read-eval-print loop, we could use

```js
function start_eceval() {
    the_global_environment = setup_environment();
    set_register_contents(eceval, "flag", false);
    return start(eceval);
}
```

 [[3]](121#footnote-link-3) Since a compiled function is an object that the system may try to print, we also modify the system print operation `user_print` (from section <ref name="sec:running-eval">[4.1.4](77)</ref>) so that it will not attempt to print the components of a compiled function:

```js
function user_print(object) {
    if (compound_procedure(object)) {
        display(list(
            "compound_procedure",
            procedure_parameters(object),
            procedure_body(object),
            "<compiler-env>"));
    } else if (compiled_procedure(object)) {
        display("<compiler-procedure>");
    } else {
        display(object);
    }
}
```

 [[4]](121#footnote-link-4) We can do even better by extending the compiler to allow compiled code to call interpreted functions. See exercise <ref name="ex:compiled-call-interpreted">[5.46](121#ex_5.46)</ref>.

[[5]](121#footnote-link-5) Independent of the strategy of execution, we incur significant overhead if we insist that errors encountered in execution of a user program be detected and signaled, rather than being allowed to kill the system or produce wrong answers. For example, an out-of-bounds array reference can be detected by checking the validity of the reference before performing it. The overhead of checking, however, can be many times the cost of the array reference itself, and a programmer should weigh speed against safety in determining whether such a check is desirable. A good compiler should be able to produce code with such checks, should avoid redundant checks, and should allow programmers to control the extent and type of error checking in the compiled code.

[[6]](121#footnote-link-6) Of course, with either the interpretation or the compilation strategy we must also implement for the new machine storage allocation, input and output, and all the various operations that we took as <quote>primitive</quote> in our discussion of the evaluator and compiler. One strategy for minimizing work here is to write as many of these operations as possible in JavaScript and then compile them for the new machine. Ultimately, everything reduces to a small kernel (such as garbage collection and the mechanism for applying actual machine primitives) that is hand-coded for the new machine.

[[7]](121#footnote-link-7) This strategy leads to amusing tests of correctness of the compiler, such as checking whether the compilation of a program on the new machine, using the compiled compiler, is identical with the compilation of the program on the original JavaScript system. Tracking down the source of differences is fun but often frustrating, because the results are extremely sensitive to minuscule details.

