流水线的冒险与处理

上一节的时空图假设每拍都能顺利往前推进一格。这一节讨论推不动的时候。

推不动只有三个原因,而这三个原因正好对应流水线成立的三个条件被破坏(5.6.1):

冒险破坏了哪个条件一句话
结构冒险各段功能部件独立两条指令要用同一个部件
数据冒险——(新问题)后一条要用前一条还没算出来的值
控制冒险——(新问题)不知道下一条该取哪一条

三类冒险的处理手段可以归成一张更根本的表:每一种手段都把代价推到 的某一个因子上,或者推到硬件成本上。这就是本节的组织主线。

机制

一、结构冒险(资源冲突)

两条指令在同一拍需要同一个硬件部件。

经典例子:若只有一个存储器,第 4 拍时 I1 在 MEM 段访存、I4 在 IF 段取指——同一个存储器要被读两次。

解决手段做法代价
停顿(插气泡)让后面的指令等一拍CPI ↑
资源重复分离指令存储器和数据存储器硬件成本 ↑
资源分时一拍内前半读、后半写(寄存器堆常用)对部件速度要求 ↑

主流做法是资源重复——这就是 5.6.2 说的分离 I-Cache / D-Cache。

寄存器堆用的是资源分时:WB 段在时钟前半拍写、ID 段在后半拍读,于是同一拍里”写在前读在后”,后面的指令能读到刚写的值。这个小设计能消掉整整一拍的数据冒险,是很多题目里”距离 3 的相关不需要停顿”的原因。

二、数据冒险

三种相关,五段流水线里只有一种真的会发生

类型全称含义五段流水线里
RAW写后读后一条要读前一条写的值真相关,会发生
WAR读后写后一条要写前一条读的位置不会——顺序流水,读总在写之前
WAW写后写两条都写同一位置不会——都在 WB 段按序写

WAR 和 WAW 只在乱序执行的流水线里才会出现(5.6.5),处理手段是寄存器重命名。408 的五段流水线题目里只需要处理 RAW。

RAW 冒险的距离计算

关键事实(5.6.2):WB 段写寄存器,ID 段读寄存器,相隔 3 个段。

I1: ADD R1,R2,R3    IF  ID  EX  MEM  WB
I2: SUB R4,R1,R5        IF  ID  ...          ← 第 3 拍读 R1,但 R1 第 5 拍才写
I3:                         IF  ID  ...      ← 第 4 拍读
I4:                             IF   ID      ← 第 5 拍读
I2 与 I1 的距离I2 读寄存器的拍是否冲突要停几拍
相邻(距离 1)第 3 拍✅2 拍(无转发)
距离 2第 4 拍✅1 拍
距离 3第 5 拍看是否”先写后读”0 或 1
距离 ≥ 4第 6 拍及以后❌0

距离 3 那一行是最常考的一处:若寄存器堆采用”前半拍写、后半拍读”,则不需要停顿;若题目没说,按需停 1 拍。做题时要看题目给的假设。

数据冒险的四种处理手段

这是本节的核心表——四种手段,代价落在四个不同的地方。

手段谁做什么时候做代价落在能否完全解决
硬件阻塞(stall / 气泡)硬件运行时CPI ↑✅ 但慢
软件插入 nop编译器编译时IC ↑(代码变长)✅ 但慢
数据转发(旁路 forwarding)硬件运行时硬件成本 ↑✅ 大部分情况
指令调度(编译器重排)编译器编译时无(若能找到无关指令)❌ 找不到就不行

疑问点:为了解决数据相关可以插入 nop 指令,是编译时就插进去的吧?还有哪些操作

是的,nop 由编译器(或汇编器)在编译时插入——这一条判断完全正确。而”还有哪些”这个问题,最有价值的答法不是罗列,是看每种手段把代价推到了哪里。

四种手段,两个维度:谁来做(软件/硬件)、代价落在哪。

手段软件 / 硬件代价落在 的哪个因子
软件插 nop编译时,软件 ↑——多了几条真实的指令, 不变
硬件阻塞运行时,硬件 ↑——指令数不变,但有些拍什么都没完成
数据转发运行时,硬件都不变,硬件成本 ↑
指令调度编译时,软件理想情况都不变——用真正有用的指令填上那几拍

前两种的效果完全一样,差别只在这几拍空转记在谁头上。 插 3 条 nop, 加 3;硬件停 3 拍, 相应上升。跑得一样慢,但公式上的账记在不同的因子里——这是1.3.1 计算机的主要性能指标三因子在本章最直接的一次应用。

