# CPU 乱序执行（Out-of-Order Execution）—— 核内怎么跑得快，以及它为什么不是内存重排

> 承 [reordering-overview.md](/concepts/memory-ordering/reordering-overview.md) 的"第2层"。上一篇把"重排"拆成三层并反复强调：**CPU 乱序执行 ≠ 内存重排**。本篇专讲这第2层——CPU 核内部**怎么**乱序、**为什么单核察觉不到**、以及**它和内存重排的边界到底在哪**。


> 一句话定位：**乱序执行是"单核性能引擎"，回答的是"CPU 怎么把执行端口喂满、别让一条慢指令堵死流水线"；它不是"多核顺序对不对"的话题（那是 [memory-reordering.md](/concepts/memory-ordering/memory-reordering.md)）。** 前置最好先读 [reordering-overview.md](/concepts/memory-ordering/reordering-overview.md) 建立三层框架。


> 本篇也是微架构演进链的"④乱序执行"环节（上游：[流水线→超标量](/concepts/microarch/pipelining-superscalar.md) 加宽了硬件却喂不饱，才需要乱序找并行）——整条链的总纲见 [cpu-microarch-overview.md](/concepts/microarch/cpu-microarch-overview.md)。

## 一、为什么要乱序：一条慢指令不该堵死后面所有人

顺序执行(in-order)的致命问题：**指令必须按程序序一条条来，前一条没完，后一条即使数据齐了也得干等。** 而指令延迟差异巨大（见 [cache-organization.md 第十一节](/concepts/cache/cache-organization.md) 的周期表）：

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  LifeLineBorderColor        #90A4AE
}
participant "顺序执行核" as IO
note over IO
  ① load r1, [X]   ← miss 到内存,等 ~200+ 拍
  ② add r2, r3, r4 ← 数据早就齐了…
  ③ add r5, r2, r6
end note
IO -> IO : 执行①,miss,**停摆 200+ 拍**
IO -> IO : (干等) ② 明明能算,却不许插队
IO -> IO : 200 拍后①回来,才轮到②③
note over IO : 一条 load miss\n把整条流水线堵死 😱
@enduml
```

指令② 和① 毫无数据依赖，本可以在① 等内存的 200 拍里**顺手算完**。乱序执行就是为此而生：**谁的操作数先就绪，谁就先执行，别让程序序绑死执行顺序。**

> 核心动机：**用"找出并行"来掩盖"个别指令的长延迟"**。现代超标量核有多个执行端口（多个 ALU、load/store 单元），乱序执行就是不断从后面捞出"数据齐了的独立指令"塞进空闲端口，把 IPC（每拍退休指令数）顶上去。

## 二、怎么做到乱序又不出错：五大部件

乱序执行不是"随便乱跑"，而是一套精密机制，保证**乱序地算、顺序地对**。关键五件套：

```plantuml
@startuml
skinparam shadowing false
skinparam componentStyle rectangle
skinparam rectangle {
  BackgroundColor<<f>> #E3F2FD
  BorderColor<<f>>     #1976D2
  BackgroundColor<<o>> #E1BEE7
  BorderColor<<o>>     #6A1B9A
  BackgroundColor<<e>> #FFF9C4
  BorderColor<<e>>     #F9A825
  BackgroundColor<<r>> #C8E6C9
  BorderColor<<r>>     #388E3C
}
rectangle "取指/译码\n(按程序序 in-order)" <<f>> as F
rectangle "重命名\n(消除假依赖)" <<o>> as RN
rectangle "保留站/调度器\n(等操作数就绪就发射)" <<o>> as RS
rectangle "执行端口\n(多个 ALU/LSU 并行,乱序)" <<e>> as EX
rectangle "重排序缓冲 ROB\n(按程序序退休 in-order)" <<r>> as ROB
F -right-> RN
RN -right-> RS
RS -right-> EX
EX -right-> ROB
note bottom of F : **前端:顺序进**
note bottom of RS : **中间:乱序算**
note bottom of ROB : **后端:顺序退**
@enduml
```

整条流水线是**"顺序进、乱序算、顺序退"** 的三明治结构：

| 部件 | 作用 | 关键点 |
|------|------|--------|
| **取指/译码** | 按程序序取指令、拆成微操作(µop) | 前端 **in-order** |
| **寄存器重命名** | 把架构寄存器映射到大量物理寄存器 | **消除假依赖**(WAR/WAW)，让更多指令能并行——见 §3（独立剖析 [register-renaming.md](/concepts/microarch/register-renaming.md)）|
| **保留站/调度器** | µop 在此等待，**操作数一就绪就发射**到空闲端口 | 乱序的**发生地**：谁齐了谁先走——独立剖析 [reservation-station.md](/concepts/microarch/reservation-station.md) |
| **执行端口** | 多个 ALU、乘除、load/store 单元并行执行 | 超标量：一拍能执行多条（LSU 独立剖析 [lsu.md](/concepts/microarch/lsu.md)）|
| **重排序缓冲(ROB)** | 记录所有在飞指令，**强制按程序序退休(commit)** | 乱序的**收口**：结果按程序序对外生效——见 §4（独立剖析 [rob.md](/concepts/microarch/rob.md)）|

## 三、寄存器重命名：为什么能挖出更多并行

乱序的自由度常被"假依赖"限制。看这段：

```asm
① add rax, rbx      ; 写 rax
② mov [mem], rax    ; 读 rax
③ add rax, rcx      ; 又写 rax  ← 和① 抢同一个 rax 名字(WAW),和② 有 WAR
```

③ 和① 逻辑上不相关，却因为**都叫 `rax`** 而不能乱序（这叫**名字依赖/假依赖**，不是真的数据依赖）。**寄存器重命名**把它们映射到不同物理寄存器：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<a>> #FFEBEE
  BorderColor<<a>>     #C62828
  BackgroundColor<<p>> #C8E6C9
  BorderColor<<p>>     #388E3C
}
rectangle "架构寄存器(程序看到的)\nrax 只有一个" <<a>> as A
rectangle "物理寄存器(硬件真有几百个)\n①的rax→P37\n③的rax→P52" <<p>> as P
A -right-> P : 重命名
note bottom of P : ① 和 ③ 各写各的物理寄存器\n假依赖消失 → 可以并行/乱序
@enduml
```

