Appearance
流水线冒险及其处理
2026 大纲 五(六)3 结构冒险、数据冒险和控制冒险的处理。
三类冒险不是列举出来的,是穷举出来的
一条指令要在流水线里顺利往前走,需要三件东西同时各就各位:它要用的部件空着、它要读的数据已经备好、它自己确实该被执行。三个条件各对应一类冒险——结构冒险、数据冒险、控制冒险,不多也不少。
这三类里,第一类在标准五段流水线中其实已经被结构设计消除了:哈佛结构(分离的 I-Cache 与 D-Cache)解决了 IF 与 MEM 抢存储器,寄存器堆前半周期写、后半周期读解决了 WB 与 ID 抢寄存器堆。所以后面两节主要处理数据冒险与控制冒险。
但那个"前半写后半读"的假设,本身就是一个必须先确认的前提:
🔴 它不只消除结构冒险,还使 WB 与 ID 能落在同一拍,从而减少数据冒险的停顿数。 同一道题,支持这个性质就停 2 拍、不支持就停 3 拍。判断停顿数之前,必须先确定题面给的寄存器堆有没有这个性质。
还有一条贯穿全篇的判题纪律:
🔴 判冒险要精确到"哪两条指令的哪两段落在同一个周期",不能只看寄存器名相同。名字相同但时间上已经错开的,根本不构成冒险——这是这类题最主要的误判来源。
交互可视化
一、三类冒险的根源
流水线的前提是"多条指令同时在机器里跑",一旦如此就会在三个层面上出冲突:
| 冒险类型 | 根源 | 冲突的是 |
|---|---|---|
| 结构冒险 | 硬件资源不够 | 部件 |
| 数据冒险 | 指令间存在数据依赖 | 数据 |
| 控制冒险 | 分支改变了执行流 | 指令地址 |
结构冒险在标准五段里有两处,都要先看清"什么时候会撞"才知道解法在解什么:
- 取指与访存撞车:lw 指令在 MEM 段访问数据存储器时,后面第三条指令正在 IF 段取指令;若指令和数据放在同一个存储器里,两者冲突。⇒ 指令存储器与数据存储器分开(哈佛结构)。
- 寄存器堆的读写撞车:一条指令在 WB 段写寄存器堆,另一条同时在 ID 段读寄存器堆。⇒ 寄存器堆支持前半周期写、后半周期读。
二、数据冒险的三种相关
| 类型 | 全称 | 定义 | 示例 |
|---|---|---|---|
| RAW(写后读) | Read After Write | 指令 j 要读指令 i 将写入的寄存器 | I1: ADD R1,R2,R3 → I2: SUB R4,R1,R5 |
| WAR(读后写) | Write After Read | 指令 j 要写的寄存器,指令 i 还没读完旧值 | I1: ADD R3,R1,R2 → I2: SUB R1,R4,R5 |
| WAW(写后写) | Write After Write | i 和 j 写同一寄存器,语义要求 j 的结果是最终值 | I1: ADD R1,R2,R3 → I2: SUB R1,R4,R5 |
三种里只有一种在按序流水线中会真出问题:
🔴 按序流水线中只有 RAW 是真问题。 读发生在 ID、写发生在 WB,且指令严格按序前进:WAR 的读(ID)永远早于后面指令的写(WB);WAW 的写顺序天然等于程序顺序;只有 RAW 的读可能早于前一条的写(ID 离 WB 隔着三拍),会违反语义——所以 RAW 又叫真相关。
🔴 WAR 与 WAW 是"名相关",可以用寄存器重命名消除。 它们只在乱序执行中才出现(后一条抢先写回,或先发的后完成写回)。它们的本质不是数据流依赖,只是两条指令恰好用了同一个寄存器名,把逻辑寄存器映射到不同的物理寄存器就没了。RAW 无法用重命名消除——它是真实的数据流动。
处理办法有两种:阻塞(插入气泡,让后面的指令停在原地等到数据可用)与数据转发(forwarding/旁路 bypassing,把结果从流水段寄存器直接送到需要它的部件输入端,不必绕一圈写回寄存器堆再读出来)。选哪种、要停几拍,判据只有一条:
🔴 比较"数据在哪一段末尾产生"与"在哪一段开头被需要":产生不晚于需要就能转发;晚了就必须先停顿把时间差抹平,再转发。ALU 结果在 EX 段末产生、下一条在 EX 段初需要,刚好够,所以大部分 RAW 能被转发完全消除,一个气泡都不用插。
🔴 load-use 是唯一转发也救不了的数据冒险。
lw的数据要到 MEM 段末尾才拿到,而下一条的 EX 段在同一个周期开头就要用——晚了整整一拍,必须停 1 个周期。编译器在lw之后插一条无关指令(指令调度)就能把这一拍填上。

