Appearance
指令流水线:结构冒险、数据冒险、控制冒险
概念
指令流水线(instruction pipeline)把一条指令的执行拆成若干段(stage),让多条指令的不同段在时间上重叠——第 1 条指令在第 2 段时,第 2 条指令已经进入第 1 段。
一句话说清它是什么:流水线不是让一条指令跑得更快,而是让很多条指令"叠着跑"。单条指令的延迟(latency)不降反升(多了流水寄存器的建立时间),但吞吐率(throughput)成倍提高。
与第 30 篇、31 篇的分工:数据通路是搬数据的"手",控制器是喊口令的"脑",流水线则是给这只手排一条装配线——它不改变单条指令要做什么,只改变"什么时候做、谁和谁重叠"。
原理
一、经典五段流水线
| 段 | 全称 | 做什么 |
|---|---|---|
| IF | Instruction Fetch | 按 PC 取指令,PC ← PC + 4 |
| ID | Instruction Decode | 译码、读寄存器堆、计算分支目标 |
| EX | Execute | ALU 运算 / 算地址 / 比较分支 |
| MEM | Memory | lw 读数据、sw 写数据;其他指令空过 |
| WB | Write Back | 结果写回寄存器堆 |
时空图(space-time diagram)——横轴是时间(拍),纵轴是段:
指令\拍 1 2 3 4 5 6 7 8 ... 103 104
I1 IF ID EX MEM WB
I2 IF ID EX MEM WB
I3 IF ID EX MEM WB
I4 IF ID EX MEM WB
I5 IF ID EX MEM WB看这张图能读出三件事:
- 填满(fill) 用掉
拍,排空(drain) 也用掉 拍——两端这两段"斜边"就是流水线的固定开销。 - 第 5 拍起,五段同时在工作,每拍流出一条指令——这就是"满速"。
- 总拍数 =
(不是 )。这张图里 、 ,总拍数 = 9。
二、性能公式(必背)
设流水线有
| 指标 | 公式 | 说明 |
|---|---|---|
| 流水线总时间 | 填满 + 满速 + 排空 | |
| 非流水线总时间 | 逐条串行 | |
| 吞吐率 | 单位时间完成几条指令 | |
| 最大吞吐率 | ||
| 加速比 | < | |
| 效率 | < 1 |
两条铁律:
永远达不到 :因为两端有填满/排空的开销,且实际还有冒险导致的停顿。 永远是"有效面积 / 总面积"——这是效率的通用定义,段长不等时也成立:
均衡流水线(各段相等)时代入
三、三类冒险(这是本章的核心)
冒险(hazard,也叫"相关")指下一条指令没法在预定的拍上执行,流水线必须停顿或绕路。
1. 结构冒险(structural hazard)——资源不够用
成因:两条指令在同一拍要用同一个硬件部件。
| 典型冲突 | 原因 | 解决 |
|---|---|---|
| IF 与 MEM 同时访存 | 只有一个存储器,第 4 条指令在取指、第 1 条在读写数据 | 指令存储器与数据存储器分开(哈佛结构);或加指令 Cache 与数据 Cache |
| WB 与 ID 同时用寄存器堆 | 一个周期要读 2 个操作数 + 写 1 个结果 | 寄存器堆做双端口读 + 单端口写,或"前半拍写、后半拍读" |
| 同时用 ALU | 分支目标计算与 EX 运算 | 加专用加法器算分支目标 |
结构冒险的本质是"资源冲突",不是数据或控制的问题——多看硬件,少看指令。
2. 数据冒险(data hazard)——数据还没算出来
成因:后面的指令要用前面指令还没写回的结果。按读写顺序分四类,408 主要考 RAW:
| 类型 | 含义 | 出现场合 |
|---|---|---|
| RAW(写后读) | 后一条要读前一条将写的值 | 顺序流水线的唯一一种,高频 |
| WAR(读后写) | 后一条要写前一条正在读的寄存器 | 只在乱序流水线出现 |
| WAW(写后写) | 两条指令写同一个寄存器 | 只在乱序 / 多周期运算出现 |
| RAR | 两条都只读 | 根本不是冒险 |
解决手段三档:
| 手段 | 做法 | 代价 |
|---|---|---|
| 转发 / 旁路(forwarding, bypassing) | 不等写回寄存器堆,直接从流水寄存器把结果送到 EX 的输入端 | 加一条比较 + 多路选择器的通路,不损失性能 |
| 停顿(stall,也叫"气泡" bubble) | 插入若干空拍,等结果可用 | 损失性能,但简单 |
| 编译调度(编译优化) | 编译器把无关指令挪到空隙里 | 需要编译器配合,可能受指令依赖限制 |
转发能救什么、救不了什么——必须分清:
add→sub用add的结果:add在 EX 段末尾(第 3 段)就算完了,而sub的 EX 段在下一拍才开头,转发即可,零停顿。lw→add用lw的结果:lw的数据要到 MEM 段末尾(第 4 段)才有,而add下一拍就要进 EX——转发也来不及,必须停顿 1 拍。这叫 load-use 冒险,是流水线题里最常见的一个停顿来源。
1 2 3 4 5 6
lw $t0,0($s1) IF ID EX MEM WB
add $t0,$t0,$s2 IF ID [停顿] EX MEM WB
↑ 这里必须停 1 拍: MEM 末才有 $t0 的新值若把顺序改成 lw → 无关指令 → add,这 1 拍就白赚回来了——这就是编译调度。
3. 控制冒险(control hazard)——还不知道下一条在哪
成因:分支指令的结果要到 EX(或 ID)段才知道,而 PC 需要在 IF 段就确定下一条指令的地址。中间这几拍取来的指令是"猜的"。
| 手段 | 做法 | 说明 |
|---|---|---|
| 停顿 | 等分支结果出来再取指 | 简单,损失最大(分支越多损失越大) |
| 静态预测 | 固定"总是转移"或"总是不转移" | 一拍不损失,但准确率取决于程序特征 |
| 动态预测 | 用 BTB(分支目标缓冲)+ 历史表(2 位饱和计数器) | 现代主流,准确率可达 90%+ |
| 延迟槽(delay slot) | MIPS 约定分支后面的那一条一定要执行,编译器负责填有用指令 | 把"浪费"变成"干活",但指令集变复杂 |
| 提前判断 | 把比较和分支目标计算挪到 ID 段(加硬件) | 停顿从 2 拍减到 1 拍 |
分支预测错了怎么办:冲刷(flush)流水线,把猜错取进来的指令全部作废,从正确地址重新取指——罚 2 拍(等分支在 EX 段出结果),这就是下面例 4 里的"预测错惩罚"。
四、超标量、超流水线、动态流水线
| 类型 | 含义 | 效果 |
|---|---|---|
| 超流水线(superpipelining) | 把段再细分,段数 | 主频提高,但填满/排空开销变大,冒险惩罚变重 |
| 超标量(superscalar) | 一个时钟周期内并发多条指令(多条流水线) | 同一拍有多条指令流出, |
| 动态流水线(乱序执行) | 硬件运行时重排指令顺序(记分牌、Tomasulo) | 需解决 WAR/WAW,要重命名寄存器 |
| 静态流水线 | 指令顺序由编译器固定 | 硬件简单 |
关键区分:超标量靠"多套硬件并行",超流水线靠"切得更细"。前者
示例
例 1:均衡五段流水线的四项指标
某流水线分 5 段,每段耗时均为 2 ns。用它执行 100 条指令,求总时间、吞吐率、加速比与效率;并与非流水线执行对比。
完整计算过程:
第一步,取段长(各段相等,
第二步,流水线总时间(
第三步,非流水线时间:
第四步,加速比:
第五步,效率(两种算法互验):
第六步,吞吐率:
| 指标 | 值 | 上限 | 达到比例 |
|---|---|---|---|
| 总时间 | 208 ns | — | — |
| 加速比 | 4.8077 | 5 | 96.2% |
| 效率 | 0.9615 | 1 | 96.2% |
| 吞吐率 | 480.8 MIPS | 500 MIPS | 96.2% |
结论:
例 2:段长不等的流水线(口径题)
某流水线 5 段,各段耗时分别为 2 ns、1.5 ns、2 ns、2 ns、1 ns。执行 100 条指令,求总时间、加速比与效率。
完整计算过程:
第一步,流水线的段长取最慢段(这是不等长流水线的第一口径):
第二步,总时间(用的还是
第三步,非流水线时间(这里要老老实实把各段加起来,不能再用
第四步,加速比:
第五步,效率(用"有效面积/总面积"这个通用定义):
结论与对照:
| 情形 | |||
|---|---|---|---|
| 均衡(各段 2 ns) | 208 ns | 4.8077 | 0.9615 |
| 不等长(2, 1.5, 2, 2, 1) | 208 ns | 4.0865 | 0.8173 |
⚠️ 三个高频错误:
- 把
取成平均段长或最小段长——必须取最慢段,因为一拍内所有段同时工作,慢段决定节拍。 - 不等长时仍用
——那会算成 ns,把加速比虚高到 4.8077。不等长的非流水线时间必须 = 。 - 用
算不等长流水线——这个化简式只对均衡流水线成立。不等长时要用"有效面积/总面积"(本例 0.8173,而误用公式会得 0.9615,差 15 个百分点)。
例 3:数据冒险——load-use 停顿与编译调度
下面这段 C 在 MIPS 上编译后,循环体内有一条 load-use 冒险。求未优化与编译优化后各需多少时钟周期(循环 1000 次)。
/* 把数组 x 的每个元素都加上 s;x 位于 $s1 指向的地址,s 在 $s2 */
for (i = 1000; i > 0; i--)
x[i] = x[i] + s;
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
MIPS 汇编(未优化):
asm
Loop: lw $t0, 0($s1) # 取 x[i]
add $t0, $t0, $s2 # 加 s <-- 冒险在这里
sw $t0, 0($s1) # 存回 x[i]
addi $s1, $s1, -4 # 下一个元素
bne $s1, $zero, Loop # 循环逐条判冒险:
| 指令对 | 关系 | 是否需要停顿 |
|---|---|---|
lw → add | add 用 lw 刚取回的值 | 需要,正好 1 拍(load-use) |
add → sw | sw 在 MEM 段用 $t0,add 在 EX 段末已产生 | 否,转发可救 |
sw → addi | 无数据依赖 | 否 |
addi → bne | addi 在 EX 段末产生 $s1,bne 下一拍在 ID 段比较 | 否,转发可救(前提:分支在 ID 段完成比较) |
计算(未优化):
编译调度后的汇编(把无关的 addi 提到 add 前面填掉那个空隙):
asm
Loop: lw $t0, 0($s1)
addi $s1, $s1, -4 # 挪上来填空隙,与 lw 无依赖
add $t0, $t0, $s2
sw $t0, 0($s1)
bne $s1, $zero, Loop计算(调度后):
对比:
结论:一条 lw 紧跟一条用它的指令,就要付 1 拍;把无关指令挪进去,这 1 拍就被赚回来了。 本例 1000 次迭代共省 1000 拍,等于把循环提速 16.7%。
注意前提:调度后
addi→bne变成"隔一条指令"的依赖,仍然需要转发;且本计算假设分支在 ID 段解决、控制冒险不额外交停顿。若分支要到 EX 段才判定,每次迭代还要再加 1~2 拍惩罚——这时必须先用分支预测,否则前面的调度收益会被吃掉。
例 4:控制冒险——分支预测策略对比
某程序共 50000 条指令,其中 20% 是分支指令,分支中 60% 实际发生转移。流水线预测错时罚 2 拍。分别求"总是预测发生""总是预测不发生""准确率 90% 的动态预测器"三种策略下的总周期数与 CPI。
完整计算过程:
第一步,分支指令条数:
第二步,逐策略算预测错次数:
| 策略 | 预测方向 | 错误率 | 预测错次数 |
|---|---|---|---|
| 总是预测转移发生 | 转移 | ||
| 总是预测不发生 | 不转移 | ||
| 两位动态预测器 | 按历史 |
第三步,总周期数 = 指令数 × 1(理想流水线 CPI=1)+ 预测错次数 × 2:
第四步,CPI:
汇总:
| 策略 | 预测错 | 总周期 | CPI | 相对最优 |
|---|---|---|---|---|
| 总是预测不发生 | 6000 | 62000 | 1.2400 | 慢 19.2% |
| 总是预测发生 | 4000 | 58000 | 1.1600 | 慢 11.5% |
| 两位动态预测器(90%) | 1000 | 52000 | 1.0400 | 1.00× |
结论:预测越准,CPI 越接近 1(
⚠️ 关键提醒:哪个静态策略好,取决于程序的转移率是否过半,不能一概而论。"静态预测总是转移"并不是普适最优;而动态预测利用了历史信息,一般优于任何单一静态策略(例中 90% vs 60%/40%)。
另一个常考口径:若把惩罚改成"分支在 ID 段判定、罚 1 拍",则上面三个总周期分别变成
、 、 ,CPI 是 1.08 / 1.12 / 1.02。惩罚拍数一变,所有数都要跟着变,答题时先看清题目给的是几拍。
例 5:Python——流水线指标全表验算
import math
def pipeline(k, dt, n):
"""均衡流水线:k 段,每段 dt ns,执行 n 条指令"""
Tp = (k + n - 1) * dt
Ts = k * n * dt
return dict(Tp=Tp, Ts=Ts, S=Ts / Tp, E=(Ts / Tp) / k,
TP_MIPS=n / Tp * 1000, TPmax=1000 / dt)
def pipeline_uneven(stages, n):
"""不等长流水线:段长列表 stages,段长取最慢段"""
k, dt = len(stages), max(stages)
Tp = (k + n - 1) * dt
Ts = n * sum(stages)
S = Ts / Tp
E = (n * sum(stages)) / (k * Tp) # 有效面积 / 总面积
return dict(k=k, dt=dt, Tp=Tp, Ts=Ts, S=S, E=E, E_eq=S / k)
print('=== 例 1: 均衡 5 段, dt=2ns, n=100 ===')
r = pipeline(5, 2.0, 100)
print('Tp = (5+100-1)*2 = %.0f ns' % r['Tp'])
print('Ts = 5*100*2 = %.0f ns' % r['Ts'])
print('S = %.4f (< k=5) E = %.4f (=100/104, <1)' % (r['S'], r['E']))
print('TP = %.2f MIPS TPmax = %.0f MIPS' % (r['TP_MIPS'], r['TPmax']))
print('\n=== 例 2: 不等长 [2,1.5,2,2,1], n=100 ===')
ru = pipeline_uneven([2.0, 1.5, 2.0, 2.0, 1.0], 100)
print('dt = max = %.1f ns, Tp = %.0f ns' % (ru['dt'], ru['Tp']))
print('Ts = 100 * %.1f = %.0f ns' % (sum([2.0, 1.5, 2.0, 2.0, 1.0]), ru['Ts']))
print('S = %.4f, E = %.4f (有效面积/总面积, 与 S/k=%.4f 一致)'
% (ru['S'], ru['E'], ru['E_eq']))
print(' 对照: 若误用 Ts=k*n*dt 会得 S=%.4f, 误用 E=n/(k+n-1) 会得 %.4f'
% (pipeline(5, 2.0, 100)['S'], pipeline(5, 2.0, 100)['E']))
print('\n=== 例 3: load-use 停顿, 1000 次迭代 ===')
print('未优化: 5 条 + 1 停顿 = 6 拍/次 -> %d 拍' % (6 * 1000))
print('已调度: 5 条 = 5 拍/次 -> %d 拍' % (5 * 1000))
print('省 %d 拍, 降幅 %.1f%%' % (1000, 1000 / 6000 * 100))
print('\n=== 例 4: 分支预测 (50000 条, 20% 分支, 60% 转移, 罚 2 拍) ===')
total, br = 50000, 50000 * 0.20
for name, acc, direction in [('总是预测不发生', 0.40, 'N'), ('总是预测发生', 0.60, 'T'),
('动态预测器(90%)', 0.90, 'D')]:
miss = br * (1 - acc)
T = total + miss * 2
print(' %-18s 预测错 %4.0f 次 -> %5.0f 周期, CPI = %.4f'
% (name, miss, T, T / total))
print(' 两静态策略谁优取决于转移率: 本例 60% 转移 -> "预测发生"更优')
print('\n=== 例 5: n 有限时 E 随 n 变化 (k=5, dt=2ns) ===')
for n in [1, 5, 10, 20, 50, 100, 1000]:
x = pipeline(5, 2.0, n)
print(' n=%5d -> S = %.4f, E = %.4f' % (n, x['S'], x['E']))
print(' n -> 无穷: S -> k = 5, E -> 1 (k 段流水线加速比上限就是 k)')
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照:例 1 得
考点
考点
1. 必背公式
- 流水线时间
; 取最慢段 - 非流水线时间:均衡
;不等长 - 加速比
( );效率 ( ) - 吞吐率
;最大吞吐率 - 均衡时化简:
, - 分支预测总周期 = 指令数 + 预测错次数 × 惩罚拍数
2. 高频陷阱
取最慢段,不是平均、不是最小。- 不等长流水线的非流水线时间 =
,用 会把加速比算虚高(例 2:4.8077 vs 4.0865)。 只对均衡流水线成立;不等长必须用"有效面积/总面积"(例 2 差 15 个百分点)。 永远 、 永远 ,因为填满/排空各 拍是纯开销。答" "必错。- 总拍数是
不是 ——这是本章第一大算错点。 - "加速比"的分母是流水线时间,分子是非流水线时间,别倒过来写
。 - 转发(forwarding)不能消除 load-use 冒险——
lw的数据在 MEM 段末才有。能靠转发零停顿的只有 ALU 结果(EX 段末产生)。这是最常考的一个判断。 - RAW 是顺序流水线唯一的冒险;WAR/WAW 只在乱序执行(动态流水线)里出现。题目说"顺序流水线"时提 WAR/WAW 是概念错误。
- 结构冒险的根因是"资源冲突",解决靠增加硬件(分开存储器、双端口寄存器堆),不是靠停顿或转发;停顿只能缓解,不能根治。
- 超标量 ≠ 超流水线:超标量是"一拍多条指令"(多套硬件),超流水线是"段切得更细"(一拍仍一条)。混为一谈要扣分。
- "总是预测转移"不是普适最优:例 4 中它优于"不转移"只是因为转移率 60% > 50%。转移率低于 50% 时结论反转。
- 分支惩罚拍数取决于分支在流水线的哪一段判定:ID 段判定罚 1 拍,EX 段判定罚 2 拍。题目不给就要按常见口径(EX 段、罚 2 拍)算并写明假设。
3. 解题模板("流水线计算题")
① 数段数 k, 定段长 dt = max(各段)
② 数指令条数 n
③ Tp = (k + n - 1) * dt
④ Ts: 均衡 = k*n*dt ; 不等长 = n * sum(各段)
⑤ S = Ts/Tp ; E = S/k (或"有效面积/总面积")
⑥ TP = n/Tp ; TPmax = 1/dt
⑦ 若有冒险: 逐条判依赖 -> 数停顿拍 -> 加到总拍数上
⑧ 若有分支: 总数 = 指令数 + 预测错次数 * 惩罚拍数 -> CPI = 总数/指令数4. 与相邻章节的接口
- 第 30 篇(数据通路):流水线的每一段就是那篇里的一批微操作;段与段之间要加流水寄存器(锁存每段的结果),这是流水线的物理代价。
- 第 31 篇(控制器):流水线需要"每拍发出的信号随段不同",所以控制器要按"当前在第几段"选信号源;流水线 CPU 几乎只能用硬布线控制器(微程序读控存的延迟会吃掉流水线的频率优势)。
- 第 33 篇(异常与中断):中断/异常会打断流水线——要把流水线里"正在飞"的指令全部处理掉(冲刷或精确断点),这引出"精确异常"这个概念。
- 第 12 篇(Cache):结构冒险的典型解药就是"指令 Cache 与数据 Cache 分开";Cache 命中率直接决定 MEM 段能否一拍完成,从而决定流水线能否满速。
- 第 01 篇(性能指标):
、 、加速比在这里被赋予"流水线"的具体含义; 时间 指令数 依然是最终落点。
小结
- 流水线 = 让多条指令的不同段在时间上重叠。它不缩短单条指令的延迟,只提高吞吐率。
- 总时间
, 取最慢段; 均衡时 、不等长时 。 、 : 拍的填满 + 拍的排空是纯开销。例 1 里 、 。- 三类冒险:结构(资源冲突,加硬件)、数据(RAW 为主,转发 / 停顿 / 编译调度)、控制(分支,预测 / 延迟槽 / 提前判定)。
- 转发救得了 ALU 结果,救不了
lw的数据——load-use 必须停 1 拍;用编译调度把这 1 拍填掉,例 3 里 1000 次迭代共省 1000 拍。 - 分支预测越准 CPI 越低:例 4 中 90% 动态预测器把 CPI 从 1.24 压到 1.04。
- 超标量是"一拍多条"(多套硬件),超流水线是"段切更细",两者不同,常结合使用。
下一篇:异常与中断机制
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。