- **真依赖(RAW，读后写的数据流)** 无法消除，必须等——这是乱序的硬边界。
- **假依赖(WAR/WAW，只是名字冲突)** 靠重命名消除——这是乱序能挖出并行的关键。

> 现代 x86 架构寄存器只有 16 个通用寄存器，但物理寄存器有几百个。重命名让"名字不够用"不再限制并行度，是乱序执行威力的放大器。

## 四、顺序退休：单核为什么完全察觉不到乱序

这是理解"乱序执行 ≠ 内存重排"的**关键**。指令可以乱序**执行**，但必须按程序序**退休(retire/commit)**——即"对外生效、写进架构状态、可被观测"这一步严格按原顺序。

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  LifeLineBorderColor        #90A4AE
}
participant "程序序\n① ② ③ ④" as PO
participant "乱序执行\n(谁齐谁先算)" as EX
participant "ROB 按序退休\n① ② ③ ④" as ROB
PO -> EX : ① load(miss,慢)
PO -> EX : ② add(快,先算完)
PO -> EX : ③ add(快,先算完)
note over EX : ②③ 早于① 算完\n结果暂存 ROB,**不对外**
EX -> ROB : ① 终于回来
ROB -> ROB : 按序退休 ①→②→③
note over ROB : 对外生效顺序仍是 ①②③\n→ 单核看到的和顺序执行**一模一样**
@enduml
```

- **投机执行**：遇到分支还没算出方向时，CPU 会**猜**一条路先执行；猜错就把 ROB 里这批投机指令**全部作废**（像没发生过）。所以乱序 + 投机也不会污染架构状态。这个"猜"由**分支预测器**负责，猜得准不准直接决定性能——详见 [branch-prediction.md](/concepts/microarch/branch-prediction.md)。
- **精确异常**：靠顺序退休，异常/中断发生时架构状态恰好停在"出错指令之前全退休、之后全没生效"，和顺序执行的语义一致。
- **结论**：**单核程序无论如何都察觉不到自己被乱序执行了**——结果和顺序一致，异常点也一致。乱序纯粹是"偷偷加速"，语义零改变。

> 这正是乱序执行和内存重排的**分水岭**：乱序执行的乱序被 ROB 顺序退休**收口在核内**，单核透明；而内存重排是**退休之后**、写还堵在 store buffer 里没对别的核可见——**那已经不是乱序执行的范畴了。**

## 五、边界：乱序执行和内存重排到底在哪交、在哪分

回扣 [reordering-overview.md](/concepts/memory-ordering/reordering-overview.md) 第一节那张交集图，现在能讲透了：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<in>> #E3F2FD
  BorderColor<<in>>     #1976D2
  BackgroundColor<<x>>  #FFF9C4
  BorderColor<<x>>      #F9A825
  BackgroundColor<<out>> #FFCCBC
  BorderColor<<out>>    #E64A19
}
rectangle "纯核内乱序\n(算术/逻辑/分支的乱序)\n**永远单核透明,与内存序无关**" <<in>> as IN
rectangle "交集:访存的投机乱序\nOoO 把 load 提前执行\nx86 靠 machine-clear 拉回 TSO" <<x>> as X
rectangle "纯内存重排\n(store buffer 的 StoreLoad)\n**顺序核也会有,与 OoO 无关**" <<out>> as OUT
IN -right-> X
X -right-> OUT
@enduml
```

三块分清：

