﻿# ROB（Reorder Buffer）—— 乱序执行的收口，顺序退休的物理基础

> 承 [cpu-out-of-order.md](/concepts/microarch/cpu-out-of-order.md) 的乱序执行五大部件和 [pipelining-superscalar.md](/concepts/microarch/pipelining-superscalar.md) §三.3.4 的 ROB 初步介绍。ROB 是"乱序执行"和"顺序语义"之间唯一的桥梁——所有 µop 执行结果汇入这里，经它确认后才对架构状态生效。

## 一、ROB 解决什么问题

乱序执行让指令不按程序序执行、不按程序序完成。但单核程序的语义要求：**对外看起来必须像顺序执行的。** 如果乱序执行完就直接写回寄存器文件，会出现：

```bash
① div rax, rbx     ; 除法，20+ 拍后才完成
② add rcx, rdx     ; 比① 先完成
如果② 直接写 rcx ← ① 除零异常 → ① 后面的② 先写回了！
→ 异常时刻 rcx 已经是② 的结果 → 程序状态错了 😱
```

ROB 做一件事：**所有完成的指令结果先暂存，按程序原序逐批提交到架构状态。** 程序序上排在前面的指令没退休，后面的即使算完了也不允许对外可见。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<ex>>  #FFF9C4
  BorderColor<<ex>>      #F9A825
  BackgroundColor<<rob>> #C8E6C9
  BorderColor<<rob>>     #388E3C
  BackgroundColor<<rf>>  #E3F2FD
  BorderColor<<rf>>      #1976D2
}
rectangle "执行单元\n(乱序完成)" <<ex>> as EX
rectangle "══════════ ROB 环形缓冲 ══════════\n┌────┬────┬────┬────┬────┬────┐\n│ I1 │ I2 │ I3 │ I4 │ I5 │ I6 │\n│ ✅ │ ✅ │ ✅ │ ⏳ │ ✅ │ ✅ │\n└────┴────┴────┴────┴────┴────┘\n     ←head                  tail→\nI1~I3 连续已完成 → 同拍全退\nI4 未完成 → 扫描停止\nI5~I6 虽完成但被 I4 挡住\n\n每拍退休宽度: 4~8 条 µop" <<rob>> as ROB
rectangle "物理寄存器文件\n(架构状态)" <<rf>> as RF
EX -down-> ROB : 执行结果写入各自 ROB 槽位
ROB -down-> RF : 退休:按程序序、逐批\n提交到架构状态
@enduml
```

## 二、功能拆解

ROB 是一个**环形 FIFO 队列**，内部每个槽位存一条在飞指令的完整信息：

| 功能 | 做什么 | 怎么实现 |
|------|--------|---------|
| **顺序退休** | 按程序序逐批提交结果到架构状态 | head 指针逐个扫描，碰到未完成即停，之前连续完成的全部提交 |
| **精确异常** | 异常发生时，异常指令之后的指令全未退休，架构状态停留在异常前 | 异常标记在 ROB 槽位上，退休到该条时触发异常处理，后面的槽位全部作废 |
| **分支误预测恢复** | 分支退休时确认预测，错了则作废后面所有投机指令 | 分支指令的 ROB 槽位标记"投机边界"，误预测 → 清空后面槽位，PC 跳回正确路径 |
| **物理寄存器回收** | 退休时释放该指令占用的物理寄存器 | 退休后，旧映射的物理寄存器归还空闲列表 |

> 每个 ROB 槽位存储的内容：µop 类型、目的物理寄存器号、结果值（或异常标志）、程序计数器 PC、是否为分支、分支预测方向。现代 CPU 的 ROB 大小：Apple M1 约 630 条目，Intel Golden Cove 512 条目，AMD Zen 4 320 条目。

## 三、运作机制

### 3.1 三阶段：分配 → 完成 → 退休

```bash
阶段             何时发生                 做了什么
─────────────────────────────────────────────────────────────────
分配(allocate)   指令从保留站发射时        在 ROB 尾部(tail)分配槽位，记录 µop 信息
                 同时分配 ROB 槽位          tail 指针后移
─────────────────────────────────────────────────────────────────
完成(complete)   执行单元算出结果后         结果值写入该 µop 对应的 ROB 槽位
                 结果写入 ROB 槽位          完成标志置位(ready bit = 1)
─────────────────────────────────────────────────────────────────
退休(retire)     该 µop 的槽位到达 head     head 连续扫描已完成槽位
                 且 head 之前全部已完成     批量提交到寄存器文件
                                           释放 ROB 槽位，head 指针前移
                                           释放旧物理寄存器
─────────────────────────────────────────────────────────────────
```

### 3.2 "保序"不等于"一次一条"——多端口并行退休

这是最容易混淆的概念。ROB 的"保序"约束的是**退休顺序不能乱**（I1 不能在 I2 之后退休），但 **不限制退休速率**。

下面用 ROB 环形缓冲区快照说明：

```bash
     head ─────────────────────────────────→ tail
    ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┐
    │  I1  │  I2  │  I3  │  I4  │  I5  │  I6  │  I7  │
    │  ✅  │  ✅  │  ✅  │  ⏳  │  ✅  │  ✅  │  ⏳  │
    └──────┴──────┴──────┴──────┴──────┴──────┴──────┘
                     ↑               ↑
             连续完成，本拍全退    虽完成，被 I4 挡住，不能退
