﻿# 流水线与超标量 —— CPU 提吞吐的两个正交手段：纵向重叠 vs 横向加宽

> 承微架构总纲 [cpu-microarch-overview.md](/concepts/microarch/cpu-microarch-overview.md)。很多人把"流水线""超标量""乱序"混着说，其实它们是**层层叠加、各自解决不同瓶颈**的手段。本篇讲最基础的两层——**流水线(pipelining)** 和 **超标量(superscalar)**：
> **流水线是"纵向"的——把一条指令切成多级，让不同指令的不同阶段在时间上重叠；超标量是"横向"的——把每一级都加宽成多份硬件，一拍处理多条指令。** 两者正交、可叠加，是现代 CPU"一拍退休好几条指令(IPC>1)"的底座。乱序执行、分支预测都是建在这个底座上的（见 [cpu-out-of-order.md](/concepts/microarch/cpu-out-of-order.md) / [branch-prediction.md](/concepts/microarch/branch-prediction.md)）。

## 一、基线：标量、非流水线（一次一条，走完再来）

最朴素的 CPU：一条指令从头走到尾（取指→译码→执行→访存→写回），**全部做完，才开始下一条**。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<s>> #FFCDD2
  BorderColor<<s>>     #C62828
}
rectangle "指令1: 取指→译码→执行→访存→写回" <<s>> as I1
rectangle "指令2: 取指→译码→执行→访存→写回" <<s>> as I2
rectangle "指令3: ..." <<s>> as I3
I1 -down-> I2
I2 -down-> I3
note right of I1 : 每条指令占用整条数据通路\n5 级各 1 拍 → 一条指令 5 拍\n**每 5 拍才完成 1 条**
@enduml
```

- 假设 5 级、每级 1 拍：一条指令 5 拍，吞吐 = **每 5 拍 1 条**（IPC = 0.2）。
- **浪费在哪**：执行"执行"级时，取指单元、写回单元全空闲——**每个部件 5 拍里只忙 1 拍**。这就是流水线要榨取的浪费。

## 二、流水线：把阶段重叠起来（洗衣店模型）

**流水线的核心洞察**：一条指令的五个阶段用的是**五个不同的硬件部件**（取指单元、译码器、ALU、访存单元、寄存器写口）。既然它们是分开的，就可以**让指令1 在做"执行"时，指令2 同时做"译码"、指令3 同时"取指"**——像洗衣店：第一批衣服在烘干时，第二批就能开始洗。

```plantuml
@startuml
skinparam shadowing false
title 流水线:不同指令的不同阶段在同一拍并行(每列是一拍)
skinparam rectangle {
  BackgroundColor<<f>> #E3F2FD
  BorderColor<<f>> #1976D2
}
rectangle "拍1\n I1:取指" <<f>> as C1
rectangle "拍2\n I1:译码\n I2:取指" <<f>> as C2
rectangle "拍3\n I1:执行\n I2:译码\n I3:取指" <<f>> as C3
rectangle "拍4\n I1:访存\n I2:执行\n I3:译码\n I4:取指" <<f>> as C4
rectangle "拍5\n I1:写回\n I2:访存\n I3:执行\n I4:译码\n I5:取指" <<f>> as C5
rectangle "拍6\n I2:写回\n I3:访存\n...\n**此后每拍退休 1 条**" <<f>> as C6
C1 -right-> C2
C2 -right-> C3
C3 -right-> C4
C4 -right-> C5
C5 -right-> C6
@enduml
```

**关键区分——流水线改的是吞吐，不是延迟**：

| 指标 | 非流水线 | 5 级流水线 |
|------|:---:|:---:|
| **单条指令延迟**(latency) | 5 拍 | **还是 5 拍**（一条指令仍要走完 5 级）|
| **吞吐**(throughput) | 每 5 拍 1 条 | **满载后每拍 1 条**（IPC≈1）|

- **流水线不让单条指令更快**（延迟不变，甚至因加了级间寄存器略增），它让**部件不空闲**——填满后每拍都有一条指令退休，吞吐提升接近**级数倍**。
- **更深的流水线（超流水线 superpipelining）**：把 5 级切成 15~20 级，每级更短→时钟频率能拉更高。代价：**分支误预测惩罚 = 流水线深度**（见 [branch-prediction.md](/concepts/microarch/branch-prediction.md)），且冒险更多。

### 2.1 六级流水线部件

一条经典流水线由以下 6 级组成，指令在它们之间逐级流动：

- **取指(IF)** — PC→I-Cache 取机器码，送入 IF/ID 级间寄存器
  - I-Cache miss 时需等几十拍从 L2/L3 拿
  - 遇到分支指令时下一拍取指地址不确定（控制冒险）→ 分支预测（[branch-prediction.md](/concepts/microarch/branch-prediction.md)）
- **译码(ID)** — 识别指令边界，查操作码 ROM 得出操作类型与操作数，读寄存器文件取值，锁存到 ID/IS 级间寄存器
  - x86 指令变长（1~15 字节），下一条起点依赖本条译码结果，形成反馈环路 → 现代 x86 用预译码(pre-decode)提前标记长度
- **发射(IS)** — 检查操作数就绪后，在保留站(reservation station)中分配槽位写入译码结果，等待送入执行单元
  - 操作数正被前面未完成指令占用（RAW 冒险）时发射必须等待 → 乱序执行（[cpu-out-of-order.md](/concepts/microarch/cpu-out-of-order.md)）让后面无依赖指令绕过去
  - 保留站的完整剖析见 **[reservation-station.md](/concepts/microarch/reservation-station.md)** —— 解耦译码与执行、tag 匹配监控操作数、乱序发射的发生地
- **执行(EX)** — ALU 做整数运算，FPU 做浮点/SIMD 运算，AGU（Address Generation Unit，地址生成单元）计算访存地址；结果送入 EX/MEM 级间寄存器。AGU 的完整剖析见 [lsu.md §三.3.0](/concepts/microarch/lsu.md)
  - 执行延迟差异巨大：整数加法 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.md](/concepts/microarch/register-renaming.md)）通过映射物理寄存器消除

### 2.2 作业时序：6 条指令流过 6 级流水线

下面用序列图走一遍 6 条指令流过这条流水线的完整过程。其中 I3 是一条 load 指令，会经过 MEM 访存阶段：

```plantuml
@startuml
skinparam shadowing false
title 6 级流水线作业过程：I3 为 load，走完整访存路径
participant "取指\nIF" as IF
participant "译码\nID" as ID
participant "发射\nIS" as IS
participant "执行\nEX" as EX
participant "访存\nMEM" as MEM
participant "写回\nWB" as WB
== 拍 1（填充开始） ==
IF -> ID: I1
note over IF #E3F2FD: PC→I-Cache\n取出 I1 机器码
== 拍 2 ==
IF -> ID: I2
ID -> IS: I1→ADD
note over ID #E3F2FD: I1 译码\n识别 ADD + 读 eax,ebx
== 拍 3 ==
IF -> ID: I3
ID -> IS: I2→ADD
IS -> EX: I1
note over IS #E3F2FD: I1 操作数就绪\n直接发射到 EX
== 拍 4 ==
IF -> ID: I4
ID -> IS: I3→LOAD [rsi+8]
IS -> EX: I2
EX -> MEM: I1(透传)
note over EX #FFF9C4: I1 ALU 算 eax+ebx\n结果送 MEM 级\n注：纯运算，MEM 只透传
== 拍 5（首条写回） ==
IF -> ID: I5
ID -> IS: I4→FMUL
IS -> EX: I3
EX -> MEM: I2(透传)
MEM -> WB: I1 写回
note over WB #C8E6C9: ⬆ I1 退休\nADD 结果写入 eax
== 拍 6（满载） ==
IF -> ID: I6
ID -> IS: I5→FADD
IS -> EX: I4
EX -> MEM: I3 AGU→LSU
MEM -> WB: I2 写回
note over IF,WB #C8E6C9: ★ 满载：6 条指令占据全部 6 级\nI6(IF) I5(ID) I4(IS) I3(EX) I2(MEM) I1(WB)
note over EX #FFE0B2: I3 AGU 算 rsi+8\n准备好访存地址
== 拍 7 ==
ID -> IS: I6→ADD
IS -> EX: I5
EX -> MEM: I4(透传)
MEM -> WB: I3 写回
note over MEM #FFE0B2: I3 LSU 读 D-Cache[rsi+8]\n数据命中，1 拍返回
note over WB #C8E6C9: I2 退休\nI3 load 结果写回
== 拍 8 ==
IS -> EX: I6
EX -> MEM: I5(透传)
MEM -> WB: I4 写回
note over WB #C8E6C9: I3 退休
== 拍 9（排空中） ==
EX -> MEM: I6(透传)
MEM -> WB: I5 写回
note over IF,ID #ECEFF1: IF/ID 已空闲
== 拍 10（排空结束） ==
MEM -> WB: I6 写回
note over WB #C8E6C9: I6 退休\n全部 6 条指令执行完毕
@enduml
```

> 这就是流水线的"工厂流水线"效应——满载时 **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 级

```bash
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 拍走完全程——流水线不减延迟、只提吞吐。

