Data Hazard Resolution in a 5-Stage RISC-V Pipeline: Forwarding & Stall
Implementing dual-path data forwarding (MEM→EX, WB→EX) and load-use hazard detection with single-cycle stall in a pipelined RISC-V processor.
TL;DR
双路转发消除 RAW 冒险(0 周期损失),load-use 冒险单周期停顿,零气泡分支冲刷。
1. The Problem: RAW Data Hazards
流水线的本质是时间上的指令级并行——多条指令的不同阶段在同一时刻执行。当后续指令依赖前序指令尚未写回的结果时,Read After Write (RAW) 数据冒险就产生了。
考虑以下指令序列:
Cycle 1: ADD x1, x2, x3 [IF]
Cycle 2: ADD x1, x2, x3 [ID] SUB x4, x1, x5 [IF]
Cycle 3: ADD x1, x2, x3 [EX] SUB x4, x1, x5 [ID]
Cycle 4: ADD x1, x2, x3 [MEM] SUB x4, x1, x5 [EX] ← SUB 读 x1
Cycle 5: ADD x1, x2, x3 [WB] SUB x4, x1, x5 [MEM] ADD 在第 5 周期的 WB 阶段才将结果写入寄存器堆,但 SUB 在第 4 周期的 EX 阶段就需要 x1 作为 ALU 操作数。没有转发机制的话,SUB 从寄存器堆读到的是 x1 的旧值——程序行为完全错误。
解决方案的核心思想:不要等数据写回寄存器堆,直接从流水线寄存器中转发(Forward)。
2. Forward Unit: Dual-Path Data Forwarding
转发单元在 EX 阶段插入一个 3:1 多路选择器(MUX),根据目标寄存器地址匹配结果,决定 ALU 操作数的来源:
Forward MUX (RS1)
┌───────────────────────────────────┐
│ │
EX/MEM 寄存器 │ 2'b01 (最高优先级: MEM→EX 转发) │──▶ ALU 操作数 A
────────────▶ │
│ │
MEM/WB 寄存器 │ 2'b10 (次优先级: WB→EX 转发) │──▶ ALU 操作数 A
────────────▶ │
│ │
Register File │ 2'b00 (默认: 从寄存器堆读取) │──▶ ALU 操作数 A
────────────▶ │
└───────────────────────────────────┘ 转发条件判断的核心逻辑——同一套比较器适用于 RS1 和 RS2:
// RS1 转发优先级
always_comb begin
forward_a = 2'b00; // 默认: RegFile
if (ex_reg_we && (ex_rd_addr != 5'h0) && (ex_rd_addr == id_rs1_addr))
forward_a = 2'b01; // MEM 前递 (最高优先级)
else if (mem_reg_we && (mem_rd_addr != 5'h0) && (mem_rd_addr == id_rs1_addr))
forward_a = 2'b10; // WB 前递
end
三个判断条件缺一不可:
ex_reg_we/mem_reg_we— 上游指令确实写寄存器(排除SW、BEQ等不写 rd 的指令)ex_rd_addr != 5'h0— 目标不是 x0(RISC-V 硬编码的零寄存器,写入被忽略)ex_rd_addr == id_rs1_addr— 上游写入的寄存器正好是当前指令要读的源操作数
优先级设计原因: MEM→EX 转发的数据比 WB→EX 更新一个周期(来自更近的流水线阶段),因此必须赋予更高优先级。两个阶段同时匹配时(极端情况),取最新的。
3. Hazard Unit: Load-Use Detection
转发并非万能。当一条 LW(Load Word)指令紧跟一条使用其结果的指令时,转发无法解决问题:
Cycle 1: LW x1, 0(x2) [IF]
Cycle 2: LW x1, 0(x2) [ID] ADD x4, x1, x5 [IF]
Cycle 3: LW x1, 0(x2) [EX] ADD x4, x1, x5 [ID]
LW: 计算有效地址 ADD: 解码,发现依赖 x1
Cycle 4: LW x1, 0(x2) [MEM] ADD x4, x1, x5 [EX] ← 转发无意义!
LW: 从内存读取数据 ADD: 需要 ALU 操作数
↑ 数据在 MEM 结束后才可用 ↑ ADD 必须在此时拿到操作数
时间线冲突,无法转发 根本原因: LW 在 MEM 阶段才从数据存储器拿到数据,但 ADD 在 EX 阶段就需要。即使你在 MEM 结束后转发,ADD 的 EX 阶段已经过去了——ALU 已经用旧值算完了。转发只能从 EX/MEM 或 MEM/WB 拿结果,而 LW 在 EX 阶段的结果是内存地址,不是数据。
因此必须 停顿(Stall)一个周期,让 ADD 的 EX 阶段推迟到 LW 拿到数据之后:
// Load-Use 冒险检测
load_use_hazard = ex_mem_re && (ex_rd_addr != 5'h0) &&
((ex_rd_addr == id_rs1_addr) || (ex_rd_addr == id_rs2_addr));
// 控制信号
stall_pipeline = load_use_hazard; // 停顿 IF 和 ID
flush_ex = load_use_hazard; // 清空 EX 中的无效指令
停顿期间的流水线状态:
- PC 冻结 — 不取新指令(IF 阶段停顿)
- IF/ID 寄存器冻结 — ID 阶段的指令保持不变(重复解码)
- EX 阶段插入气泡(bubble) — NOP 操作,不产生副作用
- MEM / WB 正常推进 — LW 继续从内存读取数据
停顿之后,LW 的数据出现在 MEM/WB 流水线寄存器中,此时 ADD 在 ID 阶段,下一周期进入 EX 时可以从 WB 路径转发。总开销:1 个时钟周期。
4. Pipeline Flush Logic
冲刷(Flush)是清除已经进入流水线的错误指令。三种场景对应不同的冲刷范围:
场景 1: 分支预测失败 (Branch Mispredict)
┌────────┬────────┬────────┬────────┬────────┐
│ IF │ ID │ EX │ MEM │ WB │
│ wrong │ wrong │ 正确! │ │ │
│ ← flush │ │ │ │
└────────┴────────┴────────┴────────┴────────┘
EX 阶段计算出实际分支结果 → 冲刷 IF/ID 中的错误路径指令
场景 2: Load-Use Hazard
┌────────┬────────┬────────┬────────┬────────┐
│ IF │ ID │ EX │ MEM │ WB │
│ │ stall │ bubble │ │ │
│ freeze │ freeze │ ← flush │ │ │
└────────┴────────┴────────┴────────┴────────┘
ID 阶段检测到冒险 → 冻结 IF/ID,EX 插入气泡
场景 3: 异常/中断 (Exception / Interrupt)
┌────────┬────────┬────────┬────────┬────────┐
│ IF │ ID │ EX │ MEM │ WB │
│ flush │ flush │ flush │ flush │ trap! │
└────────┴────────┴────────┴────────┴────────┘
WB 阶段触发 trap → 冲刷整条流水线 → 跳转到异常处理入口 分支冲刷的优化: 零气泡分支(zero-bubble branch)通过在 ID 阶段提前计算分支条件实现。如果在 ID 阶段就能确定分支方向,IF 阶段取到的下一条指令可能是错误的,但只有 1 条需要冲刷(而非等到 EX 阶段才发现,那时已经取了 2 条错误指令)。配合 BTB(Branch Target Buffer)预测跳转目标地址,可将分支惩罚进一步压缩到 0 周期。
5. Timing Diagram: Forwarding in Action
一个完整的双路转发时序,展示 ADD 的结果如何通过 EX/MEM 路径转发给 SUB:
Cycle 1: ADD x1, x2, x3 [IF]
Cycle 2: ADD x1, x2, x3 [ID] SUB x4, x1, x5 [IF]
Cycle 3: ADD x1, x2, x3 [EX] SUB x4, x1, x5 [ID]
Cycle 4: ADD x1, x2, x3 [MEM] SUB x4, x1, x5 [EX]
┊ ↑
┊ ADD 的 ALU 结果 (x2+x3) ┊
┊ 已经锁存在 EX/MEM 流水线寄存器中 ┊
┊ ┊
└──── Forward MUX: 选择 EX/MEM ──────┘
操作数 A = forwarded x1
Cycle 5: ADD x1, x2, x3 [WB] SUB x4, x1, x5 [MEM]
Cycle 6: SUB x4, x1, x5 [WB] 关键观察: SUB 在第 4 周期的 EX 阶段执行 ALU 运算时,操作数 A 来自转发路径而非寄存器堆。结果完全正确,且零停顿。这就是转发的价值——用硬件比较器和 MUX 的延迟(通常 1-2 个门延迟)换取 0 个时钟周期的流水线损失。
6. Verification
验证数据冒险处理的完整性:
| 冒险类型 | 解决方案 | 流水线损失 |
|---|---|---|
| RAW (EX→EX) | MEM→EX 转发 | 0 周期 |
| RAW (MEM→EX) | WB→EX 转发 | 0 周期 |
| Load-Use | 单周期 Stall + WB→EX 转发 | 1 周期 |
| 分支预测失败 | ID 阶段 Flush | 0 周期气泡 |
| 异常 | 全流水线 Flush | N 周期(固定开销) |
性能影响量化: 假设 CPI = 1(理想),load-use 频率约 10-15%,分支频率约 15-20%。转发消除的 RAW 冒险原本需要插入 2 个气泡(无转发时每次 RAW 损失 2 周期),转发后降至 0。Load-use 的 1 周期停顿是不可避免的硬件限制。
转发单元的面积开销:2 个 3:1 MUX + 6 组比较器(每路 RS1/RS2 各 3 组地址比较),约占流水线总面积的 3-5%。时序开销:转发 MUX 在 EX 阶段关键路径上增加约 0.1-0.2ns 延迟,在典型 100MHz RISC-V 核心中不构成瓶颈。
Related Articles:
- 5-Stage Pipelined RISC-V CPU — The complete processor architecture.
- BTB Branch Prediction — How the BTB reduces branch penalties.