跳转至

流水线、冒险、转发与停顿

五级流水线

经典 RISC 流水线把逻辑阶段对应为 IF、ID、EX、MEM、WB,并在阶段之间加入流水寄存器。第 \(i\) 条指令进入 EX 时,第 \(i+1\) 条可在 ID,第 \(i+2\) 条可在 IF。

周期: 1   2   3   4   5   6   7
I1:   IF  ID  EX  MEM WB
I2:       IF  ID  EX  MEM WB
I3:           IF  ID  EX  MEM WB

流水寄存器保存阶段输出,让各阶段在同一周期并行工作。代价是额外面积、时钟功耗和寄存器延迟。

结构冒险

两条指令同一拍争用同一个资源,就是结构冒险。例如指令和数据共享单端口存储器时,前一条 load 的 MEM 与后一条指令的 IF 同时访问会冲突。

解决办法包括:

  • 复制资源,如分离指令 cache 与数据 cache;
  • 增加端口;
  • 调度一方停顿。

复制资源提高吞吐但增加面积和功耗;停顿便宜却损失性能。

数据冒险

考虑:

add x5, x1, x2
sub x6, x5, x3

sub 在 ID 读 x5 时,add 可能尚未到 WB,直接读取会得到旧值。这是写后读(RAW)相关。

在简单按序五级流水线中,主要需要处理 RAW。写后写(WAW)和读后写(WAR)通常不会出现,因为所有指令按序读写;乱序和多发射实现中三类都要考虑。

转发

add 的结果在 EX 末尾已经算出,无需等到 WB 才可用。转发网络把 EX/MEM 或 MEM/WB 流水寄存器的值直接送回下一条指令的 ALU 输入:

add.EX result ─────────> sub.EX input

转发减少停顿,却增加比较器、多路选择器和关键路径。控制器要比较后续源寄存器号与在途目的寄存器号,并忽略写 x0 等特殊情况。

load-use 仍需停顿

lw  x5, 0(x10)
add x6, x5, x7

load 的数据到 MEM 末尾才返回,而下一条 add 在同一周期 EX 开始就需要它。时间上来不及,即使有转发也通常要停顿一拍:

周期: 1   2   3   4   5   6   7
lw:    IF  ID  EX  MEM WB
add:       IF  ID  --  EX  MEM WB

-- 是气泡。冻结 PC 和 IF/ID,向 ID/EX 注入无副作用操作,就能保持正确性。

控制冒险

分支是否跳转及目标地址可能到 EX 才确定,但前端已经取了后续顺序指令。可选择等待、提前计算分支,或预测方向和目标。预测错误时必须清空错误路径,不能让其写寄存器或内存。

控制冒险会在下一页专门展开,因为现代前端的性能很大程度上取决于预测。

编译器调度

硬件停顿不是唯一办法。若有独立指令,编译器可把它移到 load 与使用者之间:

lw   x5, 0(x10)
addi x11, x11, 1
add  x6, x5, x7

这种静态调度不增加硬件,但受控制流和运行时延迟限制。现代系统常由编译器安排明显相关,硬件再处理动态事件。

流水线平衡

时钟周期由最慢阶段加流水寄存器开销决定:

\[ T_{clk}\ge \max_i(T_i)+T_{reg}. \]

若 MEM 明显慢于其他阶段,继续拆分 ALU 不会提高频率。均衡阶段可提升吞吐,但拆分过细会增加旁路层数、分支惩罚与验证成本。

自测

  1. 为何转发能解决 add 后立即使用,却常不能消除 load-use 停顿?
  2. 分离指令和数据 cache 消除了哪一种结构冒险?
  3. 在乱序处理器中为什么会出现 WAR 和 WAW?寄存器重命名解决了什么?