ROB(Reorder Buffer)—— 乱序执行的收口,顺序退休的物理基础
承 cpu-out-of-order 的乱序执行五大部件(见 ooo-mechanism §二)和 superscalar-design §1.4 的 ROB 初步介绍。ROB 是"乱序执行"和"顺序语义"之间唯一的桥梁——所有 µop 执行结果汇入这里,经它确认后才对架构状态生效。
一、ROB 解决什么问题
乱序执行让指令不按程序序执行、不按程序序完成。但单核程序的语义要求:对外看起来必须像顺序执行的。 如果乱序执行完就直接写回寄存器文件,会出现:
① div rax, rbx ; 除法,20+ 拍后才完成
② add rcx, rdx ; 比① 先完成
如果② 直接写 rcx ← ① 除零异常 → ① 后面的② 先写回了!
→ 异常时刻 rcx 已经是② 的结果 → 程序状态错了 😱ROB 做一件事:所有完成的指令结果先暂存,按程序原序逐批提交到架构状态。 程序序上排在前面的指令没退休,后面的即使算完了也不允许对外可见。
二、功能拆解
ROB 是一个环形 FIFO 队列,内部每个槽位存一条在飞指令的完整信息:
| 功能 | 做什么 | 怎么实现 |
|---|---|---|
| 顺序退休 | 按程序序逐批提交结果到架构状态 | head 指针逐个扫描,碰到未完成即停,之前连续完成的全部提交 |
| 精确异常 | 异常发生时,异常指令之后的指令全未退休,架构状态停留在异常前 | 异常标记在 ROB 槽位上,退休到该条时触发异常处理,后面的槽位全部作废 |
| 分支误预测恢复 | 分支退休时确认预测,错了则作废后面所有投机指令 | 分支指令的 ROB 槽位标记"投机边界",误预测 → 清空后面槽位,PC 跳回正确路径 |
| 物理寄存器回收 | 退休时释放该指令占用的物理寄存器 | 退休后,旧映射的物理寄存器归还空闲列表 |
每个 ROB 槽位存储的内容:µop 类型、目的物理寄存器号、结果值(或异常标志)、程序计数器 PC、是否为分支、分支预测方向。现代 CPU 的 ROB 大小:Apple M1 约 630 条目,Intel Golden Cove 512 条目,AMD Zen 4 320 条目。
三、运作机制
3.1 三阶段:分配 → 完成 → 退休
阶段 何时发生 做了什么
─────────────────────────────────────────────────────────────────
分配(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 环形缓冲区快照说明:
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 位置那条指令没完成,后面再多已完成的指令都出不来。
元凶: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 完成了:
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 需要多拍) |
五、和周边部件的关系
保留站 ──→ 执行单元 ──→ ROB ──→ 物理寄存器文件(架构状态)
↑ ↑
└── 寄存器重命名 ────────┘
(分配物理寄存器) (释放旧物理寄存器)- 保留站:负责乱序发射("谁先走"),ROB 负责顺序收口("谁先退")——详见 reservation-station。
- 寄存器重命名:分配物理寄存器时在 RAT 中建立新映射;ROB 退休时把旧映射的物理寄存器归还空闲列表——详见 register-renaming。
- 分支预测:分支指令在 ROB 中退休时才确认预测正确性——详见 branch-prediction。
- LSU:load miss 是对头阻塞的头号元凶——详见 lsu。
- 数据冒险:data-hazards——ROB head 处的 load→use RAW(L1 miss)堵死整个 ROB,IPC 骤降至接近 0,这是数据冒险最恶劣的表现形式。
- 纵切面走读:uop-pipeline-walkthrough——ROB 在完整流水线中的分配/完成/退休时序,包含 SB 异步排空的交互。
六、观测:ROB 相关的 perf 事件
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 处一条慢指令堵住整条传送带。