﻿# µop 级流水线走读 —— 几条典型指令从取指到退休的完整旅程

> 把前面各部件文档（译码、重命名、保留站、执行端口、LSU、ROB）串起来，用三条真实 x86 指令逐拍追踪 µop 在流水线里的完整生命周期。

## 一、为什么要做 µop 级走读

各部件文档是"横切面"——讲清楚了每个部件**怎么工作**。但缺一个"纵切面"——一条指令到底**怎么穿过**所有部件，从取指到退休，每拍在哪个部件、做了什么、和谁交互。

本文以三条典型指令为例，逐拍拆解：

| 指令 | 类型 | 拆成几条 µop | 涉及部件 |
|------|------|:----------:|---------|
| `mov [rbx], eax` | store（地址+数据） | **2 µop** | AGU、Store Buffer、LSU、ROB |
| `add eax, [rsi+8]` | load + ALU | **2 µop** | AGU、L1 D-Cache、ALU、RS tag 匹配 |
| `addsd xmm0, [rdi+rcx*8]` | 浮点 load + FP 运算 | **2 µop** | AGU、FP RS、FP 执行单元、FP 物理寄存器 |

> 以下走读基于 Skylake 微架构（6 发射宽度、4 ALU + 3 AGU + 2 FMA），但原理适用于所有现代超标量乱序核。

## 二、前置知识：µop 是什么、怎么切

### 2.1 µop 的类型（功能分类）

x86 译码器产生的 µop 按**功能**可以分成以下六大类。每一类从译码出来后，进入不同 RS（保留站）、走不同执行端口、经历不同的流水线路径。

#### 2.1.1 六大类 µop

| 类别 | 典型来源（x86 指令） | 做什么 | 进哪个 RS | 执行端口（Skylake） | EX 延迟 |
|------|---------------------|--------|-----------|---------------------|:---:|
| **ALU** | `add`, `sub`, `and`, `or`, `xor`, `shl`, `shr`, `cmp`, `test`, `lea`, `inc`, `dec`, `mov reg,reg` | 整数算术/逻辑/移位/比较，计算寄存器值 | INT RS（Unified RS） | Port 0 / 1 / 5 / 6 | 1 拍 |
| **Load (LD)** | `mov eax, [rbx]` 的 load 部分，`add eax, [rsi]` 的 load µop | AGU 算地址 → LSU 读 D-Cache → 数据写回物理寄存器 | INT RS | Port 2 / 3（AGU） | **4~5 拍**（L1 hit） |
| **Store Address (STA)** | `mov [rbx], eax` 的地址 µop | AGU 算存储地址 → 写入 Store Buffer 地址域 | INT RS | Port 2 / 3 / 7（AGU） | **1 拍**（只算地址） |
| **Store Data (STD)** | `mov [rbx], eax` 的数据 µop | 从物理寄存器读数据 → 写入 Store Buffer 数据域 | INT RS | Port 4（Store Data） | **1 拍**（只搬数据） |
| **FP / SIMD** | `addsd`, `mulsd`, `addps`, `mulps`, `fmadd132sd`, `cvtsi2sd`（标量/向量浮点/SIMD 运算） | 浮点/向量运算（加减乘除、FMA、类型转换） | **FP RS**（独立调度域） | Port 0 / 1（FMA）/ Port 5（shuffle） | 3~5 拍（FMA 4~5 拍） |
| **Branch (BR)** | `jmp`, `je`, `jne`, `call`, `ret` | 计算分支方向→验证预测→误预测时触发流水线冲刷 | INT RS | Port 0 / 6（BRU） | 1 拍 |

> **为什么 FP µop 专有一个 RS**：FP 执行单元的延迟远长于 ALU（FMA 4~5 拍 vs ALU 1 拍）。如果把 FP µop 和 ALU µop 塞进同一个 RS，慢速 FP µop 会挤占 RS 槽位——长延迟意味着它占槽位久，影响 RS 利用率。独立 FP RS 让 FP 和整数两条调度通道完全解耦，互不阻塞。Skylake 的 INT RS = 97 条目，FP RS = 64 条目。

> **为什么 store 要拆成 STA + STD**：Store 有两个独立操作数——地址和要写的数据。在保留站里，**地址可能先就绪而数据还得等**（比如地址寄存器 rbx 已就绪，但数据寄存器 eax 还被前一条长延迟加法占用）。拆开以后，STA 可以先发射（算好地址送入 Store Buffer），STD 等数据就绪再发射——这不影响 STA 已做完的工作。如果把它们绑成一条 µop，就得等两个操作数都齐了才能发射。

#### 2.1.2 从 µop 类型看执行端口分配（Skylake 核心图）

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<rs>>   #FFF9C4
  BorderColor<<rs>>       #F9A825
  BackgroundColor<<port>> #E3F2FD
  BorderColor<<port>>     #1976D2
  BackgroundColor<<uop>>  #C8E6C9
  BorderColor<<uop>>      #388E3C
}

rectangle " ALU " <<uop>> as ALU
rectangle " LD  " <<uop>> as LD
rectangle " STA " <<uop>> as STA
rectangle " STD " <<uop>> as STD
rectangle " BR  " <<uop>> as BR
rectangle " FP  " <<uop>> as FP

ALU -[hidden]right- LD
LD  -[hidden]right- STA
STA -[hidden]right- STD
STD -[hidden]right- BR
BR  -[hidden]right- FP

rectangle " INT RS (97 entries) " <<rs>> as INT {
  rectangle "P0\nALU/BR/FMA"  <<port>> as P0
  rectangle "P1\nALU/FMA/Vec"  <<port>> as P1
  rectangle "P2\nLD/STA AGU"   <<port>> as P2
  rectangle "P3\nLD/STA AGU"   <<port>> as P3
  rectangle "P4\nSTD"          <<port>> as P4
  rectangle "P5\nALU/Shuffle"  <<port>> as P5
  rectangle "P6\nALU/BR/JMP"   <<port>> as P6
  rectangle "P7\nSTA AGU"      <<port>> as P7
}

rectangle " FP RS (64 entries) " <<rs>> as FP_RS {
  rectangle "FP0\nFMA/FADD" <<port>> as FP0
  rectangle "FP1\nFMA/FADD" <<port>> as FP1
  rectangle "FP5\nShuffle"  <<port>> as FP5
}

ALU -down-> P0
ALU -down-> P1
ALU -down-> P5
ALU -down-> P6
LD  -down-> P2
LD  -down-> P3
STA -down-> P2
STA -down-> P3
STA -down-> P7
STD -down-> P4
BR  -down-> P0
BR  -down-> P6
FP  -down-> FP0
FP  -down-> FP1
FP  -down-> FP5

INT -[hidden]right- FP_RS

note bottom of INT : 每拍每个端口只能发一条 µop。同类型过多时即使操作数全齐也要排队等端口，乱序也无法绕过。
@enduml
```

> 同一拍的 µop 可以同时发射到不同端口，但每个端口**一拍只能发一条**。`n` 条同类型 ALU µop 即使操作数全齐，也只能每拍发射 4 条（最多 4 个 ALU 端口），这就是**端口冲突**——乱序执行也无法绕过的结构瓶颈。

#### 2.1.3 特殊 µop —— macro-op fusion

某些 x86 指令对在译码阶段会**融合成一条 µop**，不算两条：

| x86 指令对 | 融合后的 µop | 说明 |
|-----------|:----------:|------|
| `cmp + jcc` | 1 条 CMP+BR µop | 比较+条件跳转融成一条，同时做比较和分支判断 |
| `dec + jnz` | 1 条 DEC+BR µop | 减一+非零跳转（循环计数器结尾） |
| `add/sub + jcc` | 1 条 ALU+BR µop | 加减后立即条件跳转 |

> 这不是两件事"合并执行"——是译码器识别出这对指令后，不拆成两条独立的 ALU+BR µop，而是在 µop cache（DSB）里存成一条融合 µop。**省了一条 µop = 省了一个 RS 槽位 + 省了一个发射带宽**。

#### 2.1.4 µop 类型与流水线路径的关系

不同 µop 类型走出完全不同的流水线路径：

| µop 类型 | 路径 | 关键差异 |
|---------|------|---------|
| **ALU** | RS → ALU Port → 结果直接写回 PRF → WB | 最短路径，1 拍出结果，EX 拍就可用（前推到下一拍） |
| **LD** | RS → AGU → TLB → L1 D-Cache → 等 4~5 拍 → 数据写回 PRF → WB | EX 拍只算地址，真正读缓存发生在 MEM 级，数据到 WB 才进 PRF |
| **STA** | RS → AGU → 地址写 Store Buffer | EX 拍算地址 → Store Buffer 存入即完成，不等 D-Cache 写入 |
| **STD** | RS → Store Data Port → 数据搬入 Store Buffer | EX 拍搬数据到 Store Buffer 对应槽位 |
| **FP** | FP RS → FMA Port → 3~5 拍浮点流水线 → 写 FP PRF → WB | 专用 FP RS + 专用 FP 物理寄存器堆，与整数完全隔离 |
| **BR** | RS → BRU Port → 算方向 → 和 BPU 预测比对 → 通过则继续 / 不通过则冲刷 | 分支执行发生在 EX 拍，但误预测的冲刷代价是多拍流水线 = 等待被冲刷 µop 从后端所有部件清空 |

> **PR（Physical Register，物理寄存器）和 PRF（Physical Register File，物理寄存器文件）**：
>
> 架构寄存器（RAX/RBX/XMM0 等）只有 16 个（整数）+ 16 个（浮点），但 CPU 内部实际的寄存器远不止这些。PRF 就是这些物理寄存器的存储阵列（Skylake 整数 PRF = 180 条目，FP PRF = 168 条目），里面**每一个条目就是一个 PR**。
>
> 重命名之后，所有 µop 读写的都是 PR（P15, P28, P45...），不再是架构寄存器号。"P" 开头的编号（P15/P28/P45/FP_PR P12）就是物理寄存器号。PRF 比架构寄存器多出来的那些 PR 存的是"飞行中"的中间结果——退休时架构寄存器才指向最新提交的 PR，多出来的 PR 还在等消费者读完才归还。
>
> **整数 PRF 和 FP PRF 是两块独立的物理存储**——这也是为什么 FP µop 有专用 FP RS + 专用 FP PRF（编号写作 FP_PR Px），跟整数 µop 从存储到调度都彻底隔离。

> 理解了这些 µop 类型，后面三条案例就走得通：你看到 `mov [rbx],eax` 拆成 STA+STD，就知道它**把地址和数据拆给了不同执行端口，Store Buffer 做中间聚合**；看到 `add eax,[rsi+8]` 拆成 LD+ADD，就知道 **LD µop 走 LSU→L1 D-Cache，ADD µop 走 ALU，两者在保留站里有 RAW 依赖链**。

### 2.2 x86 指令 → µop 的切分

x86 是 CISC 指令集，一条指令可能做多件事。现代 CPU 的前端译码器（MITE/DSB/LSD）把 x86 指令**切分成 µop**——每条 µop 做一件事：

```bash
指令                    → µop 分解                       说明

