# 5.3.1存储为向量

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

可以将常规的计算机存储器视为一个小孔阵列，每个小孔可以包含一条信息。 每个小洞都有一个唯一的名称，称为_地址_或_位置_。 典型的内存系统提供两种原始操作：一种用于获取存储在指定位置的数据，另一种将新数据分配到指定位置。 可以增加内存地址，以支持顺序访问某些小便池。 一般而言，许多重要的数据操作要求将存储器地址视为数据，可以将其存储在存储器位置并在机器寄存器中进行操作。 列表结构的表示是这种_地址算法_的一种应用。

为了对计算机内存进行建模，我们使用一种称为_矢量_ `(in JavaScript, a vector is essentially just a wrapper around an array)`的新型数据结构。 抽象地，向量是一个复合数据对象，其单个元素可以通过整数索引在独立于索引的时间内访问。 [[1]](107#footnote-1) 为了描述内存操作，我们使用两个原始JavaScript函数来处理向量：

*   `vector_ref(` ![%5Ctextit%7Bvector%7D%2C%20%5Ctextit%7Bn%7D](img/0810a1b864db0eda229491957385929c.jpg) `)`返回向量的第![n](img/493731e423d5db62086d0b8705dda0c8.jpg)个元素。
*   `vector_set(` _矢量_， _n_ ，_值_ `)`将矢量的![n](img/493731e423d5db62086d0b8705dda0c8.jpg)元素设置为指定值。

例如，如果`v`是向量，则`vector_ref(v, 5)`获得向量`v`中的第五个条目，并且`vector_set(v, 5, 7)`将向量`v`的第五个条目的值更改为7。 [[2 ]](107#footnote-2) 对于计算机内存，可以通过使用地址算术将_基地址_与指定了向量在内存中的起始位置的_索引_结合使用来实现此访问 指定向量中特定元素的偏移量。

## 表示JavaScript数据

我们可以使用向量来实现列表结构的内存所需的基本对结构。 让我们假设计算机内存分为两个向量：`the_heads`和`the_tails`。 我们将表示列表结构如下：指向对的指针是两个向量的索引。 该对的开头是`the_heads`中具有指定索引的条目，该对的尾部是`the_tails`中具有指定索引的条目。 我们还需要一种除成对对象（例如数字和字符串）以外的对象的表示形式，以及一种将一种数据与另一种数据区分开的方法。 有许多方法可以完成此操作，但是它们都简化为使用_类型的指针_，即扩展&lt;quote&gt;指针&lt;/quote&gt;的概念以包括有关数据类型的信息。 [[3]](107#footnote-3) 数据类型使系统能够将指向该对的指针（由&lt;quote&gt;对&lt;/quote&gt;数据类型和存储向量的索引组成）与指向其他类型的指针区分开 数据（由其他一些数据类型以及用来表示该类型数据的任何数据组成）。 如果两个数据对象的指针相同，则认为它们是相同的（`===`）。 [[4]](107#footnote-4) 图 [5.14](107#fig_5.14) 说明了使用此方法表示列表`list(list(1, 2), 3, 4)`的过程，还显示了其框和指针图。 我们使用字母前缀来表示数据类型信息。 因此，指向具有索引5的对的指针表示为`p5`，由指针`e0`表示空列表，而指向编号为4的指针表示为`n4`。 在方框和指针图中，我们在每对的左下角指示了向量索引，该向量索引指定该对的`head`和`tail`的存储位置。 `the_heads`和`the_tails`中的空白位置可能包含其他列表结构的部分（此处不涉及）。

<figure>![](img/c09d11b18e3b3de2c2c1c6c4baebc105.jpg)**[图5.14](107#fig_5.14)** 列表`list(list(1, 2), 3, 4)`的框和指针以及内存矢量表示。</figure>

指向数字的指针，例如`n4`，可以由指示数字数据的类型以及数字4的实际表示形式组成。 [[5]](107#footnote-5) 处理太大而无法表示的数字 在为单个指针分配的固定空间量中，我们可以使用不同的 _bignum_ 数据类型，为此指针指定一个列表，其中存储了部分数字。 [[6]](107#footnote-6)

字符串可以表示为类型化指针，该指针指定形成字符串打印表示形式的字符序列。 当在输入中最初遇到字符串时，此序列由JavaScript阅读器构造。 由于我们希望一个字符串的两个实例被`===`识别为与&lt;quote&gt;相同的&lt;/quote&gt;字符串，并且我们希望`===`是一个简单的指针相等性测试，因此我们必须确保如果读者看到了 相同的字符串两次，它将使用相同的指针（指向相同的字符序列）来表示两次出现。 为此，读者需要维护一张表，该表通常称为 _obarray_ ，其中包含它曾经遇到过的所有符号。 当读者遇到一个字符串并准备构造一个符号时，它会检查对象数组，以查看它是否曾经见过相同的字符串。 如果没有，它将使用字符构造一个新符号（指向新字符序列的键入指针），然后将此指针输入到对象数组中。 如果读者以前看过字符串，它将返回存储在对象数组中的符号指针。 通过唯一指针替换字符串的过程称为_插入_符号。

## 实现原始列表操作

给定上述表示方案，我们可以用一个或多个原始向量运算来替换寄存器机的每个&lt;quote&gt;基本&lt;/quote&gt;列表操作。 我们将使用两个寄存器`the_heads`和`the_tails`来标识存储向量，并假定`vector_ref`和`vector_set`可作为原始操作使用。 我们还假设对指针的数字操作（例如增加指针，使用成对指针索引向量或将两个数字相加）仅使用类型化指针的索引部分。

例如，如果我们分别将指令执行为`assign(reg![_%7B1%7D](img/1b36a225f2c8754117f75bc4725c89aa.jpg),list(op("vector_ref"), reg("the_heads"), reg![_%7B2%7D](img/c85b4cb21a8497c8dc9e23794dd63816.jpg))) assign(reg![_%7B1%7D](img/1b36a225f2c8754117f75bc4725c89aa.jpg),list(op("vector_ref"), reg("the_tails"), reg![_%7B2%7D](img/c85b4cb21a8497c8dc9e23794dd63816.jpg)))`，则可以使注册机支持指令`assign(reg![_%7B1%7D](img/1b36a225f2c8754117f75bc4725c89aa.jpg),list(op("head"), reg![_%7B2%7D](img/c85b4cb21a8497c8dc9e23794dd63816.jpg))) assign(reg![_%7B1%7D](img/1b36a225f2c8754117f75bc4725c89aa.jpg),list(op("tail"), reg![_%7B2%7D](img/c85b4cb21a8497c8dc9e23794dd63816.jpg)))`

指令`perform(list(op("set_head"), reg(![%5Ctextit%7Breg%7D_%7B1%7D](img/8174114d7a8b4b9ca15dd9f8be65fe35.jpg)), reg(![%5Ctextit%7Breg%7D_%7B2%7D](img/733459f9b3e2f49b83d609308db1fd8a.jpg)))) perform(list(op("set_tail"), reg(![%5Ctextit%7Breg%7D_%7B1%7D](img/8174114d7a8b4b9ca15dd9f8be65fe35.jpg)), reg(![%5Ctextit%7Breg%7D_%7B2%7D](img/733459f9b3e2f49b83d609308db1fd8a.jpg))))`被实现为`perform(op("vector_set"), list(reg("the_heads"), reg(![%5Ctextit%7Breg%7D_%7B1%7D](img/8174114d7a8b4b9ca15dd9f8be65fe35.jpg)), reg(![%5Ctextit%7Breg%7D_%7B2%7D](img/733459f9b3e2f49b83d609308db1fd8a.jpg)))) perform(op("vector_set"), list((reg("the_tails"), reg(![%5Ctextit%7Breg%7D_%7B1%7D](img/8174114d7a8b4b9ca15dd9f8be65fe35.jpg)), reg(![%5Ctextit%7Breg%7D_%7B2%7D](img/733459f9b3e2f49b83d609308db1fd8a.jpg))))`

通过分配未使用的索引并将自变量存储到该索引矢量位置处的`the_heads`和`the_tails`中的`pair`来执行操作`pair`。 我们假设有一个特殊的寄存器`free`，该寄存器始终包含一个包含下一个可用索引的成对指针，并且我们可以递增该指针的索引部分以找到下一个空闲位置。 [[7]](107#footnote-7) 例如，指令`assign(![%5Ctextit%7Breg%7D_%7B1%7D](img/8174114d7a8b4b9ca15dd9f8be65fe35.jpg), list(op("pair"), reg(![%5Ctextit%7Breg%7D_%7B2%7D](img/733459f9b3e2f49b83d609308db1fd8a.jpg)), reg(![%5Ctextit%7Breg%7D_%7B3%7D](img/6885a1c74e5eb515d87f3af277e2a0ac.jpg)))`被实现为以下矢量操作序列： [[8]](107#footnote-8) `perform(op("vector_set"), list(reg("the_heads"), reg("free"), reg(![%5Ctextit%7Breg%7D_%7B2%7D](img/733459f9b3e2f49b83d609308db1fd8a.jpg)))), perform(op("vector_set"), list(reg("the_tails"), reg("free"), reg(![%5Ctextit%7Breg%7D_%7B3%7D](img/6885a1c74e5eb515d87f3af277e2a0ac.jpg)))), assign(![%5Ctextit%7Breg%7D_%7B1%7D](img/8174114d7a8b4b9ca15dd9f8be65fe35.jpg), reg("free")), assign("free", list(op("+"), reg("free"), cons(1)))`

`===`操作`list(op("==="), reg(![%5Ctextit%7Breg%7D_%7B1%7D](img/8174114d7a8b4b9ca15dd9f8be65fe35.jpg)), reg(![%5Ctextit%7Breg%7D_%7B2%7D](img/733459f9b3e2f49b83d609308db1fd8a.jpg)))`仅测试寄存器中所有字段的相等性，并且诸如`is_pair`，`is_null`，`is_string`和`is_number`之类的谓词仅需要检查类型字段。

## 实现栈

尽管我们的注册机使用堆栈，但是这里不需要做任何特殊的事情，因为可以根据列表对堆栈进行建模。 堆栈可以是保存值的列表，由特殊寄存器`the_stack`指向。 因此，`save(` ![%5Ctextit%7Breg%7D](img/4e9a5bc90b6d4f3c61dc670773a9f270.jpg) `)`可以实现为`assign("the_stack", list(op("pair"), reg(![%5Ctextit%7Breg%7D](img/4e9a5bc90b6d4f3c61dc670773a9f270.jpg)), reg("the-stack")))`。类似地，`restore(` ![%5Ctextit%7Breg%7D](img/4e9a5bc90b6d4f3c61dc670773a9f270.jpg) `)`可以实现为`assign(![%5Ctextit%7Breg%7D](img/4e9a5bc90b6d4f3c61dc670773a9f270.jpg), list(op("head"), reg("the_stack"))) assign("the_stack", list(op("tail"), reg("the_stack")))`，`perform(op("initialize_stack"))`可以实现为`assign("the_stack", cons(null))`。

这些操作可以根据上面给出的向量操作进一步扩展。 但是，在常规计算机体系结构中，将堆栈分配为单独的向量通常是有利的。 然后可以通过增加或减少该向量的索引来完成堆栈的压入和弹出操作。

<exercise>Draw the box-and-pointer representation and the memory-vector representation (as in Figure <ref name="fig:box-and-pointer-memory">[5.14](107#fig_5.14)</ref>) of the list structure produced by

```js
const x = pair(1, 2);
const y = list(x, x);
```

with the `free` pointer initially `p1`. What is the final value of `free`![%5C%2C](img/4b19862177c4e90d5251360144517e34.jpg)? What pointers represent the values of `x` and `y`![%5C%2C](img/4b19862177c4e90d5251360144517e34.jpg)?<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_107_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>Implement register machines for the following functions. Assume that the list-structure memory operations are available as machine primitives.

1.  递归`count_leaves`：

    ```js
    function count_leaves(tree) {
        return is_null(tree)
            ? 0
            : ! is_pair(tree)
    	  ? 1
              : count_leaves(head(tree)) + 
                count_leaves(tail(tree));
    }
    ```

2.  具有显式计数器的递归`count_leaves`：

    ```js
    function count_leaves(tree) {
        function count_iter(tree, n) {
            return is_null(tree)
                ? n
                : ! is_pair(tree) 
                  ? n + 1
    	      : count_iter(tail(tree),
                               count_iter(head(tree), n)));
        }
        count_iter(tree, 0);
    }
    ```

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_107_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>Exercise <ref name="ex:append">[3.12](58#ex_3.12)</ref> of section <ref name="sec:mutable-list-structure">[3.3.1](58)</ref> presented an `append` function that appends two lists to form a new list and an `append_mutator` function that splices two lists together. Design a register machine to implement each of these functions. Assume that the list-structure memory operations are available as primitive operations.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_107_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]](107#footnote-link-1) We could represent memory as lists of items. However, the access time would then not be independent of the index, since accessing the ![n](img/493731e423d5db62086d0b8705dda0c8.jpg)th element of a list requires ![n-1](img/23175b78d29762e4a6d26ee3e988f9e3.jpg) `tail` operations.

[[2]](107#footnote-link-2) For completeness, we should specify a `make_vector` operation that constructs vectors. However, in the present application we will use vectors only to model fixed divisions of the computer memory.

[[3]](107#footnote-link-3) This is precisely the same <quote>tagged data</quote> idea we introduced in chapter 2 for dealing with generic operations. Here, however, the data types are included at the primitive machine level rather than constructed through the use of lists.

[[4]](107#footnote-link-4) Type information may be encoded in a variety of ways, depending on the details of the machine on which the JavaScript system is to be implemented. The execution efficiency of JavaScript programs will be strongly dependent on how cleverly this choice is made, but it is difficult to formulate general design rules for good choices. The most straightforward way to implement typed pointers is to allocate a fixed set of bits in each pointer to be a _type field_ that encodes the data type. Important questions to be addressed in designing such a representation include the following: How many type bits are required? How large must the vector indices be? How efficiently can the primitive machine instructions be used to manipulate the type fields of pointers? Machines that include special hardware for the efficient handling of type fields are said to have _tagged architectures_.

[[5]](107#footnote-link-5) This decision on the representation of numbers determines whether `===`, which tests equality of pointers, can be used to test for equality of numbers. If the pointer contains the number itself, then equal numbers will have the same pointer. But if the pointer contains the index of a location where the number is stored, equal numbers will be guaranteed to have equal pointers only if we are careful never to store the same number in more than one location.

[[6]](107#footnote-link-6) This is just like writing a number as a sequence of digits, except that each <quote>digit</quote> is a number between 0 and the largest number that can be stored in a single pointer.

[[7]](107#footnote-link-7) There are other ways of finding free storage. For example, we could link together all the unused pairs into a _free list_. Our free locations are consecutive (and hence can be accessed by incrementing a pointer) because we are using a compacting garbage collector, as we will see in section <ref name="sec:gc">[5.3.2](108)</ref>.

[[8]](107#footnote-link-8) This is essentially the implementation of `pair` in terms of `set_head` and `set_tail`, as described in section <ref name="sec:mutable-list-structure">[3.3.1](58)</ref>. The operation `get_new_pair` used in that implementation is realized here by the `free` pointer.

