PAYATHON 2026

RISC-V 五级流水线何时使用 forwarding,何时需要 stall?

数据阿明

结论:能及时前递就前递,结果尚未产生就停顿

在常见的五级流水线实现中,forwarding 和 stall 通常配合使用:

  • 所需结果已经产生,且有旁路能及时将结果送给消费者时,使用 forwarding。
  • 所需结果尚未产生,或者现有旁路无法及时送达时,使用 stall,让消费者等待。

对于紧邻的 lw 和依赖它的 add,经典五级流水线通常需要停顿一个周期,再通过 forwarding 提供加载结果。因为 add 原本需要操作数时,lw 的内存读取还没有完成。

这里假设每个阶段占一个周期,lw 的数据到 DM 阶段末尾才可用,add 在 EX 阶段使用源操作数,并具有常见的流水线寄存器到 EX 的前递路径。RISC-V 指令集本身并不规定必须采用这种流水线实现。

forwarding 能解决哪些冒险?

forwarding 将运算结果从流水线中的中间位置直接送给后续指令,使后续指令不必等待结果写回寄存器堆。

例如:

add t0, t1, t2
sub t3, t0, t4

第一条指令在 EX 阶段末尾产生 t0 的新值。下一周期,第二条指令进入 EX 时,这个值已经保存在 EX/DM 流水线寄存器中,可以通过旁路送入 ALU。

即使第一条指令还没有到 WB,第二条指令也能使用正确结果。有相应前递路径时,这种紧邻的 ALU 指令依赖通常无需停顿。

为什么紧邻的 lw 和 add 仍然需要停顿?

原示例中的 lw s1, 0(0x101) 表达了加载后立即使用的依赖关系,但它不是标准的 RISC-V 加载指令写法:括号内应为基址寄存器。

假设 t0 已经保存了有效、满足访问要求的地址,可以用下面的代码讨论相同的数据冒险:

lw s1, 0(t0)
add s2, s1, zero

如果完全不停顿,时序如下:

指令周期 1周期 2周期 3周期 4周期 5周期 6
lwIFIDEXDMWB
addIFIDEXDMWB

冲突出现在周期 4:

  • lw 正在 DM 阶段读取内存,加载结果到这个阶段末尾才可用。
  • add 同时进入 EX 阶段,需要用加载结果完成本周期的 ALU 运算。

lw 在前一周期的 EX 阶段产生的只是有效地址,不能把这个地址当成加载值传给 add。

常规前递路径只能传递已经产生且能及时使用的数据,无法提前提供仍在内存读取过程中的结果。这种紧邻的依赖称为 load-use 冒险。

停顿一个周期后,前递如何工作?

将 add 的 EX 阶段推迟一个周期,时序如下:

指令周期 1周期 2周期 3周期 4周期 5周期 6周期 7
lwIFIDEXDMWB
addIFIDID(保持)EXDMWB

常见的硬件处理方式是:

  1. 在 add 处于 ID 时,检测到它依赖前一条 lw 的目标寄存器。
  2. 保持 PC 和 IF/ID 流水线寄存器,使 add 暂时留在 ID。
  3. 向 EX 插入一个 bubble,让 lw 继续前进。
  4. 下一周期,add 进入 EX,从 DM/WB 流水线寄存器获得前递的加载结果。

这次停顿由冒险检测硬件处理,不要求程序员手工插入 nop。

什么时候还需要更多停顿?

是否需要停顿、停多久,取决于生产者何时产生结果、消费者何时需要结果,以及两者之间是否有合适的旁路。

情况常见处理方式
ALU 结果已产生,存在到后续 EX 的旁路forwarding,通常无需停顿
紧邻的 load-use 依赖停顿一个周期,再 forwarding
缺少所需的前递路径等待结果能够通过现有数据路径取得
内存访问或执行单元需要更多周期按实际完成时间继续等待

如果完全不实现 forwarding,消费者通常需要等到生产者写回寄存器堆后才能继续。具体停顿几周期,还取决于寄存器堆是否支持同一周期先写后读等实现细节,不能仅凭“五级流水线”确定。

为什么不把 DM 的结果直接送到同周期的 EX?

可以设计这样的路径,但必须重新满足时序要求。内存读出的数据还要经过旁路选择和 ALU 运算,并在同一周期结束前被锁存。这会延长组合逻辑路径,可能要求更长的时钟周期。

因此,“紧邻的 load-use 必须停顿一个周期”适用于前面约定的经典实现,RISC-V 指令集并不禁止其他设计。

“forwarding 无法处理所有冒险,所以通常应该优先 stall”的说法也不成立。停顿控制通常较简单,但会损失执行周期;前递增加硬件路径和控制逻辑,却能减少许多常见依赖造成的等待。具体取舍要结合目标频率、面积和性能要求。

备注:内容仅供参考。