## 三、超标量：把流水线"加宽"，一拍处理多条

流水线满载 IPC≈1（每拍 1 条），到顶了。想突破 1，只有一条路：**每一级都放多份硬件**，一拍能取多条、译多条、执行多条。这就是**超标量(superscalar)**。

### 3.1 总体布局：前面共享 → 中间分流 → 后面汇合

现实超标量 CPU 遵循三段式结构：

- **前面共享**：取指和译码不分指令类型，统一前端每拍取出 4~6 条 x86 指令并行译码为 µop。因为取指时还不知道操作码，无法提前分家。
- **中间分流**：译码完成后按操作码分派——整数/分支 µop 进 INT 集群，浮点/SIMD µop 进 FP 集群。每个集群内部也不是单条管线，而是**多套同构执行单元**（INT 内 ALU×4 + 分支×1，FP 内 FMA×2 + SIMD ALU×1），调度器按操作数就绪情况乱序发射到空闲单元。
- **后面汇合**：两个集群的执行结果全部汇入共享的 LSU（处理 load/store）和 ROB（统一按程序顺序退休）。

```plantuml
@startuml
skinparam shadowing false
skinparam backgroundColor transparent
skinparam rectangle {
  BackgroundColor<<front>> #BBDEFB
  BorderColor<<front>>     #1565C0
  BackgroundColor<<int>>   #C8E6C9
  BorderColor<<int>>       #2E7D32
  BackgroundColor<<fp>>    #FFE0B2
  BorderColor<<fp>>        #E65100
  BackgroundColor<<share>> #E1BEE7
  BorderColor<<share>>     #7B1FA2
}
rectangle "取指\n每拍 4~6 条" <<front>> as FETCH
rectangle "译码 → µop\n4~6 译码器" <<front>> as DECODE
rectangle "INT 调度器\n保留站" <<int>> as INT_SCHED
rectangle "ALU×4" <<int>> as INT1_ALU
rectangle "分支" <<int>> as INT1_BR
rectangle "ALU×4" <<int>> as INT2_ALU
rectangle "分支" <<int>> as INT2_BR
rectangle "FP 调度器\n保留站" <<fp>> as FP_SCHED
rectangle "FMA×2" <<fp>> as FP1_FMA
rectangle "SIMD ALU" <<fp>> as FP1_SIMD
rectangle "FMA×2" <<fp>> as FP2_FMA
rectangle "SIMD ALU" <<fp>> as FP2_SIMD
rectangle "LSU\nAGU×3~4" <<share>> as LSU
rectangle "ROB\n统一退休" <<share>> as ROB
FETCH -down-> DECODE
DECODE -down-> INT_SCHED : 整数/分支 µop
DECODE -down-> FP_SCHED : 浮点/SIMD µop
INT_SCHED -down-> INT1_ALU : 乱序发射
INT_SCHED -down-> INT1_BR
INT_SCHED -down-> INT2_ALU
INT_SCHED -down-> INT2_BR
FP_SCHED -down-> FP1_FMA : 乱序发射
FP_SCHED -down-> FP1_SIMD
FP_SCHED -down-> FP2_FMA
FP_SCHED -down-> FP2_SIMD
INT1_ALU -down-> LSU
INT2_ALU -down-> LSU
INT1_BR -down-> ROB
INT2_BR -down-> ROB
FP1_FMA -down-> LSU
FP2_FMA -down-> LSU
FP1_SIMD -down-> LSU
FP2_SIMD -down-> LSU
LSU -down-> ROB
@enduml
```

