流水线:纵向重叠,从标量基线到六级部件逐拍走读
承总纲 pipelining-superscalar。本篇把"流水线"这一层单独拎出来讲透:先立一个标量、非流水线的基线(一次一条、走完再来),再看流水线怎么把阶段重叠起来、吞吐为什么能涨而延迟纹丝不动,最后用一张序列图 + 逐拍拆解把 6 条指令流过 6 级流水线的全过程走一遍。
一、基线:标量、非流水线(一次一条,走完再来)
最朴素的 CPU:一条指令从头走到尾(取指→译码→执行→访存→写回),全部做完,才开始下一条。

- 假设 5 级、每级 1 拍:一条指令 5 拍,吞吐 = 每 5 拍 1 条(IPC = 0.2)。
- 浪费在哪:执行"执行"级时,取指单元、写回单元全空闲——每个部件 5 拍里只忙 1 拍。这就是流水线要榨取的浪费。
二、流水线:把阶段重叠起来(洗衣店模型)
流水线的核心洞察:一条指令的五个阶段用的是五个不同的硬件部件(取指单元、译码器、ALU、访存单元、寄存器写口)。既然它们是分开的,就可以让指令1 在做"执行"时,指令2 同时做"译码"、指令3 同时"取指"——像洗衣店:第一批衣服在烘干时,第二批就能开始洗。

关键区分——流水线改的是吞吐,不是延迟:
| 指标 | 非流水线 | 5 级流水线 |
|---|---|---|
| 单条指令延迟(latency) | 5 拍 | 还是 5 拍(一条指令仍要走完 5 级) |
| 吞吐(throughput) | 每 5 拍 1 条 | 满载后每拍 1 条(IPC≈1) |
- 流水线不让单条指令更快(延迟不变,甚至因加了级间寄存器略增),它让部件不空闲——填满后每拍都有一条指令退休,吞吐提升接近级数倍。
- 更深的流水线(超流水线 superpipelining):把 5 级切成 15~20 级,每级更短→时钟频率能拉更高。代价:分支误预测惩罚 = 流水线深度(见 branch-prediction),且冒险更多。
2.1 六级流水线部件
一条经典流水线由以下 6 级组成,指令在它们之间逐级流动:
- 取指(IF) — PC→I-Cache 取机器码,送入 IF/ID 级间寄存器
- I-Cache miss 时需等几十拍从 L2/L3 拿
- 遇到分支指令时下一拍取指地址不确定(控制冒险)→ 分支预测(branch-prediction)
- 译码(ID) — 识别指令边界,查操作码 ROM 得出操作类型与操作数,读寄存器文件取值,锁存到 ID/IS 级间寄存器
- x86 指令变长(1~15 字节),下一条起点依赖本条译码结果,形成反馈环路 → 现代 x86 用预译码(pre-decode)提前标记长度
- 发射(IS) — 检查操作数就绪后,在保留站(reservation station)中分配槽位写入译码结果,等待送入执行单元
- 操作数正被前面未完成指令占用(RAW 冒险)时发射必须等待 → 乱序执行(cpu-out-of-order)让后面无依赖指令绕过去
- 保留站的完整剖析见 reservation-station —— 解耦译码与执行、tag 匹配监控操作数、乱序发射的发生地
- 执行(EX) — ALU 做整数运算,FPU 做浮点/SIMD 运算,AGU(Address Generation Unit,地址生成单元)计算访存地址;结果送入 EX/MEM 级间寄存器。AGU 的完整剖析见 lsu §三.3.0
- 执行延迟差异巨大:整数加法 1 拍,浮点乘法 3~5 拍,除法 20+ 拍。只有一套执行单元时,长延迟指令堵死后面所有指令
- 访存(MEM) — load 从 D-Cache 读数据,store 写入 store buffer 后由 LSU 异步刷到 D-Cache;ALU/FPU 等纯运算指令在本级透传
- D-Cache miss 导致数十上百拍等待
- store→load forwarding:同一地址刚 store 又 load,需旁路把 store buffer 中的值直接转发给 load
- 写回(WB) — 将结果写入物理寄存器文件,释放保留站槽位,指令退休
- 同拍多条指令写同一寄存器产生 WAW/WAR 假冒险 → 寄存器重命名(register-renaming)通过映射物理寄存器消除
2.2 作业时序:6 条指令流过 6 级流水线
下面用序列图走一遍 6 条指令流过这条流水线的完整过程。其中 I3 是一条 load 指令,会经过 MEM 访存阶段:

