# C.7 贯穿各层的问题
<!-- source: p0859 -->

## RISC 指令集与流水线效率

前面讨论过，指令集简单有利于构建流水线。简单指令集还有另一项优点：更容易调度代码，使流水线高效执行。看一个简单例子：把存储器中的两个值相加，再将结果存回存储器。有些复杂指令集只需一条指令，另一些需要两三条；典型 RISC 体系结构则需要四条，即两次加载、一次加法和一次存储。在多数流水线中，若把它们直接排在一起执行，中间就会发生停顿。

RISC 把这些操作分成独立指令，因此编译器可以单独调度它们，使用前面介绍以及第 3 章将讨论的更强技术；硬件也可以动态调度，下面会简要介绍，第 3 章还会深入讨论。调度效率与实现简便这两方面的优势相当显著，因此近年的复杂指令集流水线实现，几乎都先将复杂指令转换成类似 RISC 的简单操作，再对这些操作调度并进行流水线处理。近年的 Intel 处理器全部采用这一方法，ARM 处理器也对某些较复杂的指令这样处理。

## 动态调度流水线

简单流水线取出一条指令后，只要它与流水线中的已有指令之间不存在无法通过旁路或转发掩盖的数据依赖，就会发射它。转发逻辑降低了流水线的有效延迟，使部分依赖关系不会变成冒险。如果冒险不可避免，检测硬件就从使用结果的指令开始暂停流水线；在依赖关系消除之前，不再取出或发射新指令。为了减少这种性能损失，编译器可以通过调度来避免冒险；这称为**编译器调度**，也称**静态调度**。

一些早期处理器采用另一种办法，称为**动态调度**：由硬件重新安排指令执行，以减少停顿。本节通过 CDC 6600 的记分牌（scoreboard）技术作一个较简单的介绍。有些读者会发现，先读这一部分，再学习第 3 章更复杂的 Tomasulo 算法及其推测执行扩展，会容易一些。

<!-- source: p0860 -->

本附录此前介绍的所有技术都按序发射指令。这意味着，如果流水线中某条指令停顿，其后的指令就不能继续。两条指令间一旦出现冒险，流水线即会停顿，即使后面存在与它们独立、原本不必停顿的指令，也无法前进。

前述 RISC-V 流水线在 ID 中同时检查结构冒险和数据冒险；一条指令能够正确执行时，才从 ID 发射。为了让指令只要操作数就绪便开始执行，即便某条前序指令仍然停顿，就必须把发射过程拆成两部分：检查结构冒险，以及等待数据冒险消失。我们仍按序译码、按序发射，但希望指令的数据操作数一旦可用，就立即执行。因此，流水线将采用**乱序执行**，这也意味着可能乱序完成。为实现它，须将 ID 拆成两个阶段：

1. **发射：** 指令译码，检查结构冒险。
2. **读操作数：** 等待数据冒险消除，然后读取操作数。

与原 RISC-V 流水线一样，IF 在发射之前，EX 在读操作数之后。执行可能因操作类型不同而耗费多个周期，因此要区分开始执行和完成执行两个时刻；在这段时间内，指令处于执行中。这样就允许多条指令同时执行。除了这些流水线结构变化，我们还会改变功能单元的数量、操作延迟以及功能单元的流水线化程度，以更充分地研究这些高级技术。

## 用记分牌实现动态调度

动态调度流水线中的指令都按序通过发射阶段，但在第二阶段——读操作数——可能停顿或越过彼此，从而乱序进入执行。记分牌技术允许在资源足够、没有数据依赖约束时乱序执行指令；它因首先实现这种能力的 CDC 6600 记分牌而得名。

在讨论如何把记分牌用于 RISC-V 之前，需要注意：原先整数、浮点流水线中不会出现的 WAR 冒险，在乱序执行时可能发生。例如：

~~~asm
fdiv.d f0,f2,f4
fadd.d f10,f0,f8
fsub.d f8,f8,f14
~~~

<!-- source: p0861 -->

`fadd.d` 与 `fsub.d` 之间存在潜在 WAR 冒险。如果流水线先执行 `fsub.d`，再执行 `fadd.d`，就会产生错误结果。同样，也必须避免 WAW 冒险；例如，把 `fsub.d` 的目的寄存器改为 `f10`，就会出现这种冒险。记分牌通过暂停涉及冒险的后一条指令，来避免这两类问题。

记分牌的目标是尽可能早地执行指令，从而在没有结构冒险时维持每周期一条指令的执行速率。当下一条本该执行的指令停顿时，只要其他指令不依赖任何活动或停顿中的指令，就可以继续发射和执行。记分牌全面负责指令发射与执行，包括全部冒险检测。利用乱序执行，要求多条指令同时处于 EX；这可以靠多个功能单元、流水线化的功能单元，或二者结合来实现。从流水线控制角度看，这两种能力本质上等价，因此下面假定处理器有多个功能单元。

CDC 6600 有 16 个独立功能单元，包括四个浮点单元、五个访存单元，以及七个整数运算单元。对于 RISC-V，记分牌主要用于浮点部件才有意义，因为其他功能单元的延迟很小。假定有两个乘法器、一个加法器、一个除法器，以及一个负责全部访存、分支和整数运算的整数单元。虽然比 CDC 6600 简单，这个例子已足以展示原理，无需引入大量细节或很长的指令序列。RISC-V 与 CDC 6600 都采用加载—存储体系结构，所以两者的技术几乎相同。图 C.49 给出处理器结构。