### 3.2 设计原理

**为什么前端共享**——取指和译码不分指令类型。指令从内存取出来到译码完成之前，CPU 并不知道它是什么类型——I-Cache 里存的只是原始机器码字节流，直到译码阶段查出操作码，才知道这条是 ADD 还是 FMUL。如果在取指阶段就按类型分家，需要两套独立的 I-Cache、预取器、分支预测器，面积翻倍且带宽利用率下降。因此所有现代超标量 CPU 都采用**统一前端**，译码完成、操作码已知后才分流。

**为什么分流后端**——不同指令用不同硬件，消除无效等待：

- **消除类型冲突**：单流水线中 I3(FMUL) 和 I2(ADD) 虽然用不同硬件（FPU vs ALU），却只能排队过同一个 EX。分流后整数和浮点各有独立发射口和执行单元，互不阻塞。
- **匹配延迟差异**：整数加法 1 拍，浮点 FMA 4~5 拍。混在一起时短延迟被长延迟堵死；分开后 INT 集群快速周转，FP 集群自行处理长延迟运算。
- **物理设计约束**：整数 ALU 和浮点 FMA 的晶体管规模差一个数量级，延迟路径完全不同，硬塞进同一级会让时序收敛极其困难。分集群本质上是让快路径不受慢路径拖累，同时让物理布局更规整。

### 3.3 功能单元阵列与乱序发射

从上图可以看到，每个集群内部是**功能单元阵列**而非单条管线：

- **INT 集群**：调度器下挂 4 个独立执行单元——2 个 `ALU×4` + 2 个 `分支`。共 8 个 ALU + 2 个分支单元共享同一保留站，调度器按操作数就绪情况任意指派到空闲单元。
- **FP 集群**：同样的异构阵列——2 个 `FMA×2`（融合乘加） + 2 个 `SIMD ALU`，分别处理浮点/SIMD 乘加和 SIMD 整数/位运算。

保留站的工作方式：µop 进入保留站后，**操作数就绪那一刻立即发射**到空闲执行单元，不按程序顺序（乱序）。只要保留站里同时有两条操作数就绪且无依赖的 µop，调度器就能同一拍发射两条，IPC 翻倍。INT 集群同拍最多发射 4 条（2 ALU + 2 分支），FP 集群同拍最多发射 4 条（2 FMA + 2 SIMD），理想峰值每拍 8 条 µop。详见 **[reservation-station.md](/concepts/microarch/reservation-station.md)**。