软件方案的真正好处是硬件可以更简单:早期 MIPS 干脆不做冒险检测电路,把保证正确性的责任完全交给编译器——编译器算不对就出错。这种设计叫”软件互锁”,名字来源就是”硬件没有互锁(interlock)“。MIPS 这个缩写的原始展开 “Microprocessor without Interlocked Pipeline Stages” 说的就是这件事。

但真正的主力是第三种:数据转发。

观察一下时空图:I1 的结果在 EX 段末尾就已经算出来了(存在 EX/MEM 段间寄存器里),只是还要等两拍才写回寄存器堆。既然值已经有了,为什么一定要绕道寄存器堆?

I1: ADD R1,R2,R3   IF  ID  EX ─┐ MEM  WB
                                │ 直接送过去
I2: SUB R4,R1,R5       IF  ID ←┘ EX  MEM  WB

在 EX/MEM 寄存器和 ALU 输入端之间加一条旁路,把结果直接送给下一条指令——这就是转发(forwarding / bypassing)。代价是若干个 MUX 和一套冒险检测逻辑,换来的是绝大多数 RAW 冒险零停顿。

转发有一个解决不了的情况,而且它一定会考:

I1: LW  R1,0(R2)   IF  ID  EX  MEM ─┐ WB
                                     │ 数据到 MEM 段末尾才出来
I2: ADD R3,R1,R4       IF  ID  EX ←──┘ 但 EX 段在前一拍就要用

LW 的数据要到 MEM 段结束才从存储器读出来,而下一条指令的 EX 段比它早一拍——时间上倒流了,转发做不到。 这叫 load-use 冒险,必须停顿 1 拍(然后再转发)。

判据一句话:转发能把”结果产生的段”提前接到”需要它的段”,但产生不能晚于需要。 ADD 在 EX 末产生、下一条 EX 初需要 → 可以;LW 在 MEM 末产生、下一条 EX 初需要 → 不行。

第四种手段——指令调度——是最优的,因为它不浪费任何东西。 编译器把附近与之无关的指令挪到冒险的两条之间:

原始:  LW  R1,0(R2)         调度后: LW  R1,0(R2)
        ADD R3,R1,R4                  SUB R7,R8,R9   ← 无关指令填进来
        SUB R7,R8,R9                  ADD R3,R1,R4

停顿变成了有用的工作。 代价是编译器要做依赖分析,而且附近未必有无关指令可挪——找不到就只能退回插 nop 或硬件停顿。4.3.3 说的编译器优化(归纳变量替换等)和这是同一类工作。

疑问点: nop 是执行一个时钟周期,还是一个完整的指令周期

两个都对——但它们回答的是两个不同的问题,而这正是5.6.1 那条”延迟 / 吞吐”区分最好的一道练习题。

问的是答案属于
一条 nop 自己从进入到离开流水线要多久?5 个时钟周期(走完全部五段)延迟
插入一条 nop 让后面的指令推迟多久?1 个时钟周期吞吐

nop 是一条真正的机器指令,有自己的操作码、要被取指、要被译码、要占段间寄存器、要走完 IF/ID/EX/MEM/WB。它不是”什么都不做”,它是”做了一遍完整的流程,但没有产生任何效果”——不写寄存器、不访存、不改标志位。

而它对程序的影响是 1 拍,因为流水线稳定后每拍完成一条指令,多插一条就整体后移一拍。这就是”插 条 nop 能填掉需要停顿 拍的冒险”的算法依据。

在非流水线的多周期 CPU 上,这个区分不存在——那里 nop 就是老老实实占用一个完整的指令周期(若干个时钟周期),既是它的延迟也是它的代价。“延迟和吞吐可以不相等”这件事,是流水线带来的。

顺带一提,4.4.1 提过 x86 有多种长度的 nop 编码(1 字节到 15 字节),它们的用途不是填流水线,是做代码对齐——让循环入口落在 16 字节边界上,取指效率更高。同一个助记符,两种完全不同的用途。

三、控制冒险

转移指令要到 EX 段(或更早优化到 ID 段)才知道转不转、转到哪。 而在那之前,IF 段已经按顺序取了后面的指令。

分支代价确定转移结果的段号

经典五段在 EX 段确定 → 浪费 2 拍;优化到 ID 段确定 → 浪费 1 拍。

四种处理手段

手段做法代价
阻塞取到分支就停,等结果每次分支都全额浪费
分支预测猜一个方向继续取,猜错再冲刷猜错才浪费
延迟槽分支后面固定几条指令无论如何都执行,由编译器填填不满就只能填 nop
提前判断把比较逻辑挪到 ID 段硬件成本 ↑,代价从 2 拍降到 1 拍