mov [rbx], eax          → STA [rbx]        (Store Address)    µop1: 计算目标地址 rbx → store buffer
                          STD eax           (Store Data)       µop2: 把 eax 的值写入 store buffer 数据域

add eax, [rsi+8]        → LD  tmp, [rsi+8]  (Load)            µop1: 从 [rsi+8] 读数据到临时物理寄存器
                          ADD eax, tmp      (ALU)             µop2: eax + tmp → eax

addsd xmm0, [rdi+rcx*8] → LD  tmp, [rdi+rcx*8]               µop1: 从 [rdi+rcx*8] 读 8 字节到临时浮点物理寄存器
                          ADDSD xmm0, tmp                     µop2: xmm0 + tmp → xmm0（浮点加）
```

> **关键认知**：一条 x86 store 指令 (`mov [rbx], eax`) 拆成两条 µop，一条算地址（STA）、一条写数据（STD）。这两条 µop **在保留站里独立调度**，AGU 算地址可能比 ALU 算数据先就绪——它们甚至可能在不同拍发射。这正是 µop 级走读要看清的。

### 2.3 µop 在流水线中走过的全部阶段

```plantuml
@startuml
skinparam shadowing false
skinparam sequenceMessageAlign center
skinparam BoxPadding 2

participant "IF\n取指"  as IF
participant "ID\n译码"  as ID
participant "REN\n重命名" as REN
participant "RS\n保留站" as RS
participant "EX\n执行"   as EX
participant "WB\n写回"   as WB
participant "RET\n退休"   as RET

IF -> ID : x86 指令字节
ID -> REN : µop 序列\n(操作码 + 架构寄存器号)
note right of REN : 分配目的物理寄存器(PR)\n分配 ROB 槽位\n架构寄存器 → 物理寄存器号
REN -> RS : µop + 物理寄存器 tag
note right of RS : 等待源操作数 tag 匹配\n就绪后被调度器挑选发射
RS -> EX : 就绪 µop
note right of EX : ALU 算结果\nAGU 算地址\nLSU 读 D-Cache\nFPU 算浮点\n写 Store Buffer
EX -> WB : 结果值 + 目的 PR tag
note right of WB : 结果写入 PRF\n结果总线广播 tag\n等待该 tag 的 µop 被唤醒
WB -> RET : µop 进入退休队列
note right of RET : ROB head 按序退休\nstore µop 标记可刷 D-Cache\n释放旧物理寄存器\n可批量 4~8 µop/拍
@enduml
```

每个阶段的核心动作：

| 阶段 | 缩写 | 核心动作 | 耗时 |
|------|------|---------|:----:|
| 取指 | IF | 从 I-Cache 取 x86 指令字节，送译码器 | 1 拍（I-Cache 命中） |
| 译码 | ID | x86 指令 → µop 序列，识别操作码、寄存器号 | 1 拍 |
| 重命名 | REN | 查 RAT 把架构寄存器号换成物理寄存器号；目的寄存器从空闲列表分配新 PR；ROB 分配槽位 | 1 拍 |
| 保留站 | RS | µop 在 RS 槽位里等源操作数就绪（tag 匹配）；就绪后被调度器挑中发射 | 可变（依赖链长度） |
| 执行 | EX | ALU 算结果 / AGU 算地址 / LSU 读 D-Cache / 浮点单元算 FMA | 1-5 拍（D-Cache 命中 ~4-5 拍） |
| 写回 | WB | 执行结果写入目的物理寄存器；结果总线广播 tag → RS 里等待它的 µop 被唤醒 | 1 拍 |
| 退休 | RET | ROB head 的 µop 按序退休：store µop 标记 store buffer 可刷入 D-Cache；释放旧物理寄存器 | 1 拍（可批量 4~8 µop） |

> 后面逐拍走读时，每拍都标清楚每条 µop 处于哪个阶段、在做什么。

### 2.4 贯穿全文的关键机制（术语前置）

以下四个概念在三条案例里反复出现，这里先定义清楚，后面不再重复解释。

#### 2.4.1 Store Buffer —— 为什么 store 不直接写 D-Cache

```bash
store 指令  →  [ Store Buffer ]  →  (退休后) LSU 异步排空 →  L1 D-Cache
                    ▲
              先写到这里就"完成"了
              不用等 D-Cache 写入
```

`mov [rbx], eax` 如果直接写 L1 D-Cache，需要先拿到 cache line 的**独占权**（MESI 协议），这期间流水线得 stall——几十到几百拍。Store Buffer 是 LSU 里的一个**硬件队列**（Skylake ~56 条目），store µop 只把地址和数据塞进去就算"执行完毕"，退休后 LSU 再在后台异步地把它们刷入 D-Cache。

> **关键后果**：本核后续的 load 如果读的正是一个"已在 Store Buffer 里但还没刷入 D-Cache"的地址，LSU 必须直接从 Store Buffer 把数据前推给 load，而不是去读 D-Cache（D-Cache 里还是旧值）。这叫 **store-to-load forwarding**（store 转发）。但其他核看不到你的 Store Buffer——这就是 StoreLoad 重排的根源。

#### 2.4.2 RAT + 结果总线 + tag 广播 + 唤醒

这是寄存器重命名之后，**依赖 µop 之间通信**的完整链路：

```bash
生产者 µop 在 REN 阶段分配目的 PR（如 P45）
  → 消费者 µop 在 REN 阶段查 RAT，发现源操作数映射到同一个 PR（P45），tag 标记"未就绪"
  → 消费者在 RS 里等待，监听结果总线
  → 生产者执行完毕，结果总线广播 "P45 就绪！值 = 0x05"
  → RS 里的消费者 tag 匹配 → 源操作数标记就绪 → 被调度器挑中发射
```

| 概念 | 是什么 | 在哪 |
|------|--------|------|
| **PR** (Physical Register) | 物理寄存器，是 PRF 存储阵列里的一个条目。编号如 P15/P28/P45（整数）或 FP_PR P12（浮点）。每个 PR 存一个值 | 读写贯穿 RS→EX→WB |
| **PRF** (Physical Register File) | 所有 PR 组成的存储阵列，整数 180 条目、FP 168 条目 | EX 读 PRF、WB 写 PRF |
| **RAT** (Register Alias Table) | 架构寄存器 → 物理寄存器号的映射表 | REN 阶段查/写 RAT |
| **PR tag** | 每条 µop 的源/目的物理寄存器编号，源 tag=0 表示值已就绪 | 贯穿 RS→WB |
| **结果总线** (result bus) | 执行单元写回 PRF 时广播 tag 的总线，所有 RS 槽位都在监听 | WB 阶段 |
| **唤醒** (wakeup) | RS 里的等待 µop 监听到自己的源 tag 匹配了结果总线广播 → 标记就绪 | RS 阶段 |
| **前推网络** (forwarding/bypass) | 不等结果写回 PRF 再读，执行单元输出直接旁路到下游执行单元输入，**省 1 拍 PRF 读写延迟** | EX 拍内 |

> **为什么需要 tag 广播**：重命名之后 µop 只知道"我等的是 P45"，不知道 P45 什么时候产生。结果总线广播就是硬件版的"P45 好了！"——所有 RS 槽位同时监听、同时匹配，匹配上的那个 µop 立即被唤醒。

#### 2.4.3 Store Buffer 的"可提交"与"排空"

| 阶段 | 触发条件 | 能去哪 |
|------|---------|--------|
| **填充** | STA µop 执行（写地址域）+ STD µop 执行（写数据域） | 槽位里地址+数据都有了 |
| **标记可提交** | STA 和 STD 在 ROB 中都退休了 | Store Buffer 槽位状态变更为 eligible |
| **排空 (drain)** | LSU 找机会（有空闲 D-Cache 写端口、该地址 cache line 处于可写状态） | 槽位数据刷入 L1 D-Cache，槽位归还 |
| **store forwarding** | 同核 load 地址匹配到已填充但未排空的槽位 | 数据从 Store Buffer 直送给 load，**不走 D-Cache** |

> 排空没有单独的 µop，不是流水线的一部分。它发生在 µop 退休之后，由 LSU 硬件状态机自主完成。

## 三、案例一：`mov [rbx], eax` —— store 指令的全旅程

假设条件：
- rbx = 0x7fff1000（有效虚拟地址）
- eax = 0x2a（要写入的值）
- 这条指令之前：rbx 和 eax 都已在物理寄存器中就绪（没有依赖等待）
- 这条指令之前：ROB 里没有未退休的指令堵在 head
- Store Buffer 有空闲槽位
- [rbx] 对应的 cache line 在 L1 D-Cache 中（M 态，可直接写）

### 3.1 µop 切分

```bash
mov [rbx], eax
  ├── µop₁: STA  (Store Address)  — AGU 算目标地址，写入 store buffer 地址域
  └── µop₂: STD  (Store Data)     — 把 eax 的值写入 store buffer 数据域