### 3.4 共享后端：LSU 与 ROB

从图中可以清晰看到，后端汇入有两种路径：

- **ALU/FMA/SIMD → LSU → ROB**：运算类指令的结果先过 LSU（处理其中的 load/store），再由 LSU 送入 ROB。图中所有 ALU、FMA、SIMD ALU 全部走这条路径。
- **分支 → ROB 直连**：分支执行单元的结果直接进 ROB，跳过 LSU——因为分支不产生数据也不访存，只需告诉 ROB"预测对了/错了"。

**LSU（Load/Store Unit）**

LSU 是 CPU 与内存子系统之间的唯一通道。完整的 AGU、Store Buffer、Load Buffer、store→load forwarding、memory disambiguation 分析见 **[lsu.md](/concepts/microarch/lsu.md)**。核心约束：AGU 数量（3~4 个）决定了密集访存代码的 IPC 上限。

**ROB（Reorder Buffer）**

ROB 是"乱序执行、顺序退休"的物理基础——所有 µop 结果汇入 ROB，按程序序批量提交到架构状态。完整的保序机制、多端口并行退休、对头阻塞、精确异常、分支机构恢复分析见 **[rob.md](/concepts/microarch/rob.md)**。核心约束：ROB 大小决定指令窗口（现代 CPU 200~500 条目）；对头阻塞（head 处慢指令堵住退休）是真正瓶颈。

> 上面是概念示意——真正的核里 L1 D-Cache 和 LSU 紧密耦合，EX 和调度器之间还有旁路网络。下面这张补齐这些细节：

```plantuml
@startuml
skinparam shadowing false
title 超标量核简化模型：前端统一取译，后端双流水线分流
skinparam rectangle {
  BackgroundColor<<fe>>  #BBDEFB
  BorderColor<<fe>>      #1565C0
  BackgroundColor<<int>> #C8E6C9
  BorderColor<<int>>     #2E7D32
  BackgroundColor<<fp>>  #FFE0B2
  BorderColor<<fp>>      #E65100
  BackgroundColor<<share>> #E1BEE7
  BorderColor<<share>>   #7B1FA2
}
rectangle "══════════ 前 端 ══════════" <<fe>> as FE_LABEL
rectangle "取指 Fetch\n──────\n每拍 4~6 条 x86 指令" <<fe>> as FETCH
rectangle "译码 Decode\n──────\n4~6 个译码器并行\n输出 µop（微操作）" <<fe>> as DECODE
FETCH -down-> DECODE
rectangle "══════════ 后 端 ══════════" <<share>> as BE_LABEL
rectangle "INT 调度器\n──────\n保留站 等待操作数就绪\n谁就绪谁发射（乱序）" <<int>> as INT_SCHED
rectangle "INT 执行引擎\n──────\nALU × 4  分支单元 × 1\n简单运算 1拍" <<int>> as INT_EX
rectangle "FP 调度器\n──────\n保留站 等待操作数就绪\n谁就绪谁发射（乱序）" <<fp>> as FP_SCHED
rectangle "FP 执行引擎\n──────\nFMA × 2  SIMD ALU × 1\nFMA 4~5拍  除法更长" <<fp>> as FP_EX
rectangle "加载/存储单元 LSU\n──────\nAGU × 3~4  生成访存地址\nStore Buffer 存储缓冲区" <<share>> as LSU
rectangle "L1 D-Cache\n32~48KB" <<share>> as L1D
rectangle "统一退休 ROB\n──────\n按程序序提交结果\n每拍退休 4~8 条 µop" <<share>> as RET
DECODE -down-> INT_SCHED : 整数/分支 µop
DECODE -down-> FP_SCHED  : 浮点/SIMD µop
INT_SCHED -down-> INT_EX
FP_SCHED  -down-> FP_EX
INT_EX -down-> RET : 纯计算直接退休
FP_EX  -down-> RET : 纯计算直接退休
INT_EX -down-> LSU : load/store → AGU
FP_EX  -down-> LSU : load/store → AGU
LSU -down-> L1D : 地址翻译后访问
LSU -down-> RET : store完成后退休
@enduml
```

| 部件 | 做了什么 | 为什么重要 |
|------|---------|-----------|
| **前端 取指→译码** | 把 x86 复杂指令解码成 µop，每拍 4~6 条 | 前端是"水源"——它多快，后端才有可能多快。µop Cache 缓存已解码 µop，跳过重复解码 |
| **INT/FP 调度器** | 各带保留站，操作数就绪即发射，不管程序顺序 | 顺序发射是超标量的死穴——紧邻指令常有依赖，宽端口喂不饱。乱序让后面独立指令先走 |
| **INT/FP 执行引擎** | 多个同类型执行单元并行：INT 侧 4 个 ALU+分支，FP 侧 2 个 FMA+SIMD ALU | IPC>1 的根本原因：同一拍多个执行单元同时干活。FMA 延迟长但可流水化，每拍新发一条 |
| **LSU + Store Buffer** | 两条流水线的 load/store 在此排队，地址翻译后访问 L1 D-Cache | **瓶颈点**——不管多少 ALU/FMA，L1 D-Cache 每拍只做 2~3 次 load + 1~2 次 store |
| **ROB 统一退休** | 所有 µop 按程序原顺序提交结果 | 乱序执行的"幻象机"——程序看到的世界永远是顺序的，异常能精确定位 |

