# 5.1.3子例程

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

当设计一台机器执行计算时，我们通常更喜欢安排组件由计算的不同部分共享，而不是重复这些组件。 考虑一台包含两个GCD计算的机器-一个用于查找寄存器`a`和`b`内容的GCD，另一个用于查找寄存器`c`和`d`内容的GCD。 我们可能首先假设我们具有原始的`gcd`操作，然后根据更多原始操作扩展`gcd`的两个实例。 图 [5.7](98#fig_5.7) 仅显示了生成的机器数据路径的GCD部分，而没有显示它们如何连接到机器的其余部分。 该图还显示了机器控制器顺序的相应部分。

<figure>

```js
list(...,      
     "gcd-1",
     test(list(op("="), reg("b"), constant(0))),
     branch(label("after-gcd-1")),
     assign("t", list(op("rem"), reg("a"), reg("b"))),
     assign("a", reg("b")),
     assign("b", reg("t")),
     go_to(label("gcd-1")),
     "after-gcd-1",
     ...
     "gcd-2",
     test(list(op("="), reg("d"), constant(0))),
     branch(label("after-gcd-2")),
     assign("s", list(op("rem"), reg("c"), reg("d"))),
     assign("c", reg("d")),
     assign("d", reg("s")),
     go_to(label("gcd-2")),
     "after-gcd-2",
     ...
```

**[图5.7](98#fig_5.7)** 具有两个GCD计算的机器的数据路径和控制器顺序 。</figure>

该机器有两个剩余的操作箱和两个用于测试相等性的箱。 如果重复的组件很复杂，其余的盒子也很复杂，那么这将不是构建机器的经济方法。 我们可以通过对两个GCD计算使用相同的组件来避免重复数据路径组件，前提是这样做不会影响大型机的其余计算。 如果在控制器到达`gcd-2`时不需要寄存器`a`和`b`中的值（或者这些值可以移至其他寄存器中以进行保管），我们可以更改机器，使其使用寄存器 在计算第二个GCD和第一个GCD时，要使用`a`和`b`而不是寄存器`c`和`d`。 如果这样做，我们将获得如图 [5.8](98#fig_5.8) 所示的控制器序列。

我们已经删除了重复的数据路径组件（以便数据路径再次如图 [5.1](95#fig_5.1)[5.9](98#fig_5.9) 显示了所得控制器序列的相关部分，该序列仅包含`gcd`指令的单个副本。

<figure>

```js
list(...,
     "gcd-1",
     test(list(op("="), reg("b"), constant(0))),
     branch(label("after-gcd-1")),
     assign("t", list(op("rem"), reg("a"), reg("b"))),
     assign("a", reg("b")),
     assign("b", reg("t")),
     go_to(label("gcd-1")),
     "after-gcd-1",
     ...
     "gcd-2",
     test(list(op("="), reg("b"), constant(0))),
     branch(label("after-gcd-2")),
     assign("t", list(op("rem"), reg("a"), reg("b"))),
     assign("a", reg("b")),
     assign("b", reg("t")),
     go_to(label("gcd-2")),
     "after-gcd-2",
     ...
```

**[图5.8](98#fig_5.8)** 使用相同数据路径的机器的控制器序列部分 两个不同GCD计算的组件。</figure>

<figure>

```js
list(...,      
     "gcd",
     test(list(op("="), reg("b"), constant(0))),
     branch(label("gcd-done")),
     assign("t", list(op("rem"), reg("a"), reg("b"))),
     assign("a", reg("b")),
     assign("b", reg("t")),
     go_to(label("gcd")),
     "gcd-done",
     test(list(op("="), reg("continue"), constant(0))),
     branch(label("after-gcd-1")),
     go_to(label("after-gcd-2")),
     ...
     // Before branching to "gcd" from the first place where
     // it is needed, we place 0 in the "continue" register
     assign("continue", constant(0)),
     go_to(label("gcd")),
     "after-gcd-1",
     ...
     // Before the second use of "gcd", we place 1 in the
     // "continue" register
     assign("continue", const(1)),
     go_to(label("gcd")),
     "after-gcd-2",
     ...)
```

**[图5.9](98#fig_5.9)** 使用`continue`寄存器可避免 图中 <ref name="fig:gcd-machine-2">[5.8](98#fig_5.8)</ref> 中的重复控制器序列。</figure>

<figure>

```js
list(...,
     "gcd",
     test(list(op("="), reg("b"), constant(0))),
     branch(label("gcd-done")),
     assign("t", list(op("rem"), reg("a"), reg("b"))),
     assign("a", reg("b")),
     assign("b", reg("t")),
     go_to(label("gcd")),
     "gcd-done",
     go_to(reg("continue")),
     ...
     // Before calling "gcd", we assign to "continue"
     // the label to which "gcd" should return.
     assign("continue", label("after-gcd-1"))),
     go_to(label("gcd")),
     "after-gcd-1",
     ...
     // Here is the second call to "gcd", with a different continuation.
     assign("continue", label("after-gcd-2")),
     go_to(label("gcd")),
     "after-gcd-2",
     ...)
```

**[图5.10](98#fig_5.10)** 为`continue`寄存器分配标签简化了 并概括了如图 <ref name="fig:gcd-machine-2cont">[5.9](98#fig_5.9)</ref> 中所示的策略。</figure>

这是处理小问题的合理方法，但是如果控制器序列中有许多GCD计算实例，则将很尴尬。 为了确定在`gcd`子例程后继续执行的位置，我们需要对使用`gcd`的所有位置的数据路径和控制器中的分支指令进行测试。 实现子例程的一种更强大的方法是使`continue`寄存器保存控制器序列中入口点的标签，该子例程结束后应在该序列中继续执行。 实施此策略需要在数据路径和寄存器机器的控制器之间建立新型连接：必须有一种方法可以在控制器序列中为标签分配标签，以便可以从寄存器中获取该值 并用于在指定入口点继续执行。

为了体现这种能力，我们将扩展寄存器机器语言的`assign`指令，以允许从控制器序列中将一个标签作为值分配给寄存器（作为一种特殊的常量）。 我们还将扩展`go_to`指令，以允许执行在寄存器内容所描述的入口点处继续执行，而不是仅在常量标签所描述的入口点处继续执行。 使用这些新结构，我们可以通过跳转到`continue`寄存器中存储位置的分支来终止`gcd`子例程。 这导致如图 [5.10](98#fig_5.10) 所示的控制器顺序。

一台具有多个子例程的机器可以使用多个连续寄存器（例如`gcd-continue`，`factorial-continue`），或者我们可以让所有子例程共享一个`continue`寄存器。 共享更经济，但是如果我们有一个子例程（`sub1`）调用另一个子例程（`sub2`），则必须小心。 除非`sub1`在为调用`sub2`设置`continue`之前将`continue`的内容保存在其他寄存器中，否则`sub1`完成后将不知道去哪里。 在下一部分中开发的用于处理递归的机制也为嵌套子例程调用的问题提供了更好的解决方案。