每条指令都要经过记分牌，在其中建立数据依赖记录；这一过程对应指令发射，替代了 RISC-V 的部分 ID 工作。随后，记分牌决定指令何时可以读操作数并开始执行。若不能立即执行，它就监视硬件的每次状态变化，判断何时条件满足。记分牌也控制指令何时能够把结果写入目的寄存器。因此，所有冒险检测和消解都集中在记分牌中。图 C.49 展示其组织；理解它之前，先看流水线发射与执行部分的步骤。

每条指令执行经过四步。这里重点讨论浮点运算，因此不另设访存步骤。先非形式化地说明，再讨论记分牌如何维护决定步骤间推进时机的信息。这四步替代了标准 RISC-V 流水线的 ID、EX 和 WB。

<!-- source: p0862 -->

![带记分牌的 RISC-V 处理器结构](/figures/app-c/fig-c-49.png)
{#fig-C.49 .wide .expand num="C.49" caption="带记分牌的 RISC-V 处理器基本结构。记分牌通过竖向控制线控制指令执行；所有数据通过寄存器堆与功能单元间的横向总线流动，CDC 6600 称这些总线为 trunks。这里有两个浮点乘法器、一个浮点除法器、一个浮点加法器和一个整数单元。一组总线包含两个输入和一个输出，为一组功能单元服务。第 3 章将进一步讨论记分牌及其扩展。Registers 为寄存器，Data buses 为数据总线，Control/status 为控制/状态，Scoreboard 为记分牌，FP mult、FP divide、FP add 分别为浮点乘法、除法和加法单元，Integer unit 为整数单元。"}

1. **发射。** 如果指令所需的功能单元空闲，并且没有其他活动指令具有相同目的寄存器，记分牌就把指令发射到功能单元，并更新内部数据结构。这替代 RISC-V ID 阶段的部分工作。确保没有其他活动功能单元要写同一目的寄存器，就能保证没有 WAW 冒险。若有结构或 WAW 冒险，就暂停发射；在消除之前，不再发射任何后续指令。发射停顿会使取指与发射之间的缓冲区逐渐填满。如果缓冲区只有一个条目，取指立即停顿；如果它是可容纳多条指令的队列，则队列满后才停顿。

<!-- source: p0863 -->

2. **读操作数。** 记分牌监视源操作数是否可用。若没有更早发射且仍在活动的指令将写该源寄存器，该操作数便可用。全部源操作数就绪后，记分牌通知功能单元从寄存器读取并开始执行。RAW 冒险在此动态消解，指令可以乱序进入执行。此步骤与发射一起，完成简单 RISC-V 流水线中 ID 的功能。
3. **执行。** 功能单元收到操作数后开始执行；结果准备好时，通知记分牌执行已完成。这替代原流水线的 EX；浮点执行可能持续多个周期。
4. **写结果。** 记分牌获知功能单元已完成执行后，检查 WAR 冒险，并在必要时暂停这条准备完成的指令。

WAR 冒险的例子，就是前面同时使用 `f8` 的 `fadd.d` 与 `fsub.d`：

~~~asm
fdiv.d f0,f2,f4
fadd.d f10,f0,f8
fsub.d f8,f8,f14
~~~

`fadd.d` 的源操作数 `f8`，与 `fsub.d` 的目的寄存器相同；但 `fadd.d` 还依赖更早的指令。记分牌会让 `fsub.d` 停在写结果阶段，直到 `fadd.d` 读完操作数。一般而言，只要同时满足以下条件，就不能让一条已执行完毕的指令写结果：

- 有一条按发射顺序更早的指令尚未读取操作数；
- 该指令的某个源寄存器，恰好等于这条准备写回的指令的目的寄存器。

若没有这种 WAR 冒险，或冒险已经消除，记分牌便通知功能单元把结果存入目的寄存器。这替代了简单 RISC-V 流水线的 WB。

乍看之下，记分牌似乎很难区分 RAW 与 WAR 冒险。

这套记分牌只在两个操作数都已出现在寄存器堆中时才读取它们，因此没有利用转发。这一损失并不一定像最初想象的那么大。不同于前面的简单流水线，只要没有 WAR 冒险，指令一执行完就会立即写寄存器堆，不必等一个静态安排、可能还隔着数周期的写回时隙。这缩短了流水线延迟，也降低了转发的收益。

<!-- source: p0864 -->

不过，由于写结果与读操作数不能重叠，仍会多出一周期延迟；若想消除它，还需额外缓冲。

记分牌根据自己的数据结构，与功能单元通信，控制指令从一步前进到下一步。还有一个小问题：连向寄存器堆的源操作数总线和结果总线数量有限，这构成结构冒险。记分牌必须确保，同时允许进入第 2 步和第 4 步的功能单元数不超过可用总线数。这里不再展开，只说明 CDC 6600 的解决办法：把 16 个功能单元分成四组，为每组提供一套称为 data trunks 的总线；每个时钟周期，同组只能有一个单元读取操作数或写回结果。