两条流水线的分道与汇合：**译码时按操作码分道**（`add eax`→INT 调度器，`addsd`→FP 调度器），此后各自乱序执行，只在两个地方碰头——**LSU（都得访存）和 ROB（都得按序提交）**。

### 3.5 IPC > 1 的完整机制：哪里加宽了、各自有什么限制

现在把前文的信息串起来，回答两个互为镜像的问题：**IPC 为什么能 > 1？反过来，为什么真实 IPC 又远达不到理论峰值（4~6）？**

答案藏在超标量架构的**五个加宽点**上——从取指到退休，每一级都被横向加宽，但每一级也都有各自的限制条件。下面从数据流的角度逐一拆解。

#### 五级加宽全览

```bash
加宽点          并行度                     限制条件
────────────────────────────────────────────────────────────────────────────
① 取指          每拍 4~6 条 x86 指令       I-Cache miss、分支跳转打断连续取指、
                                            x86 变长指令预译码瓶颈
────────────────────────────────────────────────────────────────────────────
② 译码          4~6 个译码器并行            复杂指令需多 µop（如 rep movsb 几十条）
                                            一条复杂指令可能占满所有译码槽
────────────────────────────────────────────────────────────────────────────
③ 发射          保留站每拍可发射             操作数依赖（RAW 冒险）——源寄存器
                INT 4 条 + FP 4 条          被前面未完成指令占用则无法发射
                                            端口冲突——多条 µop 争同一执行单元
────────────────────────────────────────────────────────────────────────────
④ 执行          INT：8 ALU + 2 分支         端口专业化——整数加法只走 ALU 端口
                FP：4 FMA + 4 SIMD ALU      长延迟指令（除法 20+ 拍）占着端口
                共 18 个执行单元             访存只能走 LSU 的 AGU（×3~4）
────────────────────────────────────────────────────────────────────────────
⑤ 退休          ROB 每拍 4~8 条 µop         对头阻塞——头部未完成则后面全堵
                （与发射宽度对称匹配）        ROB 满（200~500 条目）则前端停发射
────────────────────────────────────────────────────────────────────────────
```

#### 各加宽点的详细限制

**① 取指宽度（4~6 条/拍）——"水源"瓶颈**

现代 x86 前端每拍最多从 I-Cache 取出 16~32 字节（取决于微架构），对应 4~6 条普通指令。限制来自三个方面：

- **I-Cache miss**：取指地址不在 L1 I-Cache 中，需等 L2（~12 拍）或 L3（~40 拍），前端完全停摆。这是大代码段、虚函数-heavy 程序的常见瓶颈，perf 中表现为 `stalled-cycles-frontend` 高。
- **分支打断连续取指**：遇到 taken branch 时，下一条指令地址不在当前 cache line 的连续位置，取指需要从新地址开始。即使分支预测正确，也有 1~2 拍的取指气泡。未预测到的间接跳转（如虚函数调用 `call *%rax`）会让取指完全中断。
- **x86 变长指令**：指令长度 1~15 字节不等，译码器必须先做预译码(pre-decode)标记每条指令的边界。如果指令流中有大量长指令（如带 REX 前缀 + ModRM + SIB + displacement），实际取指带宽可能只有 2~3 条/拍。

**② 译码宽度（4~6 条/拍）——"复杂指令"陷阱**

x86 的译码器数量就是前端吞吐的天花板。Intel 从 Core 2 开始用 4 个译码器（3 简单 + 1 复杂），AMD Zen 也是 4 个。限制：

- **简单指令 1:1 译码**：`add %rax, %rbx` → 1 个 µop，占 1 个译码槽。
- **复杂指令 1:N 译码**：`rep movsb` 可能生成几十个 µop，复杂译码器独占期间简单译码器也得等。`div`、`idiv`、`cpuid` 等也是 µop 炸弹。
- **µop Cache 缓解**：现代 CPU 用 µop Cache（Intel 的 DSB，AMD 的 OC）缓存已译码的 µop 序列，命中时完全跳过译码阶段——此时取指带宽可以 > 4 µop/拍。

**③ 发射宽度（INT 4 + FP 4 = 8 µop/拍）——依赖链是头号杀手**

这是超标量实际 IPC 的**最常见瓶颈**。保留站里可能有几十条 µop 在等待，但每拍能发射到执行单元的只有有限条，受两条规则约束：