```

> #### 为什么 store 要拆成 STA + STD
>
> `mov [rbx], eax` 要干两件**彼此独立的事**——算出目标地址（rbx 的值）和拿到要写的数据（eax 的值）。这两件事：
> - 用**不同的源寄存器**：地址用 rbx，数据用 eax
> - 走**不同的执行端口**：地址算完送 AGU（Port 2/3/7），数据搬运走 Port 4（Store Data）
> - 各自的操作数**未必同时就绪**：比如 rbx 早已就绪但 eax 还在等前一条 FP 乘法的结果
>
> 如果捆成一条 µop，就得等地址和数据**两个操作数都齐了**才能发射——这浪费了 AGU 端口在等待期间的调度机会。拆成 STA+STD 后，地址先就绪就先发 STA 算地址塞进 Store Buffer，数据晚就绪就晚发 STD，互不阻塞。
>
> #### STA 和 STD 有依赖吗？谁先执行？
>
> **没有 RAW / WAR / WAW 依赖。** STA 只读 rbx，STD 只读 eax——两个源完全不同。两条 µop 也不写物理寄存器——没有目的寄存器冲突。唯一的"汇合点"是 Store Buffer 的同一个槽位：地址写地址域，数据写数据域，两者填充的是同一个槽位的不同字段，不是同一个字段的先后覆写，所以也没有写后写的冲突。
>
> **可以同拍发射，也可以任意先后顺序。** 如果 rbx 和 eax 都就绪，STA 和 STD 同一拍发射——STA 走 Port 2 的 AGU，STD 走 Port 4 的 Store Data，互不争端口。如果 rbx 就绪但 eax 还在等，那么：
> - STA 先发射 → 地址写进 Store Buffer 槽位 #7 地址域，然后 ROB 标记完成
> - STD 在 RS 里等 eax 就绪后才发射 → 数据写进同一个槽位的**数据域**
> - Store Buffer 内部做"合体"：同一个槽位里，地址域和数据域独立写入，谁先到写谁，两者都到了就标记"可提交"
>
> 这种**无依赖、可乱序发射**的设计，让 store 指令对前序长延迟指令的容忍度很高——只要地址就绪，就能先把地址算完、占领 Store Buffer 槽位，数据晚到完全没关系。

### 3.2 µop 级流水线序列图

下图把所有参与者拉成纵向生命线，µop 按拍（时钟周期）从 IF 流动到 RET。注意 **两条 µop 在不同执行端口并行展开**——STA 走 Port 2 的 AGU，STD 走 Port 4 的 Store Data 端口，两者同拍发射、同拍执行。

```plantuml
@startuml
skinparam shadowing false
skinparam sequenceMessageAlign center

title mov [rbx], eax (89 03) —— store 指令 6 拍全旅程

participant "IF\n取指" as IF
participant "ID\n译码" as ID
participant "REN\n重命名" as REN
participant "RS\n保留站" as RS
participant "AGU\nPort 2" as AGU
participant "STD\nPort 4" as P4
participant "SB\nStore Buffer" as SB
participant "WB\n写回" as WB
participant "RET\n退休" as RET

== 拍 0-1：取指 → 译码 ==

IF -> ID : **拍 0**\n取机器码 89 03
note right of ID : **拍 1**\nopcode 89 = MOV r/m32,r32\nModRM: reg=0(eax) rm=3([rbx])\n生成 µop₁(STA) + µop₂(STD)

== 拍 2：重命名 + 分配 ROB + 分配 Store Buffer ==

ID -> REN : µop₁(STA)\n源=rbx(架构)
ID -> REN : µop₂(STD)\n源=eax(架构)

note right of REN
  **µop₁ STA**: rbx→P15(RAT查找)
  ROB#42, SB槽位#7地址域
  --
  **µop₂ STD**: eax→P28(RAT查找)
  ROB#43, SB槽位#7数据域
  --
  两者均不分配目的PR（store不写寄存器）
end note

