# 5.5编译

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

Note: this section is a work in progress!

[5.4](109)JavaScript&lt;/split&gt; 程序。 在本节中，我们将看到如何在控制器不是 &lt;split&gt;JavaScript&lt;/split&gt; 解释器的寄存器机上运行 &lt;split&gt;JavaScript&lt;/split&gt; 程序。

显式控制评估器机器是通用的，它可以执行 &lt;split&gt;JavaScript&lt;/split&gt; 中描述的任何计算过程。 评估者的控制器协调使用其数据路径来执行所需的计算。 因此，评估者的数据路径是通用的：只要有合适的控制器，它们就足以执行我们想要的任何计算。 [[1]](114#footnote-1)

商业通用计算机是围绕一组寄存器和操作进行组织的寄存器计算机，这些寄存器和操作构成了高效且便捷的通用数据路径集。 通用机器的控制器是注册机语言的解释器，就像我们一直在使用的那样。 该语言称为机器的_本机语言_，或简称为_机器语言_。 用机器语言编写的程序是使用机器数据路径的指令序列。 例如，显式控制评估器的指令序列可以被视为通用计算机的机器语言程序，而不是专用解释器的控制器。

有两种常见的策略可以弥合高级语言和寄存器机器语言之间的差距。 显性控制评估者说明了解释策略。 用机器的本地语言编写的解释器将机器配置为执行以可能与执行评估的机器的本地语言不同的语言（称为_源语言_）编写的程序。 源语言的原始功能被实现为以给定机器的本地语言编写的子例程库。 将要解释的程序（称为_源程序_）表示为数据结构。 解释器遍历此数据结构，分析源程序。 这样，它通过从库中调用适当的原始子例程来模拟源程序的预期行为。

在本节中，我们探讨_编译_的替代策略。 给定源语言和机器的编译器将源程序转换为以机器的本地语言编写的等效程序（称为_目标程序_）。 我们在本节中实现的编译器将使用 &lt;split&gt;JavaScript&lt;/split&gt; 编写的程序转换为要使用显式控制评估器机器的数据路径执行的指令序列。 [[2]](114#footnote-2)

与解释相比，编译可以大大提高程序执行的效率，正如我们将在下面的编译器概述中介绍的那样。 另一方面，解释器为交互式程序的开发和调试提供了更强大的环境，因为正在执行的源程序可以在运行时进行检查和修改。 另外，由于存在完整的原语库，因此可以在调试过程中构造新程序并将其添加到系统中。

鉴于编译和解释的互补优势，现代程序开发环境追求混合策略。 JavaScript解释器通常进行组织，以便解释后的函数和编译后的函数可以相互调用。 这使程序员能够编译被假定为要调试的程序部分，从而获得编译的效率优势，同时为交互开发和调试过程中的那些程序部分保留解释性的执行模式。 在 [5.5.7](121) 部分中，实现了编译器后，我们将展示如何将其与解释器接口以生成集成的解释器-编译器开发系统。

## 编译器概述

我们的编译器在结构和执行功能上都非常类似于我们的解释器。 因此，编译器用于分析表达式的机制将类似于解释器所使用的机制。 此外，为了使编译和解释的代码易于接口，我们将设计编译器以生成遵循与解释器相同的寄存器用法惯例的代码：环境将保存在`env`寄存器中，参数列表将被累积 在`argl`中，要应用的函数将在`proc`中，函数将在`val`中返回其答案，而函数应返回的位置将保留在`continue`中。 通常，编译器将源程序转换为目标程序，该目标程序执行与解释程序在评估同一源程序时基本相同的寄存器操作。

此描述提出了一种用于实现基本编译器的策略：我们以与解释器相同的方式遍历表达式。 当我们遇到解释器将在评估表达式时执行的寄存器指令时，我们不执行该指令，而是将其累加成一个序列。 结果指令序列将是目标代码。 观察编译相对于解释的效率优势。 解释器每次评估一个表达式（例如`f(84, 96)`）时，都会执行对表达式进行分类（发现这是一个函数应用程序）并测试操作数列表的末尾（发现有两个操作数）的工作。 对于编译器，当在编译时生成指令序列时，仅对表达式进行一次分析。 编译器生成的目标代码仅包含评估运算符和两个操作数，汇编参数列表并将函数（在`proc`中）应用于参数（在`argl`中）的指令。

这与我们在 [4.1.7](80) 部分的分析评估器中实现的优化方法相同。 但是，还有更多的机会来提高编译代码的效率。 在解释器运行时，它遵循的过程必须适用于该语言中的任何表达式。 相反，编译代码的给定段旨在执行某些特定的表达式。 这可能会有很大的不同，例如在使用堆栈保存寄存器方面。 解释器评估表达式时，必须为任何意外情况做好准备。 在评估子表达式之前，解释器会保存以后需要的所有寄存器，因为子表达式可能需要任意评估。 另一方面，编译器可以利用其正在处理的特定表达式的结构来生成避免不必要的堆栈操作的代码。

作为一个适当的例子，考虑组合`f(84, 96)`。 在解释器评估组合运算符之前，它会通过保存包含操作数和环境的寄存器为该评估做准备，这些寄存器的值稍后将需要。 然后，解释器评估运算符以在`val`中获得结果，恢复已保存的寄存器，最后将结果从`val`移至`proc`。 但是，在我们要处理的特定表达式中，运算符是符号`f`，其评估是通过不更改任何寄存器的机器操作`lookup_variable_value`完成的。 我们在本节中实现的编译器将利用这一事实，并使用指令生成评估运算符的代码

```js
assign("proc", op("lookup_variable_value"), constant("f"), reg("env"));
```

This code not only avoids the unnecessary saves and restores but also assigns the value of the lookup directly to `proc`, whereas the interpreter would obtain the result in `val` and then move this to `proc`.

编译器还可以优化对环境的访问。 分析了代码之后，编译器在许多情况下可以知道特定变量将位于哪个帧中并直接访问该帧，而不用执行`lookup_variable_value`搜索。 我们将在 [5.5.6](120)，而不是使用常规的`apply`机制（请参阅练习 [5.37](119#ex_5.37)

* * *

[[1]](114#footnote-link-1) This is a theoretical statement. We are not claiming that the evaluator's data paths are a particularly convenient or efficient set of data paths for a general-purpose computer. For example, they are not very good for implementing high-performance floating-point calculations or calculations that intensively manipulate bit vectors.

[[2]](114#footnote-link-2) Actually, the machine that runs compiled code can be simpler than the interpreter machine, because we won't use the `exp` and `unev` registers. The interpreter used these to hold pieces of unevaluated expressions. With the compiler, however, these expressions get built into the compiled code that the register machine will run. For the same reason, we don't need the machine operations that deal with expression syntax. But compiled code will use a few additional machine operations (to represent compiled function objects) that didn't appear in the explicit-control evaluator machine.

