﻿# 寄存器重命名（Register Renaming）—— 消除假依赖，为保留站挖出 ILP

> 承 [cpu-out-of-order.md](/concepts/microarch/cpu-out-of-order.md) §三的寄存器重命名初步分析。寄存器重命名是乱序执行**真正能挖出并行度的前提**——没有它，保留站里大量 µop 明明操作数都齐了，却被"名字冲突"卡住发不出。它是"小寄存器集→大物理寄存器池"的映射引擎。

## 一、重命名解决什么问题

x86 只有 16 个通用寄存器（32 位模式 8 个，64 位模式 16 个）。但程序写了三行都用同一个 `rax`，并不意味着这三条指令真的互相依赖——很多时候只是"寄存器不够用，复用一下名字"。

```asm
① add rax, rbx      ; rax ← rbx + 旧rax       真依赖（读旧rax的值）
② mov [mem], rax    ; 读rax                    真依赖（读①的结果）
③ add rax, rcx      ; rax ← rcx + 旧rax？？    假依赖！
```

③ 的 `rax` 跟① 的 `rax` 是两码事——③ 没必要等① 算完，它只需要 `rcx` 的值，`rax` 只是碰巧复用了同一个名字。但因为都叫 `rax`，在寄存器层面看起来像② 写完 `rax` 之后③ 才能写（WAW 假依赖），③ 读取 `rax` 要等② 写完（WAR 假依赖）。

没有重命名的情况下，③ 被这两层假依赖卡住，不能发射。而③ 身边可能有几十条跟它无关的指令——保留站里堆满"就绪但不能发"的 µop。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<a>> #FFEBEE
  BorderColor<<a>>     #C62828
  BackgroundColor<<p>> #C8E6C9
  BorderColor<<p>>     #388E3C
}
rectangle "架构寄存器(程序看到的)\n16 个通用寄存器\nrax/rbx/rcx/rdx/..." <<a>> as ARCH
rectangle "物理寄存器(硬件真有几百个)\n①的 rax → PR37\n②读 PR37 → 等① 做完\n③的 rax → PR52 ← 不等任何 rax" <<p>> as PHYS
ARCH -down-> PHYS : 重命名表(RAT)\n一个架构寄存器 → 最新的物理寄存器
note bottom of PHYS : ① 和 ③ 各写各的物理寄存器\n假依赖(WAR/WAW)消失\n→ ③ 不依赖①,可以并行执行
@enduml
```

三种依赖中：
- **RAW（真依赖，读后写）**：② 必须等① 的结果 → 不可消除，硬边界。保留站里的 µop 必须等 tag 匹配才发射。
- **WAW（假依赖，写后写）**：③ 和① 都写 `rax`，但③ 的结果与① 无关 → **重命名消除**：各分配不同物理寄存器。
- **WAR（假依赖，读后写）**：③ 写 `rax` 前，② 必须先读完 `rax` → **重命名消除**：③ 写的是 PR52，不影响② 读 PR37。

> 现代 x86 CPU 有 ~180（Zen 4）到 ~280（Golden Cove）个整数物理寄存器，以及类似数量的浮点/SIMD 物理寄存器。16 个架构寄存器 × 6 个 µop/拍的发包速率，理论上不够一拍的发射量——重命名就是把"名字瓶颈"彻底打掉。

## 二、功能拆解

| 部件 | 做什么 | 物理形态 |
|------|--------|---------|
| **RAT（寄存器别名表）** | 记录"架构寄存器 → 最新物理寄存器"的当前映射 | SRAM 查找表，每条指令都得查 |
| **物理寄存器文件** | 存储所有物理寄存器的实际值 | 几百个物理寄存器条目，每个存 64 位值 |
| **空闲列表（Free List）** | 记录当前未被分配的物理寄存器号 | 简单的 FIFO/计数器 |

## 三、运作机制：一条指令走完重命名的全流程

```bash
阶段             操作                             RAT 状态变化
─────────────────────────────────────────────────────────────────
① 进入重命名      指令从译码级进入重命名级          查 RAT：rax→PR37, rcx→PR42
─────────────────────────────────────────────────────────────────
② 读源操作数      读取"最新的物理寄存器号"          源 PR = RAT[rax]=PR37, RAT[rcx]=PR42
                  src1 = PR37 (rax的值)
                  src2 = PR42 (rcx的值)
─────────────────────────────────────────────────────────────────
③ 分配目的寄存器  从空闲列表取一个新的物理寄存器    空闲列表弹出 PR58
                  目的操作数的值将写入 PR58
─────────────────────────────────────────────────────────────────
④ 更新 RAT        目的架构寄存器映射到新物理寄存器    RAT[rax] = PR58（覆盖旧映射）
                  旧映射 PR37 不立即释放
─────────────────────────────────────────────────────────────────
⑤ 保留旧映射      旧映射 PR37 记在 ROB 槽位上       ROB 记录：退休时释放 PR37
                  退休后归还空闲列表
─────────────────────────────────────────────────────────────────
⑥ 进入保留站      µop 携带源 PR tag + 目的 PR tag   保留站里监控 PR37 的就绪状态
                  进入保留站等待操作数就绪