- **操作数就绪（RAW 依赖）**：µop 的源操作数如果不是"就绪"状态（被前面未完成指令的结果占用），就不能发射。一个三指令依赖链 `A→B→C→D` 中，A 发射后 B 等 1 拍才能发射，C 等 B、D 等 C……超标量再宽也只能一拍一条。
- **执行端口冲突**：两条 µop 操作数都就绪，但如果它们都需要同一个执行端口（比如都是整数 ALU 运算），而该端口只有 4 个槽位，第 5 条就只能等下拍。编译器指令调度（`-O2` 以上）会尽量交错不同类型指令来缓解端口冲突。

> **这就是为什么 IPC 从 0.61（`-O0`）升到 1.68（`-O2`）后仍然远低于理论 4~6 的原因**——不是执行单元不够（8 个 ALU 闲置 7 个），而是**保留站里能发射的 µop 太少**（依赖链捆住了绝大多数候选指令）。

**④ 执行宽度（18 个执行单元）——跑不满的真正原因**

执行单元数量远超前端带宽，理论上足够支撑高 IPC。但实际中执行单元利用率极低，原因有三：

- **端口专业化是双刃剑**：Add 只走 INT ALU，FMA 只走 FP 端口，load 只走 AGU。一段连续整数加法的代码把 8 个 ALU 填得满满当当，4 个 FMA 和 4 个 SIMD ALU 完全空闲——这是"结构性空转"。
- **长延迟指令占着端口不出活**：`div` 执行 20+ 拍，期间该端口不能再接收新指令。虽然现代 CPU 对除法做了流水化（多周期但可重叠），吞吐仍远低于加法。
- **执行结果需要旁路网络**：运算结果要通过旁路网络(bypass network)转发给依赖指令，如果依赖链跨集群（INT 算完给 FP 用），旁路延迟多 1~2 拍，进一步拉长依赖链。

**⑤ 退休宽度（4~8 µop/拍）——对头阻塞**

