Load-Use冒险 — Load-Use Hazard Detection

前递无法消除的最后1周期停顿

📖 为什么前递不能解决所有冒险?

上一课的前递可以解决ALU指令之间的RAW冒险,但Load指令是特殊情况

Load-Use冒险时序: LW x3, 0(x1) ← Load指令,数据在MEM阶段才从内存读出 ADD x5, x3, x4 ← 需要x3,但x3还没从内存回来 Cycle: 1 2 3 4 5 6 7 LW: IF ID EX MEM WB ↑ 数据在这里才可用 ADD: IF ID EX ... ← 需要x3在EX阶段! 问题: ADD的EX阶段和LW的MEM阶段在同一个周期 但MEM阶段结束时数据才可用,EX阶段开始时就需要 → 差了半个周期!无法前递! 解决方案: 插入1周期停顿 LW: IF ID EX MEM WB stall: 🫥 ← 气泡 ADD: IF ID ─ EX MEM WB ↑ 现在可以从MEM前递

Load-Use是流水线中唯一需要硬件停顿的数据冒险。其他RAW冒险都可以通过前递零停顿解决。这个1周期停顿在现代处理器中约占所有停顿的10-20%。

冒险类型前递可解决?停顿周期频率
ALU→ALU (EX→EX)✅ 是0~30%
ALU→ALU (MEM→EX)✅ 是0~20%
Load→ALU (Load-Use)❌ 否1~15%
无冒险0~35%

🔬 Load-Use检测条件

Load-Use冒险的检测条件比普通前递更严格:

Load-Use检测条件: 必须同时满足: 1. EX阶段的指令是Load (ex_memread == 1) 2. EX阶段有目标寄存器 (ex_rd != 0) 3. ID阶段的指令读该寄存器 (ex_rd == id_rs1 || ex_rd == id_rs2) 4. 两条指令都有效 (ex_valid && id_valid) ┌──────────────────────────────────────────────────┐ │ if (ex_valid && ex_memread && id_valid) │ │ if (ex_rd == id_rs1 || ex_rd == id_rs2) │ │ if (ex_rd != 0) │ │ → STALL + FLUSH │ └──────────────────────────────────────────────────┘ 停顿操作: • PC不递增(重新取指) • ID/EX寄存器清零(插入气泡) • IF/ID寄存器保持(阻止ID推进)
编译器可以通过指令调度减少Load-Use停顿:在Load和Use之间插入一条无关指令。这就是为什么编译器会做Load延迟槽填充(虽然RISC-V没有延迟槽,但编译器仍尽量调度指令避免紧邻Load使用结果)。

🖥️ Verilog实现:Load-Use冒险检测

// Lesson 34: Load-Use Hazard Detection + 1-cycle Stall
module load_use_hazard #(
    parameter DATA_W = 32
)(
    input  wire [4:0]  id_rs1_i, id_rs2_i,
    input  wire        id_valid_i,
    input  wire [4:0]  ex_rd_i,
    input  wire        ex_memread_i,    // EX阶段是Load
    input  wire        ex_valid_i,
    output reg         stall_o,         // 1周期停顿
    output reg         flush_id_o,      // 清空ID
    output reg         flush_ex_o       // 清空EX(气泡)
);
    always @(*) begin
        stall_o = 0; flush_id_o = 0; flush_ex_o = 0;
        if (ex_valid_i && ex_memread_i && id_valid_i) begin
            if ((ex_rd_i == id_rs1_i && ex_rd_i != 5'd0) ||
                (ex_rd_i == id_rs2_i && ex_rd_i != 5'd0)) begin
                stall_o    = 1;  // 停顿取指和ID
                flush_id_o = 1;  // 阻止ID推进到EX
                flush_ex_o = 1;  // EX插入气泡
            end
        end
    end
endmodule
Verilator仿真验证通过 — Load-Use冒险检测+1周期停顿正确,x0不触发停顿

代码解析

📊 减少Load-Use停顿的优化技术

技术原理效果复杂度
编译器调度在Load-Use之间插入无关指令消除50-70%
Load-Use前递MEM阶段前半周期读内存,后半周期前递消除100%
提前读(Early Read)将内存读提前到EX阶段末尾消除100%
乱序执行后续无关指令先执行,Load结果出来后再执行依赖指令消除100%很高

在现代高性能处理器中,Load-Use停顿通过快速缓存访问乱序执行几乎完全消除。但在简单的顺序流水线中,1周期停顿是最实用的方案。

🧪 实验练习

  1. 实现Load-Use前递:MEM阶段的数据在周期后半段前递到EX阶段,消除停顿
  2. 统计Load-Use停顿频率:跑CoreMark统计停顿占比
  3. 实现编译器调度:用汇编手动调度指令,观察停顿减少
  4. 添加Store-Load前递:Store后紧跟Load同一地址时,从Store缓冲区前递
检测+1周期停顿
思考题:为什么MIPS有Load延迟槽而RISC-V没有?RISC-V选择没有延迟槽的设计决策对Load-Use停顿有什么影响?
参考资料:Hennessy & Patterson §3.5 | MIPS R2000 Load Delay Slot | RISC-V Spec §2.5