(袁春风《计算机组成与系统结构》第 3 版,见文末教材出处)
图中 add 在 Wr 段才写回 $1,而随后四条指令都要读 $1(灰底的 Reg 段)。斜线标出的是数据依赖:前两条读到的是旧值(真冒险),第三条正好在同一周期先写后读,第四条已经在写回之后。
阻塞与转发的逐拍时空图:停顿到底几拍、lw-use 为什么转发也救不了(想把"产生时刻 vs 需要时刻"落到具体周期上时展开)
(一)纯阻塞。 I1: ADD R1,R2,R3 的 R1 在 EX 段(T3)末尾算出、T5 才写回;I2: SUB R4,R1,R5 原本 T3 就要在 ID 段读 R1。
| 指令 / 周期 | T1 | T2 | T3 | T4 | T5 | T6 | T7 |
|---|---|---|---|---|---|---|---|
I1: ADD R1,R2,R3 | IF | ID | EX | MEM | WB | ||
I2: SUB R4,R1,R5 | IF | 停 | 停 | ID | EX | MEM |
若寄存器堆支持前半周期写、后半周期读,I2 的 ID 可与 I1 的 WB 同在 T5 完成——ID 从 T3 推迟到 T5,停顿 2 个周期;若不支持,ID 只能等到 T6,停顿 3 个周期。两种假设都成立,答案却差一拍。
(二)转发消除停顿。 同一对指令,把 I1 的 EX 结果直接送进 I2 的 EX 输入端:
| 指令 / 周期 | T1 | T2 | T3 | T4 | T5 |
|---|---|---|---|---|---|
I1: ADD R1,R2,R3 | IF | ID | EX | MEM | WB |
I2: SUB R4,R1,R5 | IF | ID | EX | MEM | |
| 转发 | └──→ | I1 的 EX 结果送入 I2 的 EX 输入 |
一个气泡都不用插。
(三)load-use:转发救不了的那一种。 I1: lw R1,0(R2) 的 R1 在 MEM 段(T4)末尾才拿到,而 I2: ADD R3,R1,R4 的 EX 段在 T4 开头就要用——晚了整整一拍:
| 指令 / 周期 | T1 | T2 | T3 | T4 | T5 | T6 | T7 |
|---|---|---|---|---|---|---|---|
I1: lw R1,0(R2) | IF | ID | EX | MEM | WB | ||
I2: ADD R3,R1,R4 | IF | ID | 停 | EX | MEM | WB | |
| 转发 | └──→ | MEM/WB 送入 EX 输入 |
先停 1 拍把时间差抹平,再转发。
三、控制冒险与分支预测
分支指令到来时,流水线不知道下一条该从哪里取,已经取入的后续指令可能全是错的。静态预测的结果与运行时历史无关:
| 策略 | 适合 |
|---|---|
| 总是预测不发生 | 前向分支(if 的条件跳转,通常不跳) |
| 总是预测发生 | 后向分支(循环回边,通常跳) |
| 按跳转方向预测 | 向后预测发生、向前预测不发生,综合上面两条 |
第三条之所以有效,是因为循环的回边一定是向后跳的,而循环体通常要执行很多次——零成本却相当准。动态预测依据运行时的历史记录:1 位预测器记录上次是否发生、本次按上次结果预测;2 位预测器(饱和计数器)四个状态,需要连续错两次才改变预测方向。