这部分前文 [三.3.4](#34-共享后端lsu-与-rob) 已详细分析。退休本身很少成为瓶颈（宽度与发射对称），但**对头阻塞**会让前面所有加宽的努力白费：一条 load miss 指令堵在 ROB 头部，后面 200 条已完成的指令全部出不来——此时 IPC 跌到接近 0，但问题不在退休宽度不够，而在那条慢指令本身。

#### 三层并行抽象

将上述五级加宽浓缩为三个逻辑层：

```bash
层级        做了什么                                  该层并行度
─────────────────────────────────────────────────────────────────
执行层      8 ALU + 4 FMA + 4 SIMD ALU + 2 分支      同一拍最多 18 个执行单元同时运算
（根本）    各自独立、互不阻塞
─────────────────────────────────────────────────────────────────
发射层      保留站乱序发射，操作数就绪即分发            INT 集群同拍最多 4 条
（调度）    找无依赖 µop 填满空闲执行单元              FP 集群同拍最多 4 条
─────────────────────────────────────────────────────────────────
退休层      ROB 多端口并行提交                         每拍 4~8 条 µop 退休
（出口）    连续完成的头部条目批量退休
─────────────────────────────────────────────────────────────────
管线的瓶颈  取指/译码 4~6 条、LSU AGU 3~4 个          I-Cache miss、变长指令、复杂 µop
```

**真实 IPC 公式**：

```bash
IPC = min(
    前端带宽,                          ← 硬件上限
    保留站可发射数,                     ← ILP（依赖链长度）
    执行单元可用端口数,                 ← 端口冲突
    ROB 退休带宽,                      ← 对头阻塞
    1 / (每条指令平均访存延迟)          ← Cache miss
)
```

最小项决定最终 IPC，五个加宽点中任何一个卡住，其他四个加得再宽也没用。

**三层之间的关系**：

- **退休层不拖后腿**：退休带宽（4~8 µop/拍）与发射带宽（4+4=8 µop/拍峰值）对称匹配。只要头部的多条指令都完成了，它们就同一拍退休，没有"逐条排队"的单入口瓶颈——ROB 是宽闸门，不是单车道。
- **真正的瓶颈在发射层**：执行单元再多，如果保留站里所有 µop 都因为操作数未就绪而卡住（依赖链太长、cache miss 没回来），那执行单元就空转。这就是为什么超标量必须配乱序执行——去挖 ILP、填满空闲端口。
- **执行层是理论上限但很少真正达到**：18 个执行单元并行运算，但端口专业化让大多数代码只能用到其中一小部分。纯整数代码只能用 8 个 ALU，浮点密集型代码只能用 4 个 FMA——全单元同时满载仅在"整数+浮点+SIMD 高度混合且无依赖"的理想情况下才有可能。

**用一个时序片段来直观感受**：假设程序有 6 条无依赖的整数加法：

```bash
拍1: 前端取指 I1~I6（6 条全部进入）
拍2: 译码完成，全部发射到 INT 保留站
拍3: 保留站乱序发射——I1→ALU1, I2→ALU2, I3→ALU3, I4→ALU4（4 条同拍开始执行）
     剩余 I5、I6 等下一拍（ALU 池有 8 个，但保留站发射宽度限制为 4）
拍4: I1~I4 完成（1 拍延迟），结果写入各自 ROB 槽位
     I5→ALU5, I6→ALU6 发射
拍5: I1~I4 全部在 ROB 头部且都已完成 → 同一拍退休 4 条（IPC=4）
     I5、I6 完成，写入 ROB
拍6: I5、I6 退休（IPC=2）
```

这个例子中，IPC=4 受限于**发射宽度**（保留站每拍只能发 4 条到 INT 集群），而非执行单元（有 8 个 ALU 闲置）或退休宽度。如果这 6 条指令两两依赖（I1→I2, I3→I4, I5→I6），则 IPC 降到 2——**依赖链长度直接吃掉一半宽度**。

> 实际 CPU 不会完美到 IPC=4 持续不断——依赖链、cache miss、分支误预测随时会打断这个理想流。但架构设计的初衷就是：**在 ILP 充足的瞬间，所有五级加宽点都能支撑 IPC > 1，没有哪一级是单入口串行瓶颈。真正限制 IPC 的是软件本身的 ILP 和访存延迟，不是硬件加宽不够。**

## 四、流水线 vs 超标量 + 为什么必须乱序

流水线和超标量是**正交的两个维度**——流水线是纵向（时间重叠），超标量是横向（空间加宽），两者独立、叠加使用：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<a>> #FFF9C4
  BorderColor<<a>> #F9A825
  BackgroundColor<<b>> #C8E6C9
  BorderColor<<b>> #388E3C
}
rectangle "流水线(纵向/时间)\n把1条指令切成多级\n让**不同指令的不同阶段**\n在时间上重叠\n→ 部件不空闲,吞吐≈级数倍" <<a>> as P
rectangle "超标量(横向/空间)\n把**每一级**复制成多份\n让**同一阶段同时处理多条**\n→ 一拍取/译/发射/退休多条\nIPC 可 >1" <<b>> as S
P -right-> S : 正交,可同时用
note bottom of P : 单独用:IPC 上限≈1
note bottom of S : 叠加用:IPC 上限≈宽度
@enduml
```

| | 流水线 | 超标量 |
|---|---|---|
| 方向 | 纵向（时间维度重叠）| 横向（空间维度加宽）|
| 做法 | 一条指令切成 N 级，级间重叠 | 每级放 M 份硬件 |
| 提升 | 吞吐 ≈ ×级数（IPC 上限≈1）| IPC 上限 ≈ 宽度 M |
| 现代 CPU | 二者**都用**：既深(15~20级)又宽(4~6路) | |

但超标量给了"一拍多条"的硬件能力，能不能真喂满，取决于程序里有多少**指令级并行(ILP)**——即"此刻有多少条互不依赖、能同时跑的指令"。如果按程序顺序取指，紧挨着的几条往往有依赖：

```asm
add rax, rbx      ; ①
mov rcx, rax      ; ② 依赖①的 rax → 必须等① → 不能同拍
sub rcx, 1        ; ③ 依赖② → 又得等
```

这三条是**依赖链**，超标量再宽也只能一条条来，端口大量空闲，IPC 掉回接近 1。

**出路：乱序执行**——不按程序顺序，从**后面**捞出与①②③无关的独立指令，填进空闲端口。这就是为什么**超标量几乎必然配乱序执行**：横向加宽的硬件，需要乱序调度才喂得饱（详见 [cpu-out-of-order.md](/concepts/microarch/cpu-out-of-order.md)）。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<bad>> #FFCDD2
  BorderColor<<bad>> #C62828
  BackgroundColor<<good>> #C8E6C9
  BorderColor<<good>> #388E3C
}
rectangle "超标量 + 顺序发射\n紧邻指令常有依赖\n→ 宽端口喂不满,IPC≈1\n(白瞎了宽度)" <<bad>> as A
rectangle "超标量 + 乱序发射\n从后面捞独立指令填空\n→ 端口喂满,IPC 逼近宽度" <<good>> as B
A -right-> B : 所以超标量必须配乱序
@enduml
```