分支预测

类型依据典型
静态预测编译时定死总是不转(最简单);向后转移预测为转(循环的回边)
动态预测运行时根据历史1 位预测器、2 位预测器、分支历史表 BHT

“向后转移预测为转”这条静态规则的准确率相当高,因为循环的回边总是向后跳,而循环通常要转很多次才退出一次。

2 位预测器比 1 位好在哪——这是常考点:

1 位预测器在循环的最后一次猜错(该退出时它还猜转),而且这次错误会让它翻转,导致下一次进入循环时的第一次又猜错。一个循环错两次。

2 位预测器需要连续错两次才改变预测方向,所以循环退出那一次错了之后不立刻翻转,下次进入循环时仍然预测”转”。一个循环只错一次。

猜错的代价是冲刷(flush):把已经进入流水线的错误路径上的指令全部作废。这和精确异常用的是同一套硬件——都是”把已经在流水线里的指令变成无效”。

三类冒险的处理手段总表

这张表是本节的收口,也是最值得记的一张。

冒险手段谁做代价落在
结构停顿硬件CPI ↑
资源重复(分离 I/D Cache)硬件硬件成本 ↑
资源分时(寄存器堆前写后读)硬件部件速度要求 ↑
数据硬件阻塞硬件CPI ↑
软件插 nop编译器IC ↑
数据转发硬件硬件成本 ↑
指令调度编译器理想情况无代价
控制阻塞硬件CPI ↑
分支预测硬件(动态)/ 编译器(静态)猜错才有代价
延迟槽编译器填填不满则等于插 nop
提前判断硬件硬件成本 ↑

竖着读这张表,能看出一条清楚的规律:

每一种手段都是在” ↑ / ↑ / 硬件成本 ↑“三者之间选一个。没有任何一种手段是免费的,唯一接近免费的是指令调度——而它的前提是”附近真的有无关指令可挪”,这个前提由程序本身决定,编译器无法创造。

边界

边界辨析:转发能解决 / 不能解决

判据:结果产生的段,不能晚于需要它的段。

  • ADD → 下一条:EX 末产生,EX 初需要 → ✅ 可转发,零停顿
  • LW → 下一条:MEM 末产生,EX 初需要 → ❌ load-use 冒险,必须停 1 拍

load-use 是必考的那一个例外。

边界辨析:软件插 nop / 硬件阻塞

效果一样,账记在不同因子上:

  • 插 nop → ↑,代码真的变长了
  • 硬件阻塞 → ↑,指令条数没变

一道题问”插入 nop 会不会增加指令条数”,答会;问”硬件阻塞会不会”,答不会。

边界辨析: nop 的延迟 / nop 的代价

  • 延迟:5 拍,走完全部段
  • 对程序的代价:1 拍

这是”延迟 ≠ 吞吐”最直接的一个例子。

边界辨析:RAW / WAR / WAW

五段顺序流水线只会出现 RAW。 WAR 和 WAW 需要乱序执行才会发生,用寄存器重命名解决,见 5.6.5。

题目若在五段流水线的背景下问 WAR,答不会发生。

对照速查

距离无转发时停顿有转发时停顿
相邻(1)2 拍0(LW 后为 1 拍)
距离 21 拍0
距离 30 或 1(看寄存器堆是否前写后读)0
距离 ≥400
说法判断
数据转发能解决所有 RAW 冒险❌ load-use 仍需停 1 拍
插入 nop 增加了指令条数✅ IC ↑
硬件阻塞增加了指令条数❌ CPI ↑
五段流水线会出现 WAR 冒险❌
2 位预测器一个循环只猜错一次✅ 1 位要错两次
分支预测错误的代价是冲刷流水线✅
结构冒险可以用分离 I/D Cache 解决✅
延迟槽由硬件填充❌ 由编译器填

考点

  • 给指令序列判断冒险类型并算停顿拍数:本节最大的题型。步骤:①标出每条指令写哪个寄存器、读哪个寄存器;②找 RAW 对;③数距离;④按有无转发查表。
  • load-use 冒险:必考的例外,转发也要停 1 拍。
  • 画带停顿的时空图:气泡要画出来。
  • 四种数据冒险手段的代价归属:IC / CPI / 硬件成本。
  • 2 位预测器优于 1 位的理由:循环的最后一次与下一次的第一次。
  • 延迟槽由谁填:编译器。

链接