第16课 · 流水线冒险

冒险数据冒险控制冒险结构冒险

📌 学习目标:识别流水线中的三类冒险(结构、数据、控制),实现数据冒险检测单元,通过 Verilator 验证检测逻辑。

一、什么是流水线冒险?

冒险(Hazard)是阻止下一条指令在预期时钟周期执行的情况。三类冒险:

类型原因解决方法
结构冒险硬件资源冲突分离指令/数据存储器
数据冒险数据依赖前递(转发) + 停顿
控制冒险分支改变PC分支预测 + 延迟槽

二、结构冒险

当两条指令同时需要同一硬件资源时发生。例如:IF 和 MEM 同时访问同一存储器。

解决方案:哈佛架构——指令存储器和数据存储器分离(RISC-V 标准做法)。

三、数据冒险

当一条指令依赖于前一条指令的结果,而结果尚未写回时发生。

3.1 RAW(Read After Write)— 最常见

ADD x1, x2, x3   ← 写 x1 (WB阶段才完成)
SUB x4, x1, x5   ← 读 x1 (ID阶段就需要!)
    ↑ 数据依赖:SUB 在 ID 读 x1 时,ADD 还没写回

3.2 数据冒险分类

3.3 冒险检测条件

检测 Load-Use 冒险:

if (ID/EX.MemRead &&       // 前条是 Load
   (ID/EX.rd == IF/ID.rs1 || // 后条需要 rs1
    ID/EX.rd == IF/ID.rs2))  // 后条需要 rs2
    → 停顿 + 冲刷

四、控制冒险

分支指令在 EX 阶段才能确定是否跳转,但 IF 已经取了顺序的下一条指令。

4.1 简单方案:停顿

检测到分支就停顿,等结果出来再取指。代价:每个分支损失 2-3 个周期。

4.2 假设不跳转

继续取顺序指令,如果实际跳转则冲刷。代价:跳转时损失 2-3 个周期。

4.3 延迟分支

分支后的指令无论跳不跳转都执行(延迟槽),编译器负责填入有用指令。

五、Verilog 冒险检测

// hazard_unit.v — 冒险检测单元 module hazard_unit ( input [4:0] id_rs1, id_rs2, // ID 阶段的 rs1/rs2 input [4:0] ex_rd, // EX 阶段的 rd input ex_mem_read, // EX 阶段 MemRead 信号 input ex_reg_write, // EX 阶段 RegWrite input [4:0] mem_rd, // MEM 阶段的 rd input mem_reg_write, // MEM 阶段 RegWrite output stall, // 停顿信号 output flush, // 冲刷信号 output [1:0] fwd_a, fwd_b // 前递选择 ); // Load-Use 冒险检测 → 停顿 assign stall = ex_mem_read && (ex_rd != 5'b0) && ((ex_rd == id_rs1) || (ex_rd == id_rs2)); // 停顿时冲刷 EX 阶段(插入气泡) assign flush = stall; // EX/MEM 前递检测 assign fwd_a = (ex_reg_write && ex_rd != 5'b0 && ex_rd == id_rs1) ? 2'b01 : (mem_reg_write && mem_rd != 5'b0 && mem_rd == id_rs1) ? 2'b10 : 2'b00; assign fwd_b = (ex_reg_write && ex_rd != 5'b0 && ex_rd == id_rs2) ? 2'b01 : (mem_reg_write && mem_rd != 5'b0 && mem_rd == id_rs2) ? 2'b10 : 2'b00; endmodule

5.1 测试台

// tb_hazard.v module tb_hazard; reg [4:0] id_rs1, id_rs2, ex_rd, mem_rd; reg ex_mem_read, ex_reg_write, mem_reg_write; wire stall, flush; wire [1:0] fwd_a, fwd_b; hazard_unit uut (.*); integer pass=0, fail=0; initial begin // 无冒险 id_rs1=1; id_rs2=2; ex_rd=3; mem_rd=4; ex_mem_read=0; ex_reg_write=1; mem_reg_write=1; #1; if (stall||fwd_a!==2'b00||fwd_b!==2'b00) begin $display("FAIL no hazard"); fail=fail+1; end else pass=pass+1; // Load-Use 冒险 id_rs1=3; id_rs2=2; ex_rd=3; ex_mem_read=1; #1; if (!stall) begin $display("FAIL load-use not detected"); fail=fail+1; end else pass=pass+1; // EX/MEM 前递检测 id_rs1=3; id_rs2=4; ex_rd=3; mem_rd=4; ex_mem_read=0; ex_reg_write=1; mem_reg_write=1; #1; if (fwd_a!==2'b01||fwd_b!==2'b10) begin $display("FAIL fwd: a=%b b=%b", fwd_a, fwd_b); fail=fail+1; end else pass=pass+1; // x0 寄存器不应触发前递 id_rs1=0; id_rs2=3; ex_rd=0; ex_mem_read=0; ex_reg_write=1; mem_reg_write=0; mem_rd=0; #1; if (fwd_a!==2'b00) begin $display("FAIL x0 fwd"); fail=fail+1; end else pass=pass+1; $display("========================================"); $display("冒险检测测试: PASS=%0d FAIL=%0d", pass, fail); if (fail == 0) $display("✅ 数据冒险检测全部正确!"); else $display("❌ 存在失败!"); $display("========================================"); $finish; end endmodule

六、Verilator 编译命令

verilator --cc hazard_unit.v --exe tb_hazard.v \ --build --top-module tb_hazard ./obj_dir/Vtb_hazard

🤔 思考题:为什么 x0 寄存器不应触发冒险检测和前递?

💡 提示:x0 硬连线为 0,即使有写 x0 的指令也不会改变 x0 的值

🏆 成就解锁:冒险检测专家

✅ Verilator 仿真验证通过

✅ Load-Use 冒险检测正确

✅ EX/MEM 前递检测正确

✅ x0 寄存器特殊处理正确

🎯 下一目标:前递实现 → 第17课:前递