# A.2 指令集体系结构的分类
<!-- source: p0670 -->

处理器内部存储的类型是最基本的区别，因此本节集中讨论体系结构这一部分的不同选择。主要方案是栈、累加器或一组寄存器。操作数可以显式指定，也可以隐含指定：栈体系结构的操作数隐含在栈顶；累加器体系结构则隐含地以累加器作为一个操作数。通用寄存器体系结构只使用显式操作数，即寄存器或存储单元。图 A.1 给出这些体系结构的框图，图 A.2 展示代码序列 \(C=A+B\) 在这三类指令集中通常如何表示。根据体系结构类别和具体指令的选择，显式操作数可以直接从存储器访问，也可能需要先装入临时存储位置。

这些图说明，寄存器计算机实际上分为两类。一类允许任意指令在执行时访问存储器，称为**寄存器—存储器体系结构**；另一类只允许加载和存储指令访问存储器，称为**加载—存储体系结构**。第三类把所有操作数放在存储器中，称为**存储器—存储器体系结构**，但如今销售的计算机中已经没有这一类。有些指令集体系结构的寄存器多于单一累加器，但限制了这些专用寄存器的用途；这种体系结构有时称为扩展累加器计算机或专用寄存器计算机。

虽然早期计算机大多采用栈或累加器体系结构，但 1980 年之后设计的几乎所有新体系结构，都采用加载—存储寄存器体系结构。**通用寄存器**（general-purpose register，GPR）计算机兴起，主要有两个原因。第一，寄存器与处理器内其他存储形式一样，速度比存储器快。第二，对于编译器而言，寄存器比其他内部存储形式更容易高效利用。<!-- source: p0672 -->例如，在寄存器计算机上，计算表达式 \((A\times B)+(B\times C)-(A\times D)\) 时，可以按任意次序进行乘法；考虑到操作数的位置或流水线因素，这种自由可能提高效率（见第 3 章）。但在栈计算机中，由于操作数隐藏在栈内，硬件只能按一种次序计算表达式，而且可能需要多次装入同一个操作数。

<!-- source: p0671 -->