```

本拍退休判决：
- I1 ✅ → 退
- I2 ✅ → 退
- I3 ✅ → 退
- I4 ⏳ → **碰到未完成，扫描停止**
- I5、I6 虽已完成，但**被 I4 挡住**（保序：不能跳过未完成指令）

**本拍退休 3 条（IPC=3），顺序 I1→I2→I3，保序约束完全满足。**

关键两点：
- **并行写入**：16 个执行单元可以同一拍把算好的结果并行写入各自 ROB 槽位——每个槽位是物理独立的寄存器，互不阻塞。
- **批量提交**：退休逻辑每拍扫描 head 开始的连续已完成条目，一次性全部提交到寄存器文件。退休宽度（4~8 µop/拍）与前端发射宽度对称匹配。

### 3.3 对头阻塞（Head-of-Line Blocking）——ROB 的真正瓶颈

ROB 吞吐的实际限制不是"保序约束"本身，而是**对头阻塞**：只要 head 位置那条指令没完成，后面再多已完成的指令都出不来。

```bash
元凶：head 处一条 load miss（等 DDR 内存，~200 拍）
后果：ROB 里 500 条指令中 400 条已完成，但全被堵住
IPC  → 接近 0
```

缓解手段：
- **增大 ROB 容量**（200→500+ 条目）：让更多指令在等待期间被发射和执行，提高"head 解堵时后面能一次性退休多少条"的吞吐。
- **投机执行**：分支预测正确的前提下，继续往后取指→译码→发射，让 ROB 在等待时也没闲着。
- **预取（prefetch）**：先用 `prefetch` 指令提前把数据拉到 cache，减少 load miss 概率。

### 3.4 精确异常：为什么异常点能精确到指令级

假设程序序为 I1→I2→**I3（除零）**→I4→I5，乱序执行中 I4、I5 先于 I3 完成了：

```bash
ROB 槽位状态：
I1: ✅完成
I2: ✅完成
I3: 💥除零异常（标记）
I4: ✅完成（投机）
I5: ✅完成（投机）
I6: ⏳执行中（投机）
```

退休扫描到 I3 时发现异常标记 → **立即停止退休**。I4、I5、I6 从 ROB 全部清空（作废），架构状态回退到 I2 退休完毕的时刻。然后 CPU 跳转到除零异常处理程序。**从程序角度看，I3 正好卡在退休前，I4/I5/I6 就像从没有过一样**——这就是"精确异常"。

分支误预测的恢复流程完全一样——分支指令退休时确认预测，错了就把投机指令从 ROB 全部清空。

## 四、限制与瓶颈

| 限制 | 说明 | 表现 |
|------|------|------|
| **对头阻塞** | head 处指令（尤其是 load miss）堵住整个 ROB | IPC 骤降，后端有大量已完成指令出不去 |
| **ROB 容量耗尽** | ROB 满了（tail = head-1），前端停发射 | `stalled-cycles-backend` 飙升，取指/译码停摆 |
| **退休宽度** | 每拍最多 4~8 条 µop，与发射宽度对称 | 正常情况不构成瓶颈，除非前面累计了大量已完成 µop 等待集中退休 |
| **投机深度** | ROB 越大投机越深，误预测时清空的也越多 | 功耗浪费 + 误预测惩罚（清空大 ROB 需要多拍） |

## 五、和周边部件的关系

```bash
保留站 ──→ 执行单元 ──→ ROB ──→ 物理寄存器文件（架构状态）
  ↑                        ↑
  └── 寄存器重命名 ────────┘
      (分配物理寄存器)       (释放旧物理寄存器)
```

- **保留站**：负责乱序发射（"谁先走"），ROB 负责顺序收口（"谁先退"）——详见 [reservation-station.md](/concepts/microarch/reservation-station.md)。
- **寄存器重命名**：分配物理寄存器时在 RAT 中建立新映射；ROB 退休时把旧映射的物理寄存器归还空闲列表——详见 [register-renaming.md](/concepts/microarch/register-renaming.md)。
- **分支预测**：分支指令在 ROB 中退休时才确认预测正确性——详见 [branch-prediction.md](/concepts/microarch/branch-prediction.md)。
- **LSU**：load miss 是对头阻塞的头号元凶——详见 [lsu.md](/concepts/microarch/lsu.md)。
- **数据冒险**：[data-hazards.md](/concepts/microarch/data-hazards.md)——ROB head 处的 load→use RAW（L1 miss）堵死整个 ROB，IPC 骤降至接近 0，这是数据冒险最恶劣的表现形式。
- **纵切面走读**：[uop-pipeline-walkthrough.md](/concepts/microarch/uop-pipeline-walkthrough.md)——ROB 在完整流水线中的分配/完成/退休时序，包含 SB 异步排空的交互。

## 六、观测：ROB 相关的 perf 事件

```bash
perf stat -e uops_retired.slots,resource_stalls.rob ./app
# uops_retired.slots — 退休槽位利用率（退休带宽的实际使用率，接近 4~8 说明 ROB 不堵）
# resource_stalls.rob  — 因 ROB 满导致前端停摆的周期数
```

- `resource_stalls.rob` 高 + IPC 低 → ROB 满了，大概率是 load miss 堵在 head。
- `uops_retired.slots` 远低于退休宽度（如 <1 而非 4~8）→ ROB 里有大量已完成指令但对头阻塞卡住了退休。

## 七、一句话总结

> **ROB 是乱序执行的"宪法"——规定"你可以乱序跑（保留站说了算），但必须按序报到（ROB 说了算）"。它把乱序的"乱"收口在核内，让单核程序完全感知不到——多端口并行写入 + 批量提交让退休不是串行瓶颈，真正的限制是对头阻塞：head 处一条慢指令堵住整条传送带。**

