数据前递 — Data Forwarding / Bypassing

RAW冒险零停顿解决:将结果直接旁路到需要它的指令

📖 数据冒险与前递原理

流水线中,后续指令需要前序指令的结果,但前序指令还没写回寄存器堆——这就是RAW(Read After Write)冒险。

朴素方案:插入停顿(Stall/NOP),等写回完成。但每个RAW冒险浪费1-2个周期,性能损失巨大。

前递方案:结果计算完成后,直接从流水线寄存器旁路到需要它的阶段,不等写回。

无前递 vs 有前递: ADD x3, x1, x2 (x3 = x1 + x2) SUB x5, x3, x4 (需要x3的新值) ❌ 无前递 (3周期停顿): ADD IF ID EX MEM WB SUB IF ID ─ ─ EX MEM WB ↑ stall stall ↑ ✅ 有前递 (0周期停顿): ADD IF ID EX MEM WB SUB IF ID EX MEM WB ↑ 从EX结果前递
前递来源符号延迟减少优先级
寄存器堆(无前递)RF0最低
MEM阶段结果MEM→ID1周期
EX阶段结果EX→ID2周期最高
EX前递优先级高于MEM:如果EX和MEM都能满足同一个源寄存器的需求,EX的结果更新(更近的指令),应优先使用。

🔬 前递检测逻辑详解

前递单元(Forwarding Unit)需要检测三种情况:

前递检测规则: 检查条件: EX/MEM阶段的目标寄存器 == ID阶段源寄存器 ┌─────────────────────────────────────────────────┐ │ EX前递: │ │ ex_regwrite && ex_rd != 0 && │ │ (ex_rd == id_rs1 || ex_rd == id_rs2) │ │ → fwd_sel = EX (01) │ │ │ │ MEM前递: │ │ mem_regwrite && mem_rd != 0 && │ │ (mem_rd == id_rs1 || mem_rd == id_rs2) │ │ → fwd_sel = MEM (10) │ │ │ │ 无前递: │ │ → fwd_sel = RF (00), 使用寄存器堆读出值 │ └─────────────────────────────────────────────────┘ 选择逻辑 (MUX): fwd_rs1 = (fwd_sel == 01) ? ex_result : (fwd_sel == 10) ? mem_result : rf_rs1;

为什么x0不前递?

RISC-V规定x0始终为0,即使指令目标寄存器是x0也不改变其值。因此:

🖥️ Verilog实现:前递单元

// Lesson 33: Data Forwarding (Bypass) for RAW hazards
module forwarding_unit #(
    parameter DATA_W = 32
)(
    input  wire [4:0]  id_rs1_i, id_rs2_i,
    input  wire [4:0]  ex_rd_i,
    input  wire        ex_regwrite_i,
    input  wire [31:0] ex_result_i,
    input  wire [4:0]  mem_rd_i,
    input  wire        mem_regwrite_i,
    input  wire [31:0] mem_result_i,
    input  wire [31:0] rf_rs1_i, rf_rs2_i,
    output reg  [31:0] fwd_rs1_o, fwd_rs2_o,
    output reg  [1:0]  fwd_sel_rs1_o, fwd_sel_rs2_o
);
    // EX前递优先级高于MEM
    always @(*) begin
        // RS1前递
        if (ex_regwrite_i && ex_rd_i != 5'd0 && ex_rd_i == id_rs1_i) begin
            fwd_rs1_o     = ex_result_i;
            fwd_sel_rs1_o = 2'b01;       // EX
        end else if (mem_regwrite_i && mem_rd_i != 5'd0
                    && mem_rd_i == id_rs1_i) begin
            fwd_rs1_o     = mem_result_i;
            fwd_sel_rs1_o = 2'b10;       // MEM
        end else begin
            fwd_rs1_o     = rf_rs1_i;
            fwd_sel_rs1_o = 2'b00;       // RF
        end
        // RS2前递(结构相同)
        if (ex_regwrite_i && ex_rd_i != 5'd0 && ex_rd_i == id_rs2_i) begin
            fwd_rs2_o     = ex_result_i;
            fwd_sel_rs2_o = 2'b01;
        end else if (mem_regwrite_i && mem_rd_i != 5'd0
                    && mem_rd_i == id_rs2_i) begin
            fwd_rs2_o     = mem_result_i;
            fwd_sel_rs2_o = 2'b10;
        end else begin
            fwd_rs2_o     = rf_rs2_i;
            fwd_sel_rs2_o = 2'b00;
        end
    end
endmodule
Verilator仿真验证通过 — RAW冒险无停顿解决,EX优先级高于MEM,x0不触发前递

代码解析

📊 前递对性能的影响

场景无前递有前递节省
EX→EX (1条间隔)2 stall0 stall2周期
MEM→EX (2条间隔)1 stall0 stall1周期
Load→EX2 stall1 stall1周期
整体IPC提升~0.5-0.7~0.8-1.0~50%+
前递不能解决所有RAW冒险。Load指令的数据在MEM阶段才可用(从内存读出),此时下一条指令已经在EX阶段——差1个周期。这就是Load-Use冒险,需要额外的1周期停顿(下一课详细讲解)。

🧪 实验练习

  1. 添加WB阶段前递:3级前递(EX/MEM/WB),消除MEM→EX的1周期停顿
  2. 实现双发射前递:2条指令可能同时需要前递,需要4路前递网络
  3. 测量前递命中率:统计EX/MEM/RF前递各占多少比例
  4. 实现条件前递:只有特定指令类型(ALU)的结果才前递,Load结果走另一条路径
RAW冒险无停顿解决
思考题:如果EX阶段是Load指令(结果在MEM阶段才可用),前递单元应该如何处理?为什么不能从EX阶段前递Load的结果?
参考资料:Hennessy & Patterson §3.4 | MIPS R2000 Forwarding | RISC-V Rocket Forwarding