1. **纯核内乱序**（左）：两个 `add` 的乱序、分支投机——**只影响执行效率，从不影响内存可见顺序**，和多核内存模型完全无关。这是乱序执行的主体。
2. **交集**（中）：乱序引擎会**投机地把 load 提前**执行（看起来像 LoadLoad 重排）。但 x86 承诺 TSO——一旦发现"提前读的行在退休前被别核改了"，触发 **memory-ordering machine clear**，把这条投机 load 及后续清空重放（`perf` 的 `machine_clears.memory_ordering`）。**所以 x86 上 OoO 造成的内存重排被硬件自动纠正回来，程序员基本看不到。**
3. **纯内存重排**（右）：store buffer 的 StoreLoad——**哪怕一个完全顺序执行、没有乱序引擎的核，只要有 store buffer 就会有**。这证明内存重排的主因是缓冲区，**不是** OoO。

| 判断 | 对/错 | 为什么 |
|------|:---:|--------|
| "有乱序执行就会有内存重排" | ❌ | x86 深度乱序，但把访存乱序用 machine-clear 兜回 TSO |
| "内存重排都是 CPU 乱序执行造成的" | ❌ | 主因是 store buffer/invalidate queue；顺序核也有 StoreLoad |
| "关掉乱序执行就没有内存重排了" | ❌ | store buffer 还在，StoreLoad 照旧 |
| "乱序执行是单核性能优化，单核透明" | ✅ | 顺序退休 + 精确异常保证 |

> **一句话边界**：乱序执行的乱序**在退休时被收口在核内**（单核透明）；内存重排发生**在退休之后的缓冲区层**（跨核可见）。二者只在"访存投机重排"处擦边，而这一擦边在 x86 上还被 machine-clear 抹平了。**所以把它们当同义词，是根本性的概念混淆。**

## 六、和性能观测的关系

乱序执行是"隐形加速"，但它的**效果和失效**都能在 `perf` 里看到：

```bash
# IPC(每拍退休指令数)是乱序执行成效的直接指标:接近端口数说明并行挖得好
perf stat -e cycles,instructions ./app          # IPC = instructions/cycles
# 前端/后端停顿:乱序引擎"喂不饱"或"退不动"的瓶颈
perf stat -e cycles,stalled-cycles-frontend,stalled-cycles-backend ./app
# 分支预测失败:投机执行猜错→ROB 清空重放,乱序收益打折
perf stat -e branches,branch-misses ./app
# 内存序 machine clear:第五节"交集"里投机 load 违反 TSO 被拉回的计数
perf stat -e machine_clears.memory_ordering ./app
```

- **IPC 高** = 乱序执行成功把多个端口喂满；**IPC 低 + backend 停顿高** = 多半卡在 load miss（乱序也掩盖不了太多的内存延迟，这时该优化的是 cache 命中/数据布局，见 [mesi.md](/concepts/cache/mesi.md) 伪共享）。
- **branch-misses 高** = 投机猜错多，ROB 频繁清空，乱序收益被吃掉——优化分支可预测性。

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

- **总纲**：[reordering-overview.md](/concepts/memory-ordering/reordering-overview.md)——三层重排框架，本篇是其"第2层"的展开。
- **正交话题**：[memory-reordering.md](/concepts/memory-ordering/memory-reordering.md)（第3层：四种内存重排）、[store-buffer.md](/concepts/memory-ordering/store-buffer.md)/[invalidate-queue.md](/concepts/memory-ordering/invalidate-queue.md)（内存重排的成因部件）——本篇反复强调它们和乱序执行**不是一回事**。
- **延迟数据**：[cache-organization.md 第十一节](/concepts/cache/cache-organization.md) 的周期表（乱序要掩盖的正是那些几百拍的 miss 延迟）。
- **应用**：[atomic.md](/concepts/cache/atomic.md)（原子操作的开销与争用）。
- **数据冒险**：[data-hazards.md](/concepts/microarch/data-hazards.md)——乱序执行是数据冒险的主要硬件解法：RS 绕过阻塞的 µop 发射其他就绪的，L1 miss 窗口内靠 MLP 并行发出多条 miss。
- **纵切面走读**：[uop-pipeline-walkthrough.md](/concepts/microarch/uop-pipeline-walkthrough.md)——乱序执行从取指到退休的完整 µop 级走读，涵盖 RS/ROB/PRF 的协作时序。

## 八、一句话总结

> **CPU 乱序执行是单核性能引擎:前端顺序取指、中间靠寄存器重命名消假依赖+保留站按就绪乱序发射、后端靠 ROB 顺序退休——"顺序进、乱序算、顺序退",配合投机执行掩盖长延迟,让 IPC 冲高。关键在"顺序退休"把乱序收口在核内,所以单核完全透明。它和内存重排只在"访存投机重排"处擦边,且 x86 用 machine-clear 把这点擦边也抹平了——乱序执行管"跑得快"、内存重排管"多核对不对",是正交的两件事,别混。**