> **关键因果链**：流水线→吞吐到 IPC≈1 见顶 → 超标量加宽想突破 1 → 但顺序发射喂不满宽机器 → 必须乱序执行找 ILP → 但乱序遇到分支会卡 → 必须分支预测跨过分支继续找。**这条链就是微架构演进的主线**（见总纲 [cpu-microarch-overview.md](/concepts/microarch/cpu-microarch-overview.md)）。IPC > 1 能成立，不是因为某一个环节特别快，而是**执行层多单元并行、发射层乱序填满、退休层多端口批量提交**三层同时加宽——每一层都没有单入口串行瓶颈（详见 [三.3.5](#35-ipc--1-的完整机制三层并行叠加)）。

## 五、极易混淆的概念，一次划清

| 概念 | 是什么 | 和超标量的区别 |
|------|--------|---------------|
| **超流水线(superpipelining)** | 流水线切得**更深**（级更多、每级更短），提**频率** | 深≠宽。超流水是纵向切更细，超标量是横向加更宽 |
| **VLIW** | 超长指令字：**编译器**在编译期把多条打包成一条宽指令，硬件不做动态调度 | 超标量是**硬件运行时**动态找并行；VLIW 把这活推给编译器（如安腾、部分 DSP）|
| **SIMD**（如 AVX）| 一条指令处理**多份数据**（数据级并行 DLP）| 超标量是多条**不同**指令并行（ILP）；SIMD 是一条指令、一个操作、多路数据 |
| **多核 / SMT** | 多个核 / 一核跑多线程（线程级并行 TLP）| 超标量是**单线程内**的并行；多核是**跨线程**并行。层次完全不同 |

> 记忆锚点：**超标量=同一拍多条不同指令(ILP)；SIMD=一条指令多份数据(DLP)；多核/超线程=多个线程(TLP)；超流水=同一条指令切更多级(提频率)。** 四者可以同时存在于一颗现代 CPU 上。

## 六、观测：IPC 就是超标量+流水线成效的总分

流水线和超标量的综合成效，直接体现在 **IPC(每周期退休指令数)**（见 [../code/perf.md](/tools/code/perf.md)）：

```bash
perf stat ./app
#   看 insn per cycle 那一行,就是 IPC
#   IPC = instructions / cycles
```

判读：

- **IPC 接近核宽度(如 3~4)** = 流水线满、超标量端口喂得饱，ILP 挖得好。
- **IPC 远低于 1(如 0.3)** = 流水线频繁停顿/清空。常见元凶：cache miss（等内存，[cache-organization.md](/concepts/cache/cache-organization.md)/[tlb.md](/concepts/cache/tlb.md)）、分支误预测（清空流水线，[branch-prediction.md](/concepts/microarch/branch-prediction.md)）、长依赖链（ILP 不足）。
- **配套事件**：`stalled-cycles-frontend`（前端喂不上，多为 I-cache/分支）、`stalled-cycles-backend`（后端卡住，多为 load miss/端口争用）。

> IPC 是"CPU 内部效率"的总分；它低，说明你的代码没喂饱这台又深又宽的机器——去查是内存(cache/TLB)、分支、还是依赖链。

## 七、和本仓库其他文档的关系

- **总纲**：[cpu-microarch-overview.md](/concepts/microarch/cpu-microarch-overview.md)——CPU 变快的演进链（标量→流水线→超标量→乱序→投机），本篇是其中"流水线 + 超标量"两层的展开。
- **超标量关键部件独立分析**：
  - [reservation-station.md](/concepts/microarch/reservation-station.md)——保留站：解耦译码与执行，乱序发射的发生地
  - [rob.md](/concepts/microarch/rob.md)——ROB：顺序退休、精确异常、对头阻塞
  - [lsu.md](/concepts/microarch/lsu.md)——LSU：AGU、store buffer、store→load forwarding
  - [register-renaming.md](/concepts/microarch/register-renaming.md)——寄存器重命名：消除假依赖
- **下一层**：[cpu-out-of-order.md](/concepts/microarch/cpu-out-of-order.md)——超标量喂不饱的问题靠乱序执行解决。
- **控制冒险的解**：[branch-prediction.md](/concepts/microarch/branch-prediction.md)——深流水线遇分支的解法。
- **IPC 掉下来的常见外因**：[cache-organization.md](/concepts/cache/cache-organization.md)、[tlb.md](/concepts/cache/tlb.md)、[mesi.md](/concepts/cache/mesi.md)（伪共享）。
- **观测**：[../code/perf.md](/tools/code/perf.md)（IPC、frontend/backend 停顿）。
- **流水线引起的冒险**：[data-hazards.md](/concepts/microarch/data-hazards.md)——RAW/WAW/WAR 三类数据冒险的根源、硬件解法与 L1 miss 场景的时序走读。
- **纵切面走读**：[uop-pipeline-walkthrough.md](/concepts/microarch/uop-pipeline-walkthrough.md)——一条指令从 L1 取指到退休的完整 µop 级路径，包含各部件交互时序。

## 八、一句话总结

> **流水线和超标量是 CPU 提吞吐的两个正交手段：流水线"纵向"把一条指令切成多级、让不同指令的不同阶段在时间上重叠（部件不空闲，IPC 逼近 1，但不改单条延迟）；超标量"横向"把每一级复制成多份、一拍处理多条（IPC 突破 1）。现代大核二者兼用（又深又宽），IPC > 1 依赖三层并行叠加——执行单元阵列（物理基础）、保留站乱序发射（调度填满）、ROB 多端口批量退休（出口不卡）——每一层都被设计成与前端发射宽度对称匹配，不存在"所有结果汇入 ROB 就退化成串行"的单入口瓶颈。但横向加宽的硬件靠顺序发射喂不满（紧邻指令常有依赖），所以超标量几乎必然配乱序执行去挖 ILP、配分支预测跨过控制冒险——这条"加宽→喂不饱→乱序→遇分支→预测"的因果链就是微架构演进主线。综合成效看 IPC：低了就去查 cache/TLB miss、分支误预测、或依赖链。别把超标量和超流水（切更深）、SIMD（一指令多数据）、多核（多线程）搞混。**