![四类指令集体系结构中的操作数位置与 ALU 数据流](/figures/app-a/fig-a-1.png)
{#fig-A.1 .wide .expand num="A.1" caption="四类指令集体系结构中的操作数位置。箭头表示操作数是算术逻辑单元（ALU）运算的输入、结果，还是同时兼作输入和结果。浅色表示输入，深色表示结果。在 (A) 中，栈顶（TOS）寄存器指向最上方的输入操作数，它与下方操作数一起运算；第一个操作数从栈中移除，结果取代第二个操作数，TOS 更新为指向结果，全部操作数均隐含指定。在 (B) 中，累加器既是隐含的输入操作数，也是结果位置。在 (C) 中，一个输入在寄存器中，另一个在存储器中，结果写入寄存器。(D) 的全部操作数都是寄存器；与栈体系结构一样，只有单独的指令才能在这些位置与存储器之间传送数据：(A) 使用 push/pop，(D) 使用 load/store。"}

图内术语：Processor＝处理器；Memory＝存储器；Stack＝栈；Accumulator＝累加器；Register-memory＝寄存器—存储器；Register-register/load-store＝寄存器—寄存器／加载—存储。

| 栈 | 累加器 | 寄存器（寄存器—存储器） | 寄存器（加载—存储） |
| --- | --- | --- | --- |
| `Push A` | `Load A` | `Load R1,A` | `Load R1,A` |
| `Push B` | `Add B` | `Add R3,R1,B` | `Load R2,B` |
| `Add` | `Store C` | `Store R3,C` | `Add R3,R1,R2` |
| `Pop C` | | | `Store R3,C` |
{#fig-A.2 .wide .expand num="A.2" caption="四类指令集实现 C=A+B 的代码序列。栈和累加器体系结构中的 Add 使用隐含操作数，寄存器体系结构使用显式操作数。假定 A、B、C 均位于存储器中，且 A、B 的原值不能被破坏。图 A.1 展示各类体系结构的 Add 操作。"}

<!-- source: p0672 -->

更重要的是，寄存器可以保存变量。将变量分配到寄存器之后，存储器流量减少，程序执行加快（因为寄存器比存储器快），代码密度也提高（因为指定一个寄存器所需的位数比指定一个存储单元少）。

A.8 节将说明，编译器作者希望所有寄存器地位相同，且不保留作特定用途。较早的计算机把一些寄存器分配给专门用途，违背了这一期望，实质上减少了通用寄存器的数量。如果真正通用的寄存器太少，尝试将变量分配到寄存器就得不偿失；编译器反而会把所有尚未指定用途的寄存器都留给表达式求值。

多少寄存器才足够？答案当然取决于编译器的有效性。多数编译器会留出一些寄存器计算表达式，用一些寄存器传递参数，再将其余寄存器分配给变量。现代编译技术能够有效使用更多寄存器，促使较新的体系结构增加了寄存器数量。

GPR 体系结构可按两项主要指令集特征分类。这两项都与典型算术或逻辑指令（ALU 指令）的操作数有关。第一项是 ALU 指令采用两个还是三个操作数。三操作数格式包含一个结果操作数和两个源操作数；两操作数格式则让其中一个操作数同时充当源和结果。第二项是 ALU 指令中有多少个操作数可以是存储器地址。典型 ALU 指令支持的存储器操作数可以从零个到三个。图 A.3 列出这两个属性的组合及相应计算机实例。<!-- source: p0673 -->虽然共有七种可能组合，但其中三种足以对几乎所有现有计算机分类。如前所述，它们是加载—存储（也称寄存器—寄存器）、寄存器—存储器，以及存储器—存储器。

| 存储器地址数 | 允许的最大操作数总数 | 体系结构类型 | 实例 |
| ---: | ---: | --- | --- |
| 0 | 3 | 加载—存储 | ARM、MIPS、PowerPC、SPARC、RISC-V |
| 1 | 2 | 寄存器—存储器 | IBM 360/370、Intel 80x86、Motorola 68000、TI TMS320C54x |
| 2 | 2 | 存储器—存储器 | VAX（也有三操作数格式） |
| 3 | 3 | 存储器—存储器 | VAX（也有两操作数格式） |
{#fig-A.3 .wide .expand num="A.3" caption="典型 ALU 指令的存储器操作数与操作数总数的常见组合，以及计算机实例。每条 ALU 指令均不访问存储器的计算机，称为加载—存储或寄存器—寄存器计算机。按一条典型 ALU 指令包含一个还是多个存储器操作数，分别称为寄存器—存储器或存储器—存储器。"}

图 A.4 给出各方案的优缺点。它们当然不是绝对结论，而是定性描述；实际影响取决于编译器与实现策略。即使 GPR 计算机提供存储器—存储器操作，编译器也完全可以忽略这些操作，将它当作加载—存储计算机使用。体系结构最普遍的影响之一，是改变指令编码以及完成一个任务所需的指令数。附录 C 和第 3 章将讨论这些体系结构方案如何影响具体实现方法。

| 类型 | 优点 | 缺点 |
| --- | --- | --- |
| 寄存器—寄存器 (0, 3) | 指令编码简单、定长；代码生成模型简单；各指令执行所需时钟周期数相近（见附录 C）。 | 相比在指令中访问存储器的体系结构，指令数更多。指令数较多、指令密度较低，使程序更大，可能影响指令缓存。 |
| 寄存器—存储器 (1, 2) | 可以直接访问数据，无需先执行单独的加载指令；指令格式通常易于编码，代码密度较高。 | 操作数地位不对等，因为二元运算会破坏一个源操作数。每条指令都要编码寄存器号和存储器地址，可能限制寄存器数。每条指令的周期数随操作数位置而异。 |
| 存储器—存储器 (2, 2) 或 (3, 3) | 最紧凑；无需占用寄存器保存临时值。 | 指令大小差异很大，尤其是三操作数指令；每条指令完成的工作量也差异很大。存储器访问造成瓶颈。（如今已不使用。） |
{#fig-A.4 .wide .expand num="A.4" caption="三种最常见 GPR 计算机的优缺点。(m, n) 表示 m 个存储器操作数、n 个操作数总数。一般而言，选择较少的计算机使编译器少做决策，从而简化编译器工作（见 A.8 节）；具有大量灵活指令格式的计算机，则能减少程序编码所需位数。寄存器数也会影响指令大小，因为指令中每个寄存器标识需要 log₂（寄存器数）位。因此，对寄存器—寄存器体系结构，将寄存器数加倍需要额外 3 位，约占一条 32 位指令的 10%。"}

## 小结：指令集体系结构的分类

本节及 A.3–A.8 各节末尾，将总结一种新指令集体系结构应当具备的特征，为 A.9 节介绍的 RISC-V 体系结构奠定基础。从本节可以明确看出，新设计应采用通用寄存器。结合图 A.4 与附录 C 对流水线的讨论，可以进一步预期，它应当是一种采用加载—存储方式的通用寄存器体系结构。

讨论了体系结构类别，接下来考察操作数寻址。
