Skip to content

流水线冒险及其处理

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 Writei 和 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 之后插一条无关指令(指令调度)就能把这一拍填上。

图 6.11 存在数据冒险的流水线例子
图 6.11 存在数据冒险的流水线例子
(袁春风《计算机组成与系统结构》第 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。

指令 / 周期T1T2T3T4T5T6T7
I1: ADD R1,R2,R3IFIDEXMEMWB
I2: SUB R4,R1,R5IFIDEXMEM

若寄存器堆支持前半周期写、后半周期读,I2 的 ID 可与 I1 的 WB 同在 T5 完成——ID 从 T3 推迟到 T5,停顿 2 个周期;若不支持,ID 只能等到 T6,停顿 3 个周期。两种假设都成立,答案却差一拍。

(二)转发消除停顿。 同一对指令,把 I1 的 EX 结果直接送进 I2 的 EX 输入端:

指令 / 周期T1T2T3T4T5
I1: ADD R1,R2,R3IFIDEXMEMWB
I2: SUB R4,R1,R5IFIDEXMEM
转发└──→I1 的 EX 结果送入 I2 的 EX 输入

一个气泡都不用插。

(三)load-use:转发救不了的那一种。 I1: lw R1,0(R2) 的 R1 在 MEM 段(T4)末尾才拿到,而 I2: ADD R3,R1,R4 的 EX 段在 T4 开头就要用——晚了整整一拍:

指令 / 周期T1T2T3T4T5T6T7
I1: lw R1,0(R2)IFIDEXMEMWB
I2: ADD R3,R1,R4IFIDEXMEMWB
转发└──→MEM/WB 送入 EX 输入

先停 1 拍把时间差抹平,再转发。

三、控制冒险与分支预测

分支指令到来时,流水线不知道下一条该从哪里取,已经取入的后续指令可能全是错的。静态预测的结果与运行时历史无关:

策略适合
总是预测不发生前向分支(if 的条件跳转,通常不跳)
总是预测发生后向分支(循环回边,通常跳)
按跳转方向预测向后预测发生、向前预测不发生,综合上面两条

第三条之所以有效,是因为循环的回边一定是向后跳的,而循环体通常要执行很多次——零成本却相当准。动态预测依据运行时的历史记录:1 位预测器记录上次是否发生、本次按上次结果预测;2 位预测器(饱和计数器)四个状态,需要连续错两次才改变预测方向。

图 6.23 两位预测位的状态转换
图 6.23 两位预测位的状态转换
(袁春风《计算机组成与系统结构》第 3 版,见文末教材出处)

四个状态排成一条线:强发生(11) ↔ 弱发生(10) ↔ 弱不发生(01) ↔ 强不发生(00)。预测正确就往本方向的"强"端走一格(到头不动),预测错误就往反方向退一格

🔴 2 位预测器更准的全部原因就是"连错两次才翻转方向",它对偶发的例外有容忍度。看循环退出这个典型场景:1 位预测器会立刻翻成"不发生",下一轮循环第一次又错,一个循环错两次;2 位预测器只从强发生退到弱发生、方向没变,一个循环只错一次

控制冒险的代价有个简单算法:

🔴 代价 = 判定所在段的编号 1 标准五段在 EX 段判定 此时已经取入 2 条错误指令 代价 2 个周期;若硬件把比较器与地址计算提前到 ID 段,代价就降为 1 个周期。优化方向永远是让分支结果尽早出来

🔴 插入气泡(stall)与清除(flush)方向相反,别混: stall 用于数据冒险,让后面的指令停在原地等,那些指令之后还会正常执行;flush 用于控制冒险预测错误,把已经进入流水线的错误指令作废,它们永远不会执行

⚠️ 顺带记一条已被淘汰的方案:延迟分支。延迟槽内的指令不管跳不跳都要执行,由编译器负责填。它有两个致命问题——① 延迟槽的数量被写进了指令集,流水线一加深就需要更多槽,而旧程序只填了一个;② 编译器常常找不到指令可填,只能填 NOP。这属于"用软件迁就硬件",现代处理器一律改用分支预测。

控制冒险与延迟分支的逐拍时空图:那 2 个周期具体丢在哪两条指令上(想弄清 flush 掉的是谁、延迟槽插在什么位置时展开)

(一)预测失败的 flush。 分支在 EX 段才确定,此时流水线已按顺序取了 I2、I3:

指令 / 周期T1T2T3T4T5
BEQ R1,R2,targetIFIDEX(判定)MEMWB
顺序指令 I2IFIDflush
顺序指令 I3IFflush
目标地址指令 TIFID

若实际发生跳转,I2、I3 必须清除,代价 2 个周期。把判定提前到 ID 段,被取入的错误指令就只剩 1 条。

(二)延迟分支。 延迟槽里的指令跳不跳都执行:

指令 / 周期T1T2T3T4T5说明
BEQ R1,R2,targetIFIDEXMEMWB分支本身
延迟槽指令 DIFIDEXMEM跳不跳都执行
目标或顺序后继IFID从这里开始取正确路径

延迟槽不是"把分支延后执行",而是体系结构规定分支后一条指令一定执行。编译器负责往里塞一条无副作用且有用的指令,塞不进去才填 NOP。

两道 CPI 演算:把冒险的损失折成 CPI 增量(想核对"频率 × 未命中率 × 代价"这一乘法结构时展开)

(一) 某程序中分支指令占 20%,采用 2 位预测器,预测准确率 90%,预测错误代价 2 个周期。

额外 CPI=0.20×(10.90)×2=0.04

理想 CPI =1 时实际 CPI=1.04。可见分支带来的损失同时取决于三个量——分支频率、预测准确率、错误代价。任一项改善都能降低损失;流水线越深,第三项越大,预测准确率的价值也就越高。

(二) 某段程序中 30% 的指令是 load,其中一半紧跟着使用其结果的指令;分支指令占 15%,预测准确率 85%,错误代价 2 周期。

load-use 停顿=0.30×0.5×1=0.15,分支停顿=0.15×(10.85)×2=0.045CPI=1+0.15+0.045=1.195

两类冒险的贡献可以直接相加,因为它们发生在不同的指令上。

考点速记

  1. 三类冒险对应三个条件——部件空着、数据备好、该被执行,是穷举而非列举;结构冒险靠哈佛结构与寄存器堆"前半写后半读"在设计阶段就消除了,而后一项同时决定数据冒险停 2 拍还是 3 拍。
  2. 按序流水线中只有 RAW 是真问题;WAR、WAW 是名相关、只在乱序时出现、可用寄存器重命名消除。转发的判据是数据产生的时刻是否早于被需要的时刻,据此 load-use 是唯一转发也救不了的数据冒险,必须停 1 拍。
  3. 控制冒险代价 = 判定所在段编号 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

相关知识

指令流水线的基本概念与实现流水线性能分析数据冒险模拟器

真题练习