流水线让CPU同时执行多条指令,但指令之间会打架。结构冒险抢硬件,数据冒险抢数据,控制冒险抢方向。这篇聊聊这三种冒险的本质和应对方法。
1. 结构冒险:硬件不够用了
1.1 什么是结构冒险
多条指令同时需要同一个硬件资源[1]。比如:
- 两条指令都要写寄存器,但寄存器堆只有一个写端口
- 两条指令都要用除法器,但除法器只有一个
- 取指和访存同时访问内存,但只有一个内存端口
1.2 MIPS浮点流水线的例子
MIPS的浮点单元有多个功能部件[2]:
- FP Adder:3-4周期
- FP Multiplier:6-7周期
- FP Divider:24周期(非流水化)
寄存器写端口冲突:
Cycle 11:
ADD.D F2, F4, F6 ; 完成,要写F2
MUL.D F8, F10, F12 ; 完成,要写F8
L.D F14, 0(R2) ; 完成,要写F14
三个指令同时到达WB阶段,但FP寄存器堆只有一个写端口[2]。这就是结构冒险。
解决方案:
- 增加写端口:成本高,利用率低(大部分时间只有一个写)
- 检测并停顿:在ID阶段检测冲突,延迟发射
- 移位寄存器跟踪:记录未来哪些周期会写寄存器,提前冲突检测
1.3 除法单元的独占
MIPS的FDIV需要24周期,且非流水化[2]:
Cycle 1: FDIV.D F1, F2, F3 ; 开始除法
Cycle 2-24: 其他指令执行,但第二个FDIV必须等
Cycle 25: FDIV.D F4, F5, F6 ; 下一个除法才能开始
这是结构冒险的极端情况——功能部件被长期占用。
解决方案:
- 流水化除法器:把24周期拆成多级,每级可重叠
- 增加除法单元:成本高,除法不频繁
- 编译器调度:在除法后面插入无关指令
2. 数据冒险:数据还没准备好
2.1 RAW(Read After Write)
最普遍的冒险。后一条指令读前一条指令要写的结果,但还没写好。
L.D F4, 0(R2) ; 加载数据到F4
MUL.D F0, F4, F6 ; 用F4做乘法
L.D在MEM阶段才拿到数据,MUL.D在EX阶段就需要F4。即使转发,也需要停顿1周期[3]。
转发(Forwarding/Bypassing)[3][4][5]:
- 把ALU结果直接传给下一条指令的EX输入
- 把MEM结果传给EX输入
- 减少但无法完全消除停顿
Load-Use Hazard的特殊性[3]:
LW x1, 0(x2) ; 加载
ADD x3, x1, x1 ; 立即使用x1
Load数据在MEM阶段末尾才可用,ADD在EX阶段就需要。转发无法解决(不能向过去转发),必须停顿1周期或插入NOP[3]。
2.2 WAW(Write After Write)
两条指令写同一个寄存器,可能乱序完成导致错误结果。
MIPS浮点例子[2]:
ADD.D F2, F4, F6 ; 延迟3周期
L.D F2, 0(R2) ; 延迟1周期
L.D比ADD.D快,可能先写F2,然后ADD.D覆盖它。程序期望的是ADD.D的结果。
解决方案:
2.3 WAR(Write After Read)
后一条指令写前一条指令要读的寄存器。但在流水线中,读总是在ID阶段,写在WB阶段,所以WAR不会自然发生[3]。
只有在乱序执行且读延后的情况下才可能发生,现代CPU通过寄存器重命名消除[7][8]。
3. 控制冒险:分支走哪条路
3.1 分支延迟
5级流水线中,分支在EX阶段才确定目标地址:
Cycle 1: BEQ taken? (EX阶段计算)
Cycle 2: 如果预测错误,已取两条错误指令
分支预测失败的代价:
3.2 预测与恢复
静态预测:总是预测不跳转,或编译器标记[10]
动态预测:基于历史记录预测[10]
- Bimodal:简单的2位饱和计数器
- Global:考虑全局分支历史
- Local:考虑每个分支的历史
恢复机制:
3.3 精确异常
异常发生时,处理器状态必须与程序顺序一致[6][7][8]。ROB保证:
- 异常指令前的所有指令已提交
- 异常指令后的指令未修改状态
- 可以正确恢复并跳转到异常处理程序
4. 三类冒险的对比
| 冒险类型 | 原因 | 检测阶段 | 解决方案 | 代价 |
|---|---|---|---|---|
| 结构冒险 | 资源争用 | ID | 增加资源/停顿调度 | 面积/性能 |
| RAW | 数据未就绪 | ID/EX | 转发/停顿/乱序执行 | 逻辑复杂度 |
| WAW | 乱序写回 | ID | ROB顺序提交/重命名 | 硬件复杂度 |
| 控制冒险 | 分支方向 | EX | 预测/延迟槽/ROB | 预测失败惩罚 |
5. 现代处理器的应对
5.1 乱序执行(Out-of-Order)
- 指令顺序发射(Issue)
- 乱序执行(Execute)
- 顺序提交(Commit)
消除WAW/WAR,隐藏RAW延迟。
5.2 推测执行(Speculative Execution)
分支预测后,提前执行后续指令[7]:
- 预测正确:加速执行
- 预测错误:ROB丢弃结果,恢复状态
5.3 转发网络
- EX→EX:ALU结果直接转发
- MEM→EX:Load数据转发
- WB→EX:写回阶段转发
6. 总结
流水线冒险的本质是并行执行与程序顺序的冲突。
结构冒险:硬件资源有限,需要复制或调度
数据冒险:数据依赖存在,需要转发或等待
控制冒险:程序流程不确定,需要预测和恢复
现代CPU通过乱序执行+ROB+寄存器重命名+分支预测的组合拳,把冒险的影响降到最低。但这些机制增加了硬件复杂度和功耗,是性能与成本的永恒权衡。
参考
-
GeeksforGeeks. Pipeline Hazards. ↩
-
Jyoti Prakash Blog. Delving Deeper into the MIPS Pipeline. FP pipeline hazards. ↩ ↩ ↩ ↩ ↩
-
Chipmunk Logic. Designing RISC-V CPU from scratch – Part 3. Pipeline hazards. ↩ ↩ ↩ ↩ ↩ ↩ ↩
-
Lori Academic Direct. Data hazards examples. ↩
-
Imperial College London. EIE2 Instruction Architectures & Compilers Lecture 8. ↩
-
IEEE. Characterizing the branch misprediction penalty. ↩
-
UPC Commons. Bypassing in pipelines. ↩