(袁春风《计算机组成与系统结构》第 3 版,见文末教材出处)
四个状态排成一条线:强发生(11) ↔ 弱发生(10) ↔ 弱不发生(01) ↔ 强不发生(00)。预测正确就往本方向的"强"端走一格(到头不动),预测错误就往反方向退一格。
🔴 2 位预测器更准的全部原因就是"连错两次才翻转方向",它对偶发的例外有容忍度。看循环退出这个典型场景:1 位预测器会立刻翻成"不发生",下一轮循环第一次又错,一个循环错两次;2 位预测器只从强发生退到弱发生、方向没变,一个循环只错一次。
控制冒险的代价有个简单算法:
🔴 代价
判定所在段的编号 。 标准五段在 EX 段判定 此时已经取入 2 条错误指令 代价 2 个周期;若硬件把比较器与地址计算提前到 ID 段,代价就降为 1 个周期。优化方向永远是让分支结果尽早出来。
🔴 插入气泡(stall)与清除(flush)方向相反,别混: stall 用于数据冒险,让后面的指令停在原地等,那些指令之后还会正常执行;flush 用于控制冒险预测错误,把已经进入流水线的错误指令作废,它们永远不会执行。
⚠️ 顺带记一条已被淘汰的方案:延迟分支。延迟槽内的指令不管跳不跳都要执行,由编译器负责填。它有两个致命问题——① 延迟槽的数量被写进了指令集,流水线一加深就需要更多槽,而旧程序只填了一个;② 编译器常常找不到指令可填,只能填 NOP。这属于"用软件迁就硬件",现代处理器一律改用分支预测。
控制冒险与延迟分支的逐拍时空图:那 2 个周期具体丢在哪两条指令上(想弄清 flush 掉的是谁、延迟槽插在什么位置时展开)
(一)预测失败的 flush。 分支在 EX 段才确定,此时流水线已按顺序取了 I2、I3:
| 指令 / 周期 | T1 | T2 | T3 | T4 | T5 |
|---|---|---|---|---|---|
BEQ R1,R2,target | IF | ID | EX(判定) | MEM | WB |
| 顺序指令 I2 | IF | ID | flush | ||
| 顺序指令 I3 | IF | flush | |||
| 目标地址指令 T | IF | ID |
若实际发生跳转,I2、I3 必须清除,代价 2 个周期。把判定提前到 ID 段,被取入的错误指令就只剩 1 条。
(二)延迟分支。 延迟槽里的指令跳不跳都执行:
| 指令 / 周期 | T1 | T2 | T3 | T4 | T5 | 说明 |
|---|---|---|---|---|---|---|
BEQ R1,R2,target | IF | ID | EX | MEM | WB | 分支本身 |
| 延迟槽指令 D | IF | ID | EX | MEM | 跳不跳都执行 | |
| 目标或顺序后继 | IF | ID | 从这里开始取正确路径 |
延迟槽不是"把分支延后执行",而是体系结构规定分支后一条指令一定执行。编译器负责往里塞一条无副作用且有用的指令,塞不进去才填 NOP。
两道 CPI 演算:把冒险的损失折成 CPI 增量(想核对"频率 × 未命中率 × 代价"这一乘法结构时展开)
(一) 某程序中分支指令占 20%,采用 2 位预测器,预测准确率 90%,预测错误代价 2 个周期。
理想 CPI
(二) 某段程序中 30% 的指令是 load,其中一半紧跟着使用其结果的指令;分支指令占 15%,预测准确率 85%,错误代价 2 周期。
两类冒险的贡献可以直接相加,因为它们发生在不同的指令上。
考点速记
- 三类冒险对应三个条件——部件空着、数据备好、该被执行,是穷举而非列举;结构冒险靠哈佛结构与寄存器堆"前半写后半读"在设计阶段就消除了,而后一项同时决定数据冒险停 2 拍还是 3 拍。
- 按序流水线中只有 RAW 是真问题;WAR、WAW 是名相关、只在乱序时出现、可用寄存器重命名消除。转发的判据是数据产生的时刻是否早于被需要的时刻,据此 load-use 是唯一转发也救不了的数据冒险,必须停 1 拍。
- 控制冒险代价
判定所在段编号 ,优化方向永远是让分支结果尽早出来;stall 与 flush 方向相反;延迟分支因延迟槽数量被写死进指令集而淘汰;2 位预测器更准是因为它连错两次才翻转方向。
这一节在真题里被考过的形式(下方「真题练习」里属于本篇的那几道):
- 给一段指令序列,问在无转发机制的五段流水线中哪一对存在数据冒险:先标出每条指令的写寄存器(WB 段)与读寄存器(ID 段),再看两条指令的 WB 与 ID 有没有落在同一周期之后。⚠️ 只看寄存器名相同会多选——名字相同但时间已错开的不是冒险;相隔 3 条以上的指令通常已经安全。
- 给指令序列问哪些指令的执行会发生阻塞:先分是哪类冒险。
lw后紧跟用它结果的指令是 load-use,转发也要停 1 拍;分支指令后面是控制冒险,代价按判定段算。两类要分别数。 - 问哪个操作不会引起流水线阻塞:数据旁路(转发)不会——它正是用来消除阻塞的;数据相关、条件转移、资源冲突都会。
- 挑关于数据冒险处理的错误叙述:常设的错点是"所有数据冒险都可以通过加入转发电路解决"——错,load-use 就解决不了。而"插入气泡能避免""调整指令顺序和插 nop 能解决""相邻指令的操作数相关可能引起冒险"三条都对。
- 大题里问哪些指令会因数据相关而阻塞、哪条会发生控制冒险、为什么某两条相关却不阻塞:最后那一问的答案通常是两条指令相隔够远,写回已经发生在读取之前——把两条指令的时空图画出来,用哪两段落在同一周期来答。
易错:判停顿数之前没确认寄存器堆是否"前半周期写、后半周期读"。这一条直接决定停 2 拍还是 3 拍。
易错:只凭寄存器名相同就判定冒险。要精确到哪两段落在同一周期。
易错:认为转发能解决所有数据冒险。load-use 必须停 1 拍。
易错:把 stall 与 flush 混用。前者的指令后面还会执行,后者的永远不执行。
教材出处
- 存在数据冒险的流水线例子(图 6.11):袁春风《计算机组成与系统结构》第 3 版 §6.3 流水线冒险及处理,印刷页 p176
- 两位预测位的状态转换(图 6.23):同上,印刷页 p186