流水线冒险
- 冒险: 阻止指令流中的下一条指令在其自己的指定时钟周期内执行.
- 3 类冒险:
- 结构冒险
- 在流水线模式下, 如果硬件无法同时支持指令的所有可能组合方式, 就会出现资源冒险, 从而导致结构冒险
- 在现代处理器中, 结构冒险主要发生在不太常用的特殊用途功能单元, 这种情况并非性能瓶颈, 我们一般不考虑
- 数据冒险
- 由于流水线中的指令重叠执行, 如果一条指令依赖先前指令的结果, 就可能导致数据冒险
- 控制冒险
- 分支指令及其他改变程序计数器的指令实现流水化时可能导致控制冒险
- 主要是分支跳转
- 结构冒险
- 后果: 流水线停顿, 在停顿的期间不会提取新的指令.
- 3 类冒险:
带有停顿的流水线性能
- 求解流水化实际加速比的简单公式: $$
\begin{aligned}
\text { 流水化加速比 } & =\frac{\text { 非流水化指令平均执行时间 }}{\text { 流水化指令平均执行时间 }} \
& =\frac{\text { 非流水化CPI } \times \text { 非流水化时钟周期 }}{\text { 流水化CPI } \times \text { 流水化时钟周期 }} \
& =\frac{\text { 非流水化CPI }}{\text { 流水化CPI }} \times \frac{\text { 非流水化化时钟周期 }}{\text { 流水化时钟周期 }}
\end{aligned}
$$
- 流水化处理器的理想 CPI 总是等于 1, 则流水化 CPI 为$$ \begin{align} 流水化CPI&=理想化CPI+每条指令的流水线停顿时钟周期\ &=1+每条指令的流水线停顿时钟周期 \end{align} $$
- 如果忽略流水化的周期时间开销, 并假定流水级之间达到完美平衡, 则两个处理器的周期时间相等$$ 加速比=\frac{非流水化CPI}{1+每条指令的流水线停顿周期} $$
- 则得到最后的加速比$$ 加速比=\frac{流水线深度}{1+每条指令的流水线停顿周期} $$
数据冒险
- 当流水线改变堆操作数的读写访问顺序, 使该顺序不同于在非流水化处理器上的执行指令顺序, 可能会发生数据冒险.
- 3 类数据冒险 (指令 i、j):
- 写后读 (RAW) 冒险: 指令 j 对寄存器 x 的读取发生在指令 i 对寄存器 x 的写入之前, 发生 RAW 冒险
- 读后写 (WAR) 冒险: 指令 i 对寄存器 x 的读取发生在指令 j 对寄存器 x 的写入之后, 发生 WAR 冒险
- 写后写 (WAW) 冒险: 指令 i 对寄存器 x 的写入发生在指令 j 对寄存器 x 的写入之后, 发生 WAW 冒险
[!attention] WAR 与 WAW 冒险 WAR 和 WAW 冒险在简单的五级整数流水线中不可能发生, 但是在指令执行顺序变化时会发生, 即后续会提到的乱序执行
- 举例:
- 这一段指令在
sub
和and
处会发生 RAW 冒险 - 在
or
指令处不会发生 RAW 冒险, 因为写是在前半周期执行的, 读是在后半周期执行的
- 这一段指令在
- 流水线的安全与否:
dist(i, j) <= dist(X, Y)
时候, 流水线不安全- 在 RISC-V 的 5 级流水线中,
dist(i, j) <= dist(ID, WB) = 3
- 在 RISC-V 的 5 级流水线中,
- 数据冒险的解决方法:
- 流水线暂停 (halt)
- 数据前推 (forward)
流水线暂停
- 暂停: 使相关的指令暂停处理, 直到该指令所需要的源数据就绪
- 停止所有下游的流水段处理
- 排干所有上游的流水段任务
- 如何实现暂停
- 关闭 PC 和 IR 的锁存, 确保被暂停的指令停留在其原来的阶段
- 向被阻塞阶段的后续流水段插入 nop 指令/气泡 bubble
- 何时暂停:
dist(i, j) <= dist(ID, WB)
- 暂停条件的检测逻辑:
rs(I)
返回指令 I 的 rs 域 (可能存在冲突的域)- \((rs(IR_{ID})==dest_{EX})\quad \&\&\quad RegWrite_{EX}\)
- \((rs(IR_{ID})==dest_{MEM})\quad \&\&\quad RegWrite_{MEM}\)
- \((rs(IR_{ID})==dest_{WB})\quad \&\&\quad RegWrite_{WB}\)
- 流水线暂停的影响:
- 浪费了一个周期
- 降低 CPI
数据前推
- 基本思想: 当指令所需的操作数可用时, 尽快将其送给相关的指令
- 数据流视角的解析
add rx ry rz
的意义:- 获得最新的
R(ry)
和R(rz)
的状态 - 通过
add
重新定义R(rx)
的状态 - 后续指令访问
R(rx)
所获得的值就是当前指令add
的结果, 直到另一条指令重新定义R(rx)
- 获得最新的
- 保证计算的正确性的关键:
- 维持正确的数据流
- 正确的异常