REN -> RS : µop₁(ROB#42, src=P15, port=AGU)
REN -> RS : µop₂(ROB#43, src=P28, port=STD)

== 拍 3：保留站等待 → 调度器挑选发射 ==

note right of RS
  源 tag 均就绪 (P15,P28 tag=0)
  µop₁ 候选 Port 2/3/7
  µop₂ 候选 Port 4
  调度器同拍挑中两者发射
end note

RS -> AGU : µop₁ 发射\nROB#42
RS -> P4  : µop₂ 发射\nROB#43

== 拍 4：执行（两条 µop 并行在不同端口） ==

note right of AGU
  读 PRF → P15 = 0x7fff1000
  AGU 计算: VA=0x7fff1000
end note

note right of P4
  读 PRF → P28 = 0x0000002a
  数据 0x2a 就绪
end note

AGU -> SB : VA=0x7fff1000\n写入地址域
P4  -> SB : data=0x2a\n写入数据域

note right of SB
  槽位#7: 地址+数据均已填充
  状态: 等待 µop₁,µop₂ 均退休
end note

== 拍 5：写回 ==

SB -> WB : µop₁(ROB#42) 完成\nµop₂(ROB#43) 完成
note right of WB
  STA/STD 无目的PR → 无结果广播
  ROB 标记两者为"完成"
end note

== 拍 6：退休 → Store Buffer 异步写 D-Cache ==

WB -> RET : ROB#42 (head) 退休\nROB#43 同拍批量退休

note right of RET
  **程序视角**: mov [rbx],eax 正式生效
  Store Buffer 槽位#7 标记"可提交"
  LSU 异步写入 L1 D-Cache (拍 N, N≥6)
end note

RET -> SB : 槽位#7 可提交
SB -> SB : 异步写入 L1 D-Cache\n(延迟不定, 多拍之后)

@enduml
```

### 3.3 各阶段的关键细节

**拍 0-1（IF→ID）**：`mov [rbx], eax` 的 x86 编码为 `89 03`（2 字节，ModRM 编码）。译码器解析 opcode `89` = MOV r/m32, r32，ModRM 解码得到 reg=eax dst=[rbx]。生成两条 µop：`µop₁: STA [rbx]` 和 `µop₂: STD eax`。

**拍 2（REN）**：重命名引擎查 RAT（Register Alias Table）把架构寄存器号换成物理寄存器号——`rbx→P15`、`eax→P28`。STA 和 STD 都不写目的寄存器，不分配新 PR。ROB 各分配一个槽位（#42、#43），Store Buffer 分配同一个槽位 #7（STA 绑定地址域，STD 绑定数据域）。

> **注意**：STA 和 STD 虽然是同一条 x86 指令切出来的，但在 ROB 里各占一个槽位。Store Buffer 槽位 #7 在 µop₁ 退休前就已经写入了地址，但**只有等 µop₂ 也退休后**，这个槽位才标记为"可提交"。

**拍 3（RS）**：两条 µop 进入 INT RS。源操作数 tag = P15 和 P28 早已就绪（前序指令已将数据写入 PRF），均进入候选池。调度器看到 µop₁→Port 2 AGU 空闲、µop₂→Port 4 STD 空闲，同拍发射。

**拍 4（EX）**：两条 µop 分走不同执行端口——µop₁ 的 AGU 计算 VA = 0x7fff1000，µop₂ 从 PRF 读出 0x2a，地址和数据汇入 Store Buffer 槽位 #7。两种工作在**同一拍内并行完成**——这正是 STA/STD 拆分的收益。

**拍 5（WB）**：STA 和 STD 都不产生寄存器写回，ROB 标记两者"完成"。

**拍 6（RET）**：ROB head #42、#43 按序退休。Store Buffer 槽位 #7 标记"可提交"，之后 LSU **异步**地将数据写入 L1 D-Cache——写入发生在退休之后的不定拍（拍 N，N≥6）。从程序视角，这条 `mov [rbx], eax` 在退休拍正式生效。

> **写入 L1 D-Cache 没有单独的 µop。** store 指令的全生命周期只涉及两条 µop（STA + STD），退休之后的事情是 Store Buffer 控制器（LSU 内部状态机）在后台自行完成的——它按槽位序号排空（drain）已退休的 store buffer 条目到 L1 D-Cache，不消耗发射带宽、不占 ROB 槽位、不进 RS。唯一能"看到"它的地方是：如果后续同一个核的 load 命中了一个"已退休还没排空"的 store buffer 条目，会触发 **store-to-load forwarding**——直接从 store buffer 拿数据，不走 D-Cache。

### 3.4 关键洞察

> **store 指令的"执行完毕" ≠ "内存已更新"**
>
> - **拍 4**：执行完毕（Store Buffer 已填充地址和数据）
> - **拍 6**：退休（Store Buffer 槽位标记"可提交"，程序视角生效）
> - **拍 N（N≥6）**：真正写入 L1 D-Cache（LSU 异步完成，可能很多拍之后）
>
> ★ 多核编程的核心：其他核在"退休"和"写入 D-Cache"之间的窗口期看不到这个 store——这就是 **StoreLoad 重排**的根源。详见 [store-buffer.md](/concepts/memory-ordering/store-buffer.md)。

## 四、案例二：`add eax, [rsi+8]` —— load + ALU 混合指令

假设条件：
- rsi = 0x7fff2000
- eax = 0x10（当前值）
- [rsi+8] = [0x7fff2008] 在 L1 D-Cache 中命中，值 = 0x05
- 这条指令之前 rsi 和 eax 都已就绪
- 前一条对 eax 的写已完成（eax 的物理寄存器 P28 已释放，但此 add 写的是新 PR）

### 4.1 µop 切分与依赖关系

```bash
add eax, [rsi+8]
  ├── µop₁: LD  tmp, [rsi+8]     ← Load：从 [rsi+8] 读 4 字节 → 临时物理寄存器 P_new
  └── µop₂: ADD eax, tmp         ← ALU：eax + tmp → eax（新 PR）
         │         │
         │         └── 源2：依赖 µop₁ 的结果 P_new（RAW 真依赖！）
         └── 源1：读 eax 的当前 PR（P28，已就绪）
```

> **这是有 RAW 依赖的一对 µop**：µop₂ 的源操作数 `tmp` 来自 µop₁ 的结果。µop₂ 必须在 RS 里等 µop₁ 的结果总线广播（tag 匹配）才能发射。这就是为什么 load→use 延迟对性能影响大。

### 4.2 µop 级流水线序列图

下图展示 LD→ADD 这对有 RAW 依赖的 µop 如何穿过流水线各阶段。关键看点：**µop₂(ADD) 在 RS 里等待 µop₁(LD) 的结果总线广播，P45 的 tag 从"等待"→"就绪"的那一拍的唤醒逻辑。**

```plantuml
@startuml
skinparam shadowing false
skinparam sequenceMessageAlign center

title add eax, [rsi+8] (03 46 08) —— load + ALU，带 RAW 依赖

participant "IF\n取指" as IF
participant "ID\n译码" as ID
participant "REN\n重命名" as REN
participant "RS\n保留站" as RS
participant "AGU\nPort 2" as AGU
participant "LSU\nL1 D-Cache" as LSU
participant "ALU\nPort 0" as ALU
participant "WB\n写回"   as WB
participant "RET\n退休"   as RET

== 拍 0-1：取指 → 译码 ==

IF -> ID : **拍 0**\n取机器码 03 46 08
note right of ID : **拍 1**\nopcode 03=ADD r32,r/m32\n生成 µop₁(LD) + µop₂(ADD)

== 拍 2：重命名 —— RAW 依赖在此建立 ==

ID -> REN : µop₁(LD)\nsrc=rsi, dst=tmp
ID -> REN : µop₂(ADD)\nsrc1=eax, src2=tmp, dst=eax

note right of REN
  **µop₁ LD**: rsi→P22, tmp→P45(新分配)
  ROB#100
  --
  **µop₂ ADD**: eax→P28, tmp→P45, eax→P46(新分配)
  ROB#101
  --
  ★ µop₂的src2=P45 tag≠0
  → RAW真依赖: µop₂必须等µop₁算出P45
end note

REN -> RS : µop₁(ROB#100, src=P22, dst=P45)
REN -> RS : µop₂(ROB#101, src1=P28, src2=P45≠0!, dst=P46)

== 拍 3：µop₁ 就绪发射 / µop₂ 在 RS 中等待 ==

note right of RS
  µop₁: P22 tag=0 → 就绪
  µop₂: P45 tag≠0 → 等待
  调度器挑中µop₁→Port 2 AGU
end note

RS -> AGU : µop₁ 发射\nROB#100

note right of AGU
  读 PRF → P22 = 0x7fff2000
  AGU: VA = 0x7fff2000 + 8
             = 0x7fff2008
end note

note right of RS #FFCDD2 : **µop₂ 仍在 RS 等待**\nsrc2 tag=P45 未就绪

== 拍 4：µop₁ 查 L1 D-Cache / µop₂ 继续等待 ==

AGU -> LSU : VA = 0x7fff2008
note right of LSU
  TLB 翻译 → PA (命中)
  查 Store Buffer → 无匹配
  查 L1 D-Cache Tag → **命中!**
  数据 0x05 本拍末到达
end note

note right of RS #FFCDD2 : **µop₂ 仍在等待**

== 拍 5：µop₁ 写回 → µop₂ 唤醒、发射 ==

LSU -> WB : **P45 = 0x05**
note right of WB
  结果总线广播 "P45 就绪!"
  ROB#100 标记完成
end note

WB -[#388E3C]> RS : **tag 匹配: P45!**
note right of RS #C8E6C9
  µop₂ src2 P45 tag→0 ✓
  src1 P28 tag=0 ✓
  → 就绪, 发射到 Port 0 ALU
end note

RS -> ALU : µop₂ 发射\nROB#101

note right of ALU
  读 P28 = 0x10 (eax旧值)
  前推拿 P45 = 0x05 (不经PRF读)
  计算: 0x10 + 0x05 = **0x15**
end note

== 拍 6：µop₂ 写回 ==

ALU -> WB : **P46 = 0x15**
note right of WB
  结果总线广播 "P46 就绪!"
  ROB#101 标记完成
end note

== 拍 7：退休 ==

WB -> RET : ROB#100 (head) 退休\nROB#101 同拍退休

note right of RET
  eax→P46 正式生效 (eax=0x15)
  eax 旧映射 P28 归还空闲列表
end note

@enduml
```

### 4.3 各阶段的关键细节

**拍 0-1（IF→ID）**：`add eax, [rsi+8]` 编码为 `03 46 08`（3 字节，ModRM+disp8）。译码器生成两条 µop：`µop₁: LD tmp, [rsi+8]` 和 `µop₂: ADD eax, tmp`。注意 `tmp` 是译码器自动插入的**伪寄存器（pseudo-register）**——它在重命名阶段会分配真实的物理寄存器（下一拍分配 P45），但此物理寄存器**不对应任何架构寄存器**（RAX/RBX/RDI 等的 RAT 表项都不会指向它），程序层面对它完全无感知。它唯一的作用是承载 load 结果，供给 ADD µop 消费后立即归还。

**拍 2（REN）**：重命名时的 RAW 依赖建立：

```bash
; 译码产物（架构寄存器）               →  重命名后（物理寄存器）
; µop₁: LD  tmp, [rsi+8]               →  LD  P45, [P22+8]     ROB#100
; µop₂: ADD eax, tmp                   →  ADD P46,  P28, P45   ROB#101  ← P45 tag 非零！
```

- µop₁：源 rsi→P22（已就绪），目的 tmp 从空闲列表分配 P45；ROB 分配 #100
- µop₂：源1 eax→P28（已就绪），源2 tmp→**P45**（tag 非零！），目的 eax 分配 P46；ROB 分配 #101

> **为什么 ADD 重命名后变成了三个操作数？**
>
> x86 的 `add eax, tmp` 语义是 `eax = eax + tmp`——两操作数，eax 既是源又是目的。但 µop 内部是 **RISC 风格三操作数 `dest = src1 OP src2`**，原因在寄存器重命名的核心规则：**每次写入必须分配新的物理寄存器**。
>
> eax 的旧值是 P28，不能把加法结果写回 P28（可能有其他飞行中的 µop 还在读 P28），所以分配 P46 作为新目的。于是 `ADD eax, tmp` 变成 `ADD P46, P28, P45`——源和目的完全分离：
>
> ```
> ADD P46,  P28,  P45
>  ↑新目的  ↑旧eax  ↑tmp
>  (= src1) (= src2)
> ```
>
> 所有 x86 的"累加器风格"两操作数指令，在 µop 层面全部展开为三操作数。这也是 PRF 条目数远超架构寄存器数的根本原因——每条写寄存器的 µop 都要消耗一个新的 PR，旧 PR 要等最后一个消费者读完才归还。

> **重命名的精妙之处**：µop₁ 的目的 PR P45 和 µop₂ 的源2 PR P45 是同一个物理寄存器。RAT 在 µop₁ 分配时就建立了 `tmp→P45` 映射，µop₂ 紧接着查 RAT 就拿到了 P45。这就是 RAT 的"写后读"转发——不需要 µop₁ 执行完，µop₂ 就知道"我等的是 P45"。

**拍 3（RS）**：µop₁ 的 P22 就绪，发射到 Port 2 AGU 算地址。µop₂ 的 P45 tag 非零，留在 RS 里等待。

**拍 4（LSU）**：µop₁ 的 LSU 流水线——TLB 翻译 VA→PA，查 Store Buffer 无匹配（无前向可能），查 L1 D-Cache Tag，命中，数据 0x05 本拍末返回。µop₂ 仍在 RS 里空等。

**拍 5（WB + RS 唤醒）**：µop₁ 的结果 0x05 写入 P45，结果总线广播 tag。RS 里的 µop₂ 监听到 P45 就绪→源2 tag 清零→emit 到 Port 0 ALU。ALU 通过前推网络直接拿 P45 的值（跳过 PRF 读，省 1 拍）。

**拍 6（WB）**：µop₂ 的结果 0x15 写入 P46，后续依赖 eax 的 µop 被唤醒。

**拍 7（RET）**：ROB #100、#101 同拍退休，eax→P46 正式生效。

### 4.4 load→use 延迟分析

| 事件 | 拍号 | 距 µop₁ 发射的延迟 |
|------|:---:|:---:|
| µop₁ 发射到 AGU | 拍 3 | — |
| µop₁ L1 D-Cache 命中，数据返回 | 拍 4 末 | 1 拍 |
| µop₁ 写回 P45，唤醒 µop₂ | 拍 5 | 2 拍 |
| µop₂ 发射到 ALU（前推拿 P45） | 拍 5 | **2 拍** |
| µop₂ 写回 P46 | 拍 6 | 3 拍 |

> **load→use 最小延迟 = 2 拍**（L1 D-Cache 命中 + 前推旁路）。如果 L1 miss 则需额外 12 拍（L2 hit）到数百拍（DDR）。
>
> 这 2 拍间隙里，如果 µop₁ 和 µop₂ 之间还有其他不依赖的指令，乱序执行可以用它们填入，掩藏这段延迟。

## 五、案例三：`addsd xmm0, [rdi+rcx*8]` —— 浮点 load + FP 运算，涉及跨集群调度

```bash
假设条件：
  - rdi = 0x7fff3000, rcx = 3
  - xmm0 当前值 = 1.5 (double)
  - [rdi + rcx*8] = [0x7fff3018] 在 L1 D-Cache 中命中，值 = 2.5 (double)
  - 前一条对 xmm0 的 FP 写已完成
```

### 5.1 µop 切分与跨集群路径

```bash
addsd xmm0, [rdi+rcx*8]
  ├── µop₁: LD  tmp, [rdi+rcx*8]     ← Load：AGU 算地址，读 8 字节 → 临时 FP 物理寄存器
  └── µop₂: ADDSD xmm0, tmp          ← FP ADD：xmm0 + tmp → xmm0（走 FP 执行单元）
```

> **和案例二的关键区别**：µop₁ 走 INT 集群（AGU 在 INT 侧），µop₂ 走 FP 集群（FP ADD 在 FP 侧）。两条 µop 在重命名后**分到不同的保留站**——µop₁ 进 INT RS，µop₂ 进 FP RS。跨集群的 tag 广播需要额外 1 拍的物理走线延迟。

### 5.2 µop 级流水线序列图

和案例二不同的是，µop₁(LD) 进 **INT RS**、µop₂(ADDSD) 进 **FP RS**——两条 µop 分属不同调度域。序列图中 INT RS 和 FP RS 各占一条生命线，跨集群的 tag 广播延迟一目了然。

```plantuml
@startuml
skinparam shadowing false
skinparam sequenceMessageAlign center

title addsd xmm0, [rdi+rcx*8] (F2 0F 58 04 CF) —— 跨集群浮点 load + FP ADD

participant "IF\n取指" as IF
participant "ID\n译码" as ID
participant "REN\n重命名" as REN
participant "INT RS\n(97)" as INT_RS
participant "AGU\nPort 2" as AGU
participant "LSU\nL1 D-Cache" as LSU
participant "FP RS\n(64)" as FP_RS
participant "FMA\nPort 0" as FMA
participant "WB\n写回"   as WB
participant "RET\n退休"   as RET

== 拍 0-1：取指 → 译码 ==

IF -> ID : **拍 0**\n取机器码 F2 0F 58 04 CF
note right of ID : **拍 1**\nF2 0F 58=ADDSD xmm,xmm/m64\nSIB: base=rdi, idx=rcx, scale=8\n生成 µop₁(LD) + µop₂(ADDSD)

== 拍 2：重命名 —— 分叉到两个 RS ==

ID -> REN : µop₁(LD)\nsrc=rdi,rcx dst=tmp(FP)
ID -> REN : µop₂(ADDSD)\nsrc1=xmm0 src2=tmp dst=xmm0

note right of REN
  **µop₁**: rdi→P50, rcx→P51
  dst=FP_PR P12, ROB#200
  → **进 INT RS** (AGU在INT集群)
  --
  **µop₂**: xmm0→FP_PR P8
  src2=FP_PR P12 (tag≠0!)
  dst=FP_PR P13, ROB#201
  → **进 FP RS** (FMA在FP集群)
  --
  ★ 两条µop进不同的RS! 跨集群依赖
end note

REN -> INT_RS : µop₁(#200)\nsrc=P50,P51 dst=FP_PR P12
REN -> FP_RS : µop₂(#201)\nsrc1=FP_PR P8, src2=FP_PR P12≠0

== 拍 3：INT RS 发射 µop₁ / FP RS 里 µop₂ 等待 ==

note right of INT_RS
  µop₁: P50,P51 tag=0 → 就绪
  调度器挑中→Port 2 AGU
end note

INT_RS -> AGU : µop₁ 发射\nROB#200

note right of AGU
  P50(rdi=0x7fff3000)+P51(rcx=3)×8
  = 0x7fff3000 + 24
  = **0x7fff3018**
end note

note right of FP_RS #FFCDD2 : **µop₂ 在 FP RS 等待**\nsrc2 FP_PR P12 tag≠0

== 拍 4-7：µop₁ LSU 查 D-Cache（~4拍）/ µop₂ 继续等待 ==

AGU -> LSU : VA = 0x7fff3018
note right of LSU
  拍4: TLB翻译→PA (命中)
  拍4-7: 查L1 D-Cache
  L1命中! 2.5 (0x4004000000000000)
  result ready end of 拍7
end note

note right of FP_RS #FFCDD2 : **µop₂ 等待了4拍**\n(拍4→拍7)\n乱序期间可能有其他\nFP µop在此期间发射

== 拍 8：µop₁ 写回 → 跨集群唤醒 µop₂ ==

LSU -> WB : **FP_PR P12 = 2.5**
note right of WB
  结果总线广播 "FP_PR P12就绪!"
  ROB#200 标记完成
  ★ INT→FP 跨集群广播 (+1拍走线延迟)
end note

WB -[#E64A19]> FP_RS : **跨集群 tag 匹配: FP_PR P12!**
note right of FP_RS #C8E6C9
  µop₂ src2 P12 tag→0 ✓
  src1 P8 tag=0 ✓
  → 就绪, 发射到 Port 0 FMA
end note

FP_RS -> FMA : µop₂ 发射\nROB#201

== 拍 9-12：µop₂ FP ADD 流水线（4拍） ==

note right of FMA
  拍9-12: FMA流水线深度4拍
  前推拿 FP_PR P8 = 1.5
  前推拿 FP_PR P12 = 2.5
  1.5 + 2.5 = **4.0** (0x4010000000000000)
end note

== 拍 13：µop₂ 写回 ==

FMA -> WB : **FP_PR P13 = 4.0**
note right of WB
  结果总线广播 "FP_PR P13就绪!"
  ROB#201 标记完成
end note

== 拍 14：退休 ==

WB -> RET : ROB#200 (head) 退休\nROB#201 同拍退休
note right of RET
  xmm0→FP_PR P13 正式生效
  xmm0 旧映射 FP_PR P8 归还空闲列表
end note

@enduml
```

### 5.3 各阶段的关键细节

**拍 0-1（IF→ID）**：`addsd xmm0, [rdi+rcx*8]` 编码 5 字节（`F2 0F 58 04 CF`），带 SIB 字节编码 base+index\*scale 寻址。生成 `µop₁: LD tmp, [rdi+rcx*8]` 和 `µop₂: ADDSD xmm0, tmp`。

**拍 2（REN）——本案例最关键的一拍**：
- µop₁：rdi→P50、rcx→P51（都就绪），目的 `tmp` 从 **FP 空闲列表**分配 FP_PR P12，**进 INT RS**
- µop₂：xmm0→FP_PR P8（就绪），src2 `tmp`→**FP_PR P12**（tag 非零），目的 xmm0 分配 FP_PR P13，**进 FP RS**

> **两条 µop 进了不同的 RS**。INT RS 的 tag 广播和 FP RS 的 tag 匹配走不同的物理总线，µop₁ 在 INT 集群完成 load 后，结果要跨集群广播到 FP 集群才能唤醒 µop₂——这额外增加了 ~1 拍物理走线延迟。

**拍 3（INT RS 发射 µop₁）**：µop₁ 的源操作数全就绪，AGU 算地址 0x7fff3018。µop₂ 在 FP RS 里等待 FP_PR P12。

**拍 4-7（µop₁ LSU 查缓存，4 拍）**：LSU 走 TLB→L1 D-Cache，数据 2.5 在拍 7 末就绪。µop₂ 在 FP RS 里空等这 4 拍。

**拍 8（跨集群唤醒）**：FP_PR P12 写入 2.5，结果总线跨集群广播到 FP RS，µop₂ 被唤醒并发射到 Port 0 FMA。

**拍 9-12（FP 流水线，4 拍）**：FMA 单元内部 4 拍流水线计算 1.5+2.5=4.0。

**拍 13（µop₂ WB）**：FP_PR P13 写入 4.0。

**拍 14（RET）**：ROB #200、#201 同拍退休，xmm0→FP_PR P13 正式生效。

### 5.4 跨集群调度的性能影响

> **INT 集群 vs FP 集群的调度差异**
>
> - **案例二**（`add eax, [rsi+8]`）：两条 µop 都在 INT 集群 → 同一个 RS → tag 广播 0 额外延迟
> - **案例三**（`addsd xmm0, [rdi+rcx*8]`）：µop₁ 在 INT 集群（AGU），µop₂ 在 FP 集群（FMA）
>   - 跨集群 tag 广播额外 ~1 拍走线延迟
>   - 但 FP 执行单元延迟本来就长（4 拍 vs 1 拍 ALU），多 1 拍不敏感
>   - 真正敏感的是：如果 AGU 和 FP 单元之间需要频繁传递数据（如混合精度计算），跨集群通信成为瓶颈

## 六、三条指令对比总览

把三条指令的全部 µop 从取指到退休的流水线时间线叠在一起对比：

#### 案例一：`mov [rbx], eax`（~6 拍，无 RAW 依赖）

> STA 和 STD 两个操作数相互独立，同拍进入 RS 后可以同一拍或相邻拍发射。

| µop | 拍1 | 拍2 | 拍3 | 拍4 | 拍5 | 拍6 |
|-----|:---:|:---:|:---:|:---:|:---:|:---:|
| **STA** | IF | ID | REN | RS | EX | RET |
| **STD** | — | IF | ID | REN | EX | RET |

STA 的 EX 走 Port 2 AGU（算地址写 Store Buffer 地址域），STD 的 EX 走 Port 4 Store Data（搬数据进 Store Buffer 数据域）——互不争端口，可同拍发射。退休后 LSU 异步排空到 L1 D-Cache。

#### 案例二：`add eax, [rsi+8]`（~7 拍，RAW 依赖，load→use = 2 拍）

| µop | 拍1 | 拍2 | 拍3 | 拍4 | 拍5 | 拍6 | 拍7 |
|-----|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| **LD** | IF | ID | REN | RS | LSU | WB | RET |
| **ADD** | — | IF | ID | REN | ⏸ RS | EX | RET |

> ⏸ = 等待，◀ = 前推旁路

ADD µop 在拍 2 REN 阶段就通过 RAT 拿到了 `P45` tag，但必须等 LD µop 的 LSU 完成（拍 4-5）、结果在拍 5 写回 PRF 并广播 tag 后才被唤醒。唤醒后 ALU 通过**前推网络**直接取 P45 的值（跳过一次 PRF 读），一拍算出结果。

#### 案例三：`addsd xmm0, [rdi+rcx*8]`（~14 拍，跨集群 + FP 4 拍流水线）

| µop | 拍1 | 拍2 | 拍3 | 拍4 | 拍5..8 | 拍9 | 拍10..13 | 拍14 |
|-----|:---:|:---:|:---:|:---:|:------:|:---:|:--------:|:---:|
| **LD** | IF | ID | REN | RS | LSU×4 | WB | RET | |
| **ADDSD** | — | IF | ID | REN | ⏸ FP | ⏸  | FMA×4 | RET |

> 拍 10..13 的 FMA×4 在专有 FP 执行流水线内完成，与整数 RS 完全隔离。

ADDSD µop 的关键瓶颈：
1. LD µop 走**整数** RS + 整数 PRF，结果 fp_tmp 写回 FP PRF（需要跨集群搬运，额外 1 拍）
2. ADDSD µop 走**浮点** RS + FP PRF，且 FP FMA 单元自身 4 拍流水线
3. 因此从 IF 到 RET 总延迟 ~14 拍，其中等待 + FP 流水线占 ~9 拍

| 维度 | `mov [rbx], eax` | `add eax, [rsi+8]` | `addsd xmm0, [rdi+rcx*8]` |
|------|:---:|:---:|:---:|
| µop 数 | 2（STA + STD） | 2（LD + ADD） | 2（LD + ADDSD） |
| µop 间依赖 | 无 RAW | 有 RAW（µop₂ 等 µop₁） | 有 RAW（µop₂ 等 µop₁） |
| µop 在同一个 RS？ | 是（INT RS） | 是（INT RS） | 否（INT RS + FP RS） |
| 最短总延迟（L1 命中） | ~6 拍 | ~7 拍 | ~14 拍 |
| 延迟瓶颈 | 无（可并行发射） | load→use 2 拍 | FP 流水线 4 拍 + 跨集群 |
| 退休时做什么 | 标记 store buffer 可提交 | 释放 eax 旧 PR | 释放 xmm0 旧 FP_PR |

## 七、从前端到退休：µop 在各部件的生命周期表

把三组 µop 的轨迹汇总成一张通用生命周期表：

| 阶段 | 部件 | µop 做了什么 | 输入 | 输出 | 耗时 |
|------|------|-------------|------|------|:--:|
| **IF** | 取指单元 | 从 I-Cache 取 x86 指令字节，PC 指向下一条 | PC 值 | 指令字节流（16/32 字节/拍） | 1 拍 |
| **ID** | 译码器 | x86 指令 → µop 序列，识别操作码、寄存器号、立即数 | 指令字节 | µop 序列（含类型、架构寄存器号、立即数） | 1 拍 |
| **REN** | 重命名引擎 | 架构寄存器号 → 物理寄存器号（查 RAT）；目的寄存器分配新 PR（从空闲列表）；ROB 分配槽位；store 指令分配 store buffer 槽位 | 架构寄存器号 | µop 携带物理寄存器 tag、ROB 槽位号 | 1 拍 |
| **RS** | 保留站 | µop 在槽位里等源操作数就绪（tag 匹配）；就绪后被调度器挑中发射 | 源 tag | 发射到执行端口 | 可变 |
| **EX** | 执行单元 | ALU 算结果 / AGU 算地址 / LSU 查 D-Cache / FMA 算浮点 / STA 写 store buffer 地址 / STD 写 store buffer 数据 | 操作数值、地址 | 结果值、store buffer 条目 | 1~5 拍 |
| **WB** | 结果总线 | 结果写入目的物理寄存器；广播 tag 唤醒 RS 中的依赖 µop | 结果值 + 目的 PR tag | RS 中依赖 µop 的源 tag 清零 | 1 拍 |
| **RET** | ROB | 按序退休：store µop 退休后 store buffer 槽位标记可提交；释放旧 PR 归还空闲列表 | ROB head 指针 | 架构状态更新、PR 回收 | 1 拍 |

## 八、store buffer / load buffer 的生命周期视角

从三个案例可以提炼出 store buffer 和 load buffer 的完整生命周期：

#### Store Buffer 槽位生命周期序列图

```plantuml
@startuml
skinparam shadowing false
skinparam sequenceMessageAlign center

title Store Buffer 槽位 #7 生命周期 —— mov [rbx], eax 的 STA + STD 异步填充

participant "REN\n重命名" as REN
participant "AGU\nPort 2" as AGU
participant "STD\nPort 4" as P4
participant "SB #7\nStore Buffer 槽位" as SB
participant "ROB\n退休单元" as ROB
participant "LSU\nLoad/Store 单元" as LSU
participant "L1\nD-Cache" as L1

== 拍 2（REN）：分配槽位 ==
REN -> SB: <b>分配 SB 槽位 #7</b>\\n状态：已分配
activate SB
note right #E3F2FD: STA + STD 各拿槽位号 #7\\n地址域/数据域均空

== 拍 4（EX）：地址 & 数据独立写入 ==
AGU -> SB: STA：写入地址域 0x7fff1000
P4 -> SB: STD：写入数据域 0x2a
note right #FFF3E0: <b>地址和数据走不同端口</b>\\nPort 2 AGU → 地址域\\nPort 4 Store Data → 数据域\\n两路可同拍写入，无冲突
note over SB #C8E6C9: <b>状态：已填充，等待退休</b>\\n地址 + 数据齐备\\n但槽位不能提交

== 拍 6（RET）：按序退休 → 标记可提交 ==
ROB -> SB: STA µop 退休
ROB -> SB: STD µop 退休（两条都退才算）
note right #FFCDD2: <b>为什么两条都要退？</b>\\nSTA/STD 是同一条指令的\\n两个 µop，共享同一个 ROB 条目\\nROB head 推进时两条一起退
note over SB #FFF9C4: <b>状态：可提交</b>\\n地址 + 数据均有效\\n等待 LSU 仲裁

== 拍 N（异步）：LSU 仲裁 → 写回 L1 → 释放 ==
LSU -> SB: 仲裁选中槽位 #7
SB -> L1: 数据写入 L1 D-Cache（Store 提交）
deactivate SB
note right #E8F5E9: <b>状态：已提交 → 槽位释放</b>\\n槽位 #7 回到空闲池\\n可取新 store µop

note over REN, L1 #F3E5F5
  <b>★ Store Buffer 三条关键设计</b>
  <b>① 地址&数据解耦</b>：STA/STD 走不同端口，可乱序填充同一槽位
  <b>② 退休 ≠ 提交</b>：ROB 退休只标记"可提交"\\n真正写 L1 由 LSU 仲裁决定时机
  <b>③ 异步排空</b>：写 L1 在退休之后，与流水线解耦\\nstore 对 L1 miss 的延迟完全透明
end note

@enduml
```

#### Load Buffer 槽位生命周期序列图

```plantuml
@startuml
skinparam shadowing false
skinparam sequenceMessageAlign center

title Load Buffer 槽位生命周期 —— add eax, [rsi+8] 的 LD µop 全路径

participant "RS\n保留站" as RS
participant "AGU\nPort 2/3" as AGU
participant "LB\nLoad Buffer" as LB
participant "TLB" as TLB
participant "SB\nStore Buffer" as SB
participant "L1\nD-Cache" as L1
participant "PRF\n物理寄存器堆" as PRF

== 拍 3（EX）：AGU 算地址 → 分配 Load Buffer ==
RS -> AGU: 发射 LD µop
activate AGU
AGU -> LB: 分配 Load Buffer 槽位\\n记录 VA = [rsi+8]
activate LB
note right #E3F2FD: VA 先存，等 TLB 翻译\\n才能得到 PA

== 拍 4（EX）：TLB + SB + L1 三级流水 ==
AGU -> TLB: VA → PA 翻译
activate TLB
TLB --> AGU: PA 返回
deactivate TLB
AGU -> SB: PA 查 SB 全相联比对（forwarding）
activate SB
SB --> AGU: <color:#666666>未命中（无匹配地址）</color>
deactivate SB
AGU -> L1: PA 索引 + tag 比较
activate L1
L1 --> AGU: <color:#4CAF50><b>命中</b></color> 数据 = 0x05
deactivate L1
note right #FFF3E0: <b>TLB + SB 并发</b>\\nSB 全相联比对\\nL1 set/way/tag 并行读出\\n全部在 1 拍内完成

== 拍 5（WB）：写回 PRF → 广播唤醒 → 释放 ==
AGU -> PRF: WB：数据 0x05 写入 P45\\n结果总线广播 tag=P45
activate PRF
deactivate PRF
deactivate AGU
deactivate LB
note right #C8E6C9: <b>Load Buffer 槽位释放</b>\\n生命周期：3 拍（分配→填充→写回）\\n远短于 Store Buffer（需等退休+仲裁）

note over RS, PRF #E8F5E9
  <b>★ Load Buffer vs Store Buffer 对比</b>
  <b>Load Buffer</b>：短寿命（3 拍），WB 后立即释放\\n不需要退休，不需要仲裁
  <b>Store Buffer</b>：长寿命（6~N 拍），退休后还要等\\nLSU 仲裁才能写 L1 并释放
  <b>共同点</b>：都是内存流水独占资源\\n满了一样 stall 前端
end note

@enduml
```

上述两个序列图揭示了 store buffer 和 load buffer 两种内存流水线结构的本质差异——它们虽然都叫 "buffer"，但生命周期形态截然不同。

**Store Buffer：为"解耦退休与提交"而设计**

store 指令的核心矛盾在于：x86 的 TSO 内存模型要求 store 对**本核**立即可见（store→load forwarding 让本核后续 load 能读到刚写的值），但对**其他核**必须等退休后才全局可见。如果 store 退休后直接写 L1 D-Cache，那 L1 miss 时整个流水线就会卡在退休阶段——这对吞吐是灾难性的。

Store Buffer 用"退休标记可提交 → LSU 异步排空"的两阶段设计解耦了这个矛盾：

| 阶段 | 触发条件 | 做什么 | 对流水线的影响 |
|------|---------|--------|-------------|
| **退休标记** | STA + STD 都退休 | 槽位地址+数据齐备，标记"可提交" | 同拍完成，不阻塞退休 |
| **LSU 仲裁 & 提交** | LSU 选中该槽位，L1 可写 | 数据写入 L1 D-Cache，槽位释放 | 退休之后异步进行，流水线完全透明 |

> **关键设计：STA 和 STD 独立发射**。地址先就绪就先写地址域，数据后到后填数据域——同一个槽位的两个字段独立填充，互不阻塞。这在案例一的 `mov [rbx], eax` 中体现为 0 额外延迟——两条 µop 同拍进 RS、同拍发射（走不同端口），退休后 LSU 挑一个时间点把数据刷进 L1。

但异步排空也有代价——store buffer 槽位是物理硬件资源：

- **槽位满了**：前端停止取指译码（store µop 在 REN 阶段无法分配 SB 槽位 → stall）
- **LSU 仲裁策略**：优先写同一 cache line 的数据（合并写），减少 L1 的 RFO 开销
- **WC（Write Combining）的批量提交**：写合并缓冲区是 store buffer 的兄弟结构——突发写入时不是逐条提交，而是攒够 64 字节一口气写（PCIe 设备映射内存的关键优化）

**Load Buffer：短寿命的流水暂存器**

load buffer 没有"退休→仲裁→提交"的异步尾巴——load 的结果（数据）在 WB 阶段就写入了 PRF，槽位当场释放。之所以还需要 load buffer，是因为以下三个"时间窗口"里需要暂存请求状态：

| 窗口 | 暂存内容 | 用途 |
|------|---------|------|
| **TLB→SB→L1 流水期间** | VA + 请求状态 | LSU 在这一拍内串行查询三级结构，load buffer 记录"正在处理中" |
| **L1 miss→L2/L3 期间** | PA + fill buffer 索引（LFB/MSHR） | 记录未完成的 miss 请求，L2 数据返回后找到对应的 PRF 目标 |
| **内存序违规检测** | VA + 年龄（ROB 位置） | 如果一条 younger load 在 older store 的地址算出之前就先执行了，load buffer 记录它的位置，等 store 地址出来时做冲突检测 |

**Store Buffer vs Load Buffer：生命周期差异的本质原因**

| 维度 | Store Buffer | Load Buffer | 根源 |
|------|-------------|-------------|------|
| 分配时机 | REN 阶段（前端） | EX 阶段（AGU 算完地址） | store 的地址早分、数据晚到，需提前占位；load 的地址=数据起点，先算后占 |
| 释放时机 | LSU 提交到 L1 后 | WB 写 PRF 后立即 | store 要等退休+仲裁；load 结果进 PRF 就算完成了 |
| 生命周期 | 6~N 拍（异步尾巴不确定） | 3 拍（固定） | store 的"提交到 L1"与流水线异步；load 全部在流水线内完成 |
| 满的后果 | REN 阶段 stall（前端停摆） | EX 阶段 stall（RS 不发射新的 load） | 分配阶段不同 |
| 硬件容量 | ~40—60 条 | ~10—12 条（含 LFB/MSHR） | load buffer 寿命短，不需大容量；store buffer 要覆盖 L1 miss 延迟，需要更多条目 |

> **Store Buffer 可视为"退休后的异步队列"**，Load Buffer 可视为"执行中的流水暂存器"。这是理解 LSU 微架构的关键——两者虽然都服务于内存 µop，但在流水线里处于完全不同的位置：store buffer 连接退休层和 L1，load buffer 连接执行层和 L1。

### 8.1 Store Buffer、Load Buffer、LSU 三者的层级关系

这三个概念很容易搞混——有的文档说"LSU 管理 store buffer"，有的说"store buffer 是 LSU 的一部分"，其实它们不是平级关系，而是**容器—子模块—职责**的嵌套结构：

```plantuml
@startuml
skinparam shadowing false
skinparam packageBorderColor #455A64
skinparam packageBackgroundColor #ECEFF1
skinparam componentBackgroundColor #E3F2FD
skinparam componentBorderColor #1E88E5
skinparam arrowColor #546E7A

title LSU 内部组件及数据流关系

package "LSU（Load/Store Unit）" as LSU #ECEFF1 {
  
  component "AGU × 3~4\n────────\nVA = base +\nindex×scale + offset\n────────\nPort 2 / 3 / 7" as AGU #BBDEFB
  component "Store Buffer\n────────\n~40—60 条\n────────\n· 地址域 (VA/PA)\n· 数据域\n· 状态：已分配/已填充/可提交" as SB #BBDEFB
  component "Load Buffer\n────────\n~10—12 条\n────────\n· VA + PA\n· ROB 序号 (年龄)\n· 状态：等待/进行中/完成" as LB #BBDEFB
  component "LFB / MSHR\n────────\nLine Fill Buffer\n────────\n跟踪未完成的\nL1 miss 请求" as LFB #BBDEFB
  
  AGU -right-> SB : 地址送给\n(STA µop)
  SB -left-> LB : store→load\nforwarding\n地址比对命中后\n旁路给 load
  LB -down-> LFB : load miss 时\n分配 LFB
  LFB -[hidden]down- LB
}

component "L1 D-Cache\n32KB, 8-way" as L1 #C8E6C9
component "TLB" as TLB #FFE0B2
component "ROB\n(退休触发)" as ROB #FFE0B2

SB -down-> L1 : 退休后 LSU 仲裁\n异步排空写入
LFB -down-> L1 : L2 数据返回后\n填充 L1 + PRF 写回
AGU -down-> TLB : VA → PA 翻译
SB -down-> ROB : 退休标记\n"可提交"

note bottom of L1 #FFF9C4
  <b>L1 D-Cache</b> 不在 LSU 内部
  但 LSU 是其唯一的访存端口
end note

note right of ROB #F3E5F5
  <b>ROB</b> 退休后触发：
  · SB 槽位标记"可提交"
  · 旧 PR 回收
end note

note top of AGU #E8F5E9
  <b>AGU</b> 是 LSU 的入口
  所有 load/store µop
  都从这里进入 LSU
end note

@enduml
```

**LSU 是容器，Store Buffer 和 Load Buffer 是它的两个核心子模块，AGU 是它的地址生成前端，LFB/MSHR 是它的 miss 跟踪后端。**

三者的职责边界如下：

| 组件 | 在 LSU 中的角色 | 核心职责 | 不负责什么 |
|------|---------------|---------|-----------|
| **LSU** | 整体容器 + 控制逻辑 | 协调所有访存 µop 的执行、排队、转发；管理 store buffer 的仲裁排空、load buffer 的 miss 跟踪、store→load forwarding 的地址比对 | 不执行非访存 µop（ALU/FMA 不走 LSU） |
| **Store Buffer** | 写入侧缓冲 | 暂存 store 的地址和数据；退休后等 LSU 仲裁排空到 L1；为 load 提供 forwarding 数据 | 不主动写 L1（LSU 仲裁决定时机）；不处理 load miss |
| **Load Buffer** | 读取侧暂存 + miss 跟踪 | 暂存进行中的 load 请求状态；记录 L1 miss 的 LFB 索引；参与内存序违规检测 | 不缓存数据（数据直接进 PRF）；不参与 store 提交 |
| **AGU** | 地址生成 | 算 VA（base + index×scale + offset），送给 TLB 翻译 | 不访问内存（AGU 只算地址，LSU 拿地址去访问） |
| **LFB/MSHR** | miss 状态跟踪 | 记录每条 L1 miss 的 PA + 目标 PRF tag；L2 返回后路由到正确的 PRF | 不缓存数据本身 |

**数据流角度：store 和 load 穿过 LSU 的路径完全不同**：

- **store 路径**：`AGU 算地址 → Store Buffer 槽位（地址域+数据域）→ ROB 退休标记可提交 → LSU 仲裁 → L1 D-Cache`
- **load 路径**：`AGU 算地址 → TLB 翻译 → Store Buffer forwarding 检查 → L1 D-Cache 查询 → (命中) PRF 写回 / (miss) LFB 跟踪 → L2 返回 → PRF 写回`

> **LSU 就是内存 µop 的"交通调度中心"**——AGU 是入口收费站，Store Buffer 是出城方向的暂存区（等退休后正式出城），Load Buffer + LFB 是进城方向的快递追踪号（进城慢，先记下来），LSU 控制逻辑负责仲裁"谁先出城"（store buffer 排空策略）、检测"有没有人快递已经到了没登记"（store→load forwarding 地址比对）、防止"年轻人插队进城"（memory disambiguation）。

### 8.2 线程切换时，流水线里的"半成品"怎么办

线程切换（时钟中断触发内核抢占）的本质是：CPU 当前正在投机执行线程 A 的指令，突然要停下来去运行线程 B。问题的关键不在于"能不能停"，而在于**哪些状态算线程 A 的、哪些可以直接扔掉**。

答案藏在 ROB 的一个核心设计目标里：**精确中断（precise interrupt）**。中断发生时，CPU 必须能确定一个"中断发生点"（interrupt point），该点之前的所有指令**已完成**（架构状态已更新），该点之后的所有指令**似乎从未开始**（无架构状态残留）。

这对应的硬件动作是 — 中断信号到达后，CPU 做如下清理：

```bash
中断到达时的流水线快照（假设 ROB 里有 120 条 µop，head 在 #50，tail 在 #170）：

ROB:  [已退休 1..49] [① #50 当前head,待退休] [#51..#170 投机态]  ← ① 处的指令对应的 x86 指令边界被选为"中断点"
                  ↑──────────────────┬────────────────────↑
                             架构边界（精确中断点）             投机域（全部丢弃）
```

**各部件被清理的内容**：

| 部件 | 中断时的动作 | 丢弃的工作量 | 切回来要不要重做 |
|------|------------|------------|:--:|
| **ROB** | head 之前全部退休 → 中断点标记为退休边界 → head 之后的 µop 全部冲刷（flush） | 几十~两百条 µop | ✅ 全部重做 |
| **保留站（RS）** | 所有槽位清空（未发射的、正在等的、已发射等 WB 的） | 几十条 µop | ✅ 全部重做 |
| **Load Buffer** | 所有条目释放（未完成的 load 全部作废） | ~10 条 | ✅ 全部重做 |
| **LFB/MSHR** | 所有进行中的 L2 miss 请求继续完成（数据回来后发现 load 已被冲刷→丢弃） | 已经在飞行的 miss 请求无法撤回 | ❌ 白飞了 |
| **物理寄存器堆（PRF）** | 中断点之后分配的 PR 全部回收（归还空闲列表） | 几十~上百个 PR | ✅ 重新分配 |
| **RAT** | 恢复到中断点退休时刻的映射状态（ROB 保存了 checkpoint） | — | — |
| **Store Buffer** | ⚠️ **分两种情况，见下文** | 视情况 | 视情况 |
| **分支预测器** | 历史状态保留（不随中断清空） | — | — |
| **L1 I/D-Cache** | 内容保留（线程 A 的 cache 热数据对线程 B 可能有用） | — | — |

> **一句话**：ROB 的 head 指针就是"精确中断"的分界线——head 之前是已退休的（不可撤销），head 之后是投机态的（可以干净利落地一笔勾销）。中断处理程序只看到 head 之前那些指令的结果。

**Store Buffer 的特殊性——这是唯一"退休了但还没完"的状态**：

Store Buffer 和上面其他部件有一个根本区别：store µop **退休后**槽位还在 SB 里等 LSU 排空。其他部件（RS、LB、未退休 ROB 条目）在退休前都是投机态，可以随意丢弃。但已经退休的 SB 条目——线程 A 从程序视角已经认为"写完了"——如果 CPU 不做特殊处理就切到线程 B，线程 B 的 load 应该能看到线程 A 写的那些值吗？

**x86 的做法：中断前排空 Store Buffer**。x86 要求 store 在退休时对**本核后续 load** 可见（store→load forwarding），但中断/异常属于**序列化事件（serializing event）**。序列化事件发生前，CPU 必须：

1. ROB 排空到中断点（所有中断点前的指令退休）
2. **Store Buffer 全部排空到 L1 D-Cache**（所有已退休的 store 全局可见）
3. 冲刷中断点之后的所有投机状态

所以 store buffer 在中断时**不会丢弃已退休的内容**——反而要**加速排空**。这是中断惩罚的一部分：

```bash
中断到达

  ├── 中断点之前的 store µop：全部退休 → SB 槽位标记"可提交"
  │                           → LSU 立即仲裁排空（不等自然排空时机）
  │                           → 写入 L1 D-Cache
  │
  ├── 中断点之后的 store µop：SB 槽位释放（未退休 → 丢弃）
  │                           → 切回来重新执行
  │
  └── 中断点之后的所有其他 µop：ROB/RS/LB/LFB 全部冲刷
```

**量化：一次线程切换丢失了多少工作**

假设一个典型场景——线程 A 在 3 GHz CPU 上跑了 10 ms 后被抢占（时间片用完）。当时 ROB 里有 ~ 150 条 µop 在飞（Skylake ROB = 224 条目，保守估计 150 条 speculative），RS 里有 ~ 60 条 µop 在等/在执行：

| 丢弃的 | 数量 | 等价于 | 切回来重做的代价 |
|--------|------|--------|:--:|
| ROB 中未退休 µop | ~150 条 | ~40—50 条 x86 指令 | ~15—20 ns |
| RS 中等待/执行中 µop | ~60 条 | ~20 条 x86 指令 | ~7—10 ns |
| Load Buffer + LFB 飞行中 | ~5—10 条 miss | 已在 L2/L3 飞行的 miss 不可撤回 | 数据白取，浪费带宽 |
| Store Buffer 排空 | ~20—40 条 | 积压的 store 逐条写 L1 | ~10—30 ns |
| **合计浪费** | — | — | **~50—100 ns** |

> 10 ns 听起来不多，但考虑 10 ms 时间片里线程 A 执行了约 3000 万条指令——丢掉的 50 条指令才 0.00017%。**上下文切换真正的开销不在"重做"，而在 TLB/cache 污染**：线程 B 的页表切换导致 TLB flush（或 PCID 保护的 partial flush），线程 B 的代码和数据冷启动导致大量 cache miss——这比丢掉几十条 µop 的代价大几个数量级。

**Store Buffer 排空是上下文切换的一个隐性延迟来源**

特别值得注意的是，x86 的序列化事件强制排空 store buffer。如果线程 A 是一个"写入密集型"程序（比如 `memset` 循环），store buffer 里可能积压了 40+ 条已退休的 store 等待提交。中断触发时，LSU 必须逐条把这些 store 写入 L1 D-Cache——每条需要 L1 的 write port 空闲 + cache line 处于可写 coherence 状态（M/E 态）。如果恰好在写一个不在 L1 中的 cache line（L1 miss），还需要先发 RFO 从 L2/L3 拿 cache line——**这个排空过程可能长达几百拍**。

这也是为什么实时系统要避免在写入密集的关键路径上被抢占——不是怕"重做"，是怕 store buffer 排空引入不可预测的延迟抖动。

| 涉及部件 | 对应文档 | 本文展示了什么 |
|---------|---------|---------------|
| 译码（x86→µop） | [pipelining-superscalar.md](/concepts/microarch/pipelining-superscalar.md) §二 | 译码器如何把 `mov [rbx], eax` 切成 STA + STD |
| 寄存器重命名 | [register-renaming.md](/concepts/microarch/register-renaming.md) | RAT 查表、PR 分配、tmp→P45 的建立与 µop₂ 的读取 |
| 保留站 | [reservation-station.md](/concepts/microarch/reservation-station.md) | µop 在 RS 里等 tag 匹配、被调度器挑中发射 |
| 数据冒险 | [data-hazards.md](/concepts/microarch/data-hazards.md) | load→use RAW 依赖的 2 拍延迟，前推网络的旁路 |
| LSU / Store Buffer | [lsu.md](/concepts/microarch/lsu.md) | store buffer 生命周期、store→load forwarding、load buffer |
| ROB | [rob.md](/concepts/microarch/rob.md) | µop 按序退休、旧 PR 回收、store buffer 提交触发 |
| 乱序执行全景 | [cpu-out-of-order.md](/concepts/microarch/cpu-out-of-order.md) | 三条指令的乱序可能（µop 间独立→可并行）与限制（RAW 依赖→串行） |

## 十、一句话总结

> **`mov [rbx], eax` 拆成 STA+STD 两条无依赖 µop，可同拍发射、store buffer 异步提交；`add eax, [rsi+8]` 拆成 LD+ADD 两条有 RAW 依赖的 µop，load→use 至少 2 拍延迟；`addsd xmm0, [rdi+rcx*8]` 拆成 LD+ADDSD 两条跨集群 µop，FP 执行单元 4 拍流水线是延迟大头。每条 x86 指令穿过 IF→ID→REN→RS→EX→WB→RET 七级，重命名消除假依赖、保留站等真依赖、ROB 按序收口——这三个机制让"切碎的 µop"既能乱序并行又不破坏单核语义。**