─────────────────────────────────────────────────────────────────
```

### 3.1 RAT 查找：每次重命名都要查

RAT 是一个快速 SRAM 表，16 个架构寄存器号 → 各自的"最新物理寄存器号"。每条指令译码后，它的源架构寄存器号 → 查 RAT → 得到物理寄存器号（源 tag）。每拍 4~6 条 µop 可能需要查 8~10 个源寄存器，RAT 有多读端口支持并行查找。

### 3.2 旧映射的延迟回收：为什么必须等"退休"

③ 更新 RAT 后，旧映射 PR37 不能立即归还空闲列表——因为**可能还有以前的指令在引用 PR37**。比如：

```asm
① add rax, rbx     → rax 映射到 PR37
② mov rcx, rax     → 源操作数引用 PR37，此刻 RAT[rax]=PR37
③ add rax, rdx     → rax 映射更新为 PR52（覆盖 RAT[rax]）
```

③ 更新 RAT 时，② 还在流水线里、还没退休，② 必须读 PR37 的值。如果 PR37 被立即回收并被④ 分配出去、写入了新值，② 就会读到错误的数据。

所以旧映射的回收必须**延迟到② 退休之后**——这是 ROB 的职责：退休时，ROB 把该指令的旧物理寄存器号归还空闲列表。

### 3.3 分支误预测时的 RAT 恢复

分支指令后的所有指令都可能基于错误预测发射。当分支在 ROB 退休时发现预测错了，不仅 ROB 后面的投机指令要清空，**RAT 也要恢复**到分支指令之前的状态。

现代 CPU 用 **checkpoint（检查点）** 机制：每次遇到分支时，拍一个 RAT 的快照。误预测后，恢复到该快照——不需要逐条回滚，O(1) 恢复。

## 四、限制与瓶颈

| 限制 | 说明 | 表现 |
|------|------|------|
| **物理寄存器耗尽** | 空闲列表空了，新指令分配不到目的物理寄存器，前端停摆 | 程序里大量使用不同寄存器（编译器过度展开循环、大量变量生命期重叠） |
| **RAT 查表端口** | 每拍 4~6 µop 需要同时查 8~10 个源寄存器的映射 | 很少作为瓶颈，RAT 多端口设计能匹配前端带宽 |
| **重命名宽度** | 每拍最多重命名 4~6 条 µop（与译码宽度一致），因为分配物理寄存器、更新 RAT 都需要 1 拍内完成 | 前端带宽就是上限，硬件已匹配 |
| **分支误预测后的 RAT 恢复** | checkpoint 数量有限，嵌套分支过深时无法全部保存快照 | 回退到更早的 checkpoint 或重新从零填充 |

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

```bash
译码 ──→ 寄存器重命名 ──→ 保留站 ──→ 执行单元 ──→ ROB ──→ 退休
              │              ↑                         │
              └── 分配 PR ───┘(监控这PR的就绪状态)      └── 释放旧 PR
```

- **保留站**：重命名后 µop 携带源/目的物理寄存器 tag，进入保留站监控 tag 就绪状态。没有重命名，保留站里一组 µop 被假依赖卡死。详见 [reservation-station.md](/concepts/microarch/reservation-station.md)。
- **ROB**：退休时释放该指令"覆盖了的旧物理寄存器"，归还空闲列表。分支机构还需要 ROB 触发 RAT checkpoint 恢复。详见 [rob.md](/concepts/microarch/rob.md)。
- **分支预测**：每次分支生成一个 RAT checkpoint，误预测后用于快照恢复。详见 [branch-prediction.md](/concepts/microarch/branch-prediction.md)。
- **数据冒险**：重命名消除的是 WAR/WAW 假依赖；RAW 真依赖不可消除，需要转发网络和乱序执行来掩盖。详见 [data-hazards.md](/concepts/microarch/data-hazards.md)。
- **纵切面走读**：[uop-pipeline-walkthrough.md](/concepts/microarch/uop-pipeline-walkthrough.md)——重命名在完整流水线中的位置与 RAT 检查点恢复的时序。

## 六、观测：重命名相关的 perf 事件

```bash
perf stat -e uops_issued.any,uops_retired.any,resource_stalls.any ./app
# uops_issued.any — 发射的 µop 总数
# uops_retired.any — 退休的 µop 总数
```

重命名本身没有直接的 perf 计数器（它嵌在译码→发射的中间路径上），但可以通过间接指标判断：
- 大量 `nop`（`mov rax, rax`）指令 → 可能因为架构寄存器不够，编译器插入了无用的"改名"指令。重命名让这些 nop 变成零延迟的 register move elimination。
- `uops_retired` / `uops_issued` 比值低 → 大量 µop 发射了但没退休（投机路径错误被清空），RAT checkpoint 恢复频率高。

## 七、一句话总结

> **寄存器重命名是保留站的"前置放大器"——它把 16 个架构寄存器的名字瓶颈打掉，映射到几百个物理寄存器，让 WAR/WAW 假依赖消失，为保留站准备更多互不依赖的 µop。没有重命名，保留站里一半 µop 会因为名字冲突卡住，乱序执行基本废掉。RAT 的旧映射回收必须等 ROB 退休、分支误预测靠 checkpoint 机制快速恢复——这三个机制构成一个完整的"物理寄存器生命周期管理"闭环。**