这就是流水线的"工厂流水线"效应——满载时 6 条指令同时占据 6 个不同阶段。单条指令延迟仍是 6 拍,但吞吐做到了每拍 1 条退休(IPC≈1)。注意 I3 走 EX(AGU 算地址) → MEM(LSU 读 D-Cache) 的完整访存路径,而 I1、I2 等纯运算指令在 MEM 级只是透传。
2.3 从填充到排空:逐拍拆解
下面按填充期、满载期、排空期三个阶段逐拍拆解上面的时序图。
填充期(拍 1 ~ 拍 5)——流水线逐渐被指令填满
| 拍 | IF | ID | IS | EX | MEM | WB | 说明 |
|---|---|---|---|---|---|---|---|
| 1 | I1 | — | — | — | — | — | 只有 IF 干活,其余空闲 |
| 2 | I2 | I1 | — | — | — | — | I1 进入译码,I2 开始取指 |
| 3 | I3 | I2 | I1 | — | — | — | I1 发射到 EX,I2 在译码,I3 取指 |
| 4 | I4 | I3 | I2 | I1 | — | — | I1 进 EX 运算,4 级活跃 |
| 5 | I5 | I4 | I3 | I2 | I1(透传) | — | I3 进入 EX 开始算地址 |
填充期规律:每拍比上一拍多一级活跃,使用率逐步上升。
满载期(拍 6)——6 条指令占据全部 6 级
I6(IF) I5(ID) I4(IS) I3(EX→MEM) I2(MEM) I1(WB)| 部件 | 拍 6 在做什么 |
|---|---|
| IF | 取出 I6(ADD r8,r9)的机器码 |
| ID | 译码 I5(FADD xmm2,xmm3),识别为浮点加,读取 xmm2、xmm3 |
| IS | 检查 I4(FMUL xmm0,xmm1)操作数就绪后发射到保留站 |
| EX | I3 的 AGU 计算 rsi+8 得到访存地址,I4 在保留站等 EX 空闲 |
| MEM | I2(ADD ecx,edx)纯运算透传,把结果按拍传递给 WB |
| WB | I1 退休:ADD eax,ebx 的结果写入 eax 物理寄存器 |
此时每拍一条新指令进入 IF,同时一条老指令从 WB 退休(IPC=1)。注意 I3(AGU) 和 I2(ALU) 虽然共享同一套 EX 硬件,但做的事完全不同,只能串行。
排空期(拍 7 ~ 拍 10)——不再取新指令,流水线逐级清空
| 拍 | IF | ID | IS | EX | MEM | WB | 关键事件 |
|---|---|---|---|---|---|---|---|
| 7 | — | I6 | I5 | I4 | I3(访存) | I2 写回 | I3 在 MEM 读 D-Cache |
| 8 | — | — | I6 | I5 | I4(透传) | I3 写回 | I3 退休 |
| 9 | — | — | — | I6 | I5(透传) | I4 写回 | IF/ID 已空闲 |
| 10 | — | — | — | — | I6(透传) | I5 写回 | 只剩 MEM→WB |
两个观察:
- I3 load vs I2 ALU:I2 EX→MEM→WB 只用了 2 拍(透传),I3 走 EX(AGU)→MEM(LSU)→WB 也是 3 拍(D-Cache 命中前提下),延迟一致,只是 EX 阶段做的事不同。
- I6 验证延迟不变:I6 拍 6 取指,拍 10 才到 MEM,仍需 6 拍走完全程——流水线不减延迟、只提吞吐。
三、一句话总结
流水线是"纵向"提吞吐:把一条指令切成多级,让不同指令的不同阶段在同一拍重叠——满载后每拍退休 1 条(IPC≈1),但单条指令延迟不变。它榨的是"部件空闲"的浪费:标量 CPU 每个部件 5 拍只忙 1 拍,流水线让它们全转起来。代价是深流水线把分支误预测惩罚放大成"级数"(见 branch-prediction),六级部件里发射(IS)要应对 RAW 冒险(保留站)、写回(WB)要应对 WAW/WAR(寄存器重命名)——这些"级与级打架"的问题正是数据冒险(data-hazards)和后续乱序执行(cpu-out-of-order)的引子。