寄存器重命名与顺序退休:并行度怎么挖出来、正确性怎么收住
承乱序执行总纲 cpu-out-of-order,五大部件框架见 ooo-mechanism。本篇讲乱序执行的两个关键部件:寄存器重命名(§三)负责挖并行——把"名字冲突"的假依赖抹掉,让更多指令能乱序;顺序退休(§四)负责收正确性——乱序算完的结果必须按程序序对外生效,这是"单核完全察觉不到乱序"的根本原因。
一、寄存器重命名:为什么能挖出更多并行
乱序的自由度常被"假依赖"限制。看这段:
① add rax, rbx ; 写 rax
② mov [mem], rax ; 读 rax
③ add rax, rcx ; 又写 rax ← 和① 抢同一个 rax 名字(WAW),和② 有 WAR③ 和① 逻辑上不相关,却因为都叫 rax 而不能乱序(这叫名字依赖/假依赖,不是真的数据依赖)。寄存器重命名把它们映射到不同物理寄存器:

- 真依赖(RAW,读后写的数据流) 无法消除,必须等——这是乱序的硬边界。
- 假依赖(WAR/WAW,只是名字冲突) 靠重命名消除——这是乱序能挖出并行的关键。
现代 x86 架构寄存器只有 16 个通用寄存器,但物理寄存器有几百个。重命名让"名字不够用"不再限制并行度,是乱序执行威力的放大器。
重命名消除假依赖的完整机制(寄存器重命名表 RAT、物理寄存器池 PRF、回收时机与分支恢复的细节)见 register-renaming——它是乱序执行真正能挖出并行度的前提:没有它,保留站里大量 µop 明明操作数都齐了,却被"名字冲突"卡住发不出。
二、顺序退休:单核为什么完全察觉不到乱序
这是理解"乱序执行 ≠ 内存重排"的关键。指令可以乱序执行,但必须按程序序退休(retire/commit)——即"对外生效、写进架构状态、可被观测"这一步严格按原顺序。

- 投机执行:遇到分支还没算出方向时,CPU 会猜一条路先执行;猜错就把 ROB 里这批投机指令全部作废(像没发生过)。所以乱序 + 投机也不会污染架构状态。这个"猜"由分支预测器负责,猜得准不准直接决定性能——详见 branch-prediction。
- 精确异常:靠顺序退休,异常/中断发生时架构状态恰好停在"出错指令之前全退休、之后全没生效",和顺序执行的语义一致。
- 结论:单核程序无论如何都察觉不到自己被乱序执行了——结果和顺序一致,异常点也一致。乱序纯粹是"偷偷加速",语义零改变。
这正是乱序执行和内存重排的分水岭:乱序执行的乱序被 ROB 顺序退休收口在核内,单核透明;而内存重排是退休之后、写还堵在 store buffer 里没对别的核可见——那已经不是乱序执行的范畴了。
为什么不能"执行完就生效"? 有人会问:结果算完直接写架构寄存器不就完了,何必非要等退休?答案是精确异常:如果指令② 在① 之前算完就生效,而① 中途抛了异常(比如 load 到非法地址),此时架构状态已经写入了② 的结果——但按程序序,② 根本不该执行。顺序退休保证"异常发生时,出错指令之前全部生效、之后全部未生效",这是调试器和信号处理能工作的前提。没有它,异常处理程序看到的寄存器状态将是一锅粥。
ROB 的完整机制(保序退休、多端口并行退休、对头阻塞、精确异常、分支误预测恢复)见 rob。
三、重命名与 ROB 的协同:物理寄存器的完整一生
重命名和顺序退休不是两个孤立的机制,它们靠物理寄存器池咬合在一起。一条指令的"暂存格"是这样流转的:
- 分配:重命名时从 free list 拿一个物理寄存器(比如 P41)映射给架构寄存器(rax),记录进 RAT;
- 写值:执行完成后把结果写进 P41(PRF,物理寄存器文件);
- 等待退休:P41 里的值暂存着——即使后面指令已经把 rax 改成新映射,P41 的旧值也不能丢,因为还没退休的指令可能还要读它;
- 释放:等这条指令按序退休、且所有更老的引用它的指令都退休了,P41 才回到 free list 被复用。
回收时机是重命名的隐藏难点:物理寄存器不能"指令一退休就释放",还要等所有引用它的指令都过掉(它们的 ROB 条目都退休)——这个"延迟释放"决定了物理寄存器池需要几百个而不是几十个。池子不够,重命名就得停,乱序并行度直接受限。
分支误预测时的回滚也靠这套结构:重命名每拍维护 RAT 的 checkpoint,猜错时整批投机映射作废、RAT 恢复到 checkpoint 那一拍——所以"误预测恢复"本质上是"重命名状态的回滚"。
四、顺序退休的代价:为什么 ROB 不能无限宽
顺序退休保证了正确性,但它是单点瓶颈:
- 退休带宽有限:Skylake 每拍最多退休 4 条。即使执行端每拍跑满 6~8 条,对外生效仍是 4 条/拍封顶——IPC 不可能超过退休带宽。
- 对头阻塞:ROB 是按序退休的队列,队头的指令没完成(比如一条 load miss),后面的指令算完了也得排队——"队头堵住,后面全等"。这正是 §二 序列图里"① load miss、②③ 先算完也不能先生效"的原因。
- 指令集差异:x86 的复杂指令常被拆成多个 µop,退休按 µop 粒度处理;RISC 大多 1 条指令 1 个 µop,退休管理更简单。
这就是"挖并行"和"收正确性"的完整配合:重命名把并行度挖出来(假依赖消失),ROB 把并行度限制在安全范围内(按序退休),物理寄存器池在两者之间流转——池子越大并行越高,回收越谨慎正确性越稳。 RAT/PRF 的硬件细节见 register-renaming,ROB 结构见 rob。
一个常见误解:重命名 ≠ 消除所有依赖
重命名只消除名字冲突(WAR/WAW),不消除数据流依赖(RAW)。前者是"换名字",后者是"等结果"。看三条指令:
① add rax, rbx
② add rax, rcx ; 与① WAW——重命名后各写各的物理寄存器,可并行
③ add rdx, rax ; 与② RAW——必须等② 的新值② 和① 能并行,是因为它们只是"都叫 rax";③ 和② 必须串行,是因为③ 真的要用② 的结果。判断一条依赖能不能被重命名消除:把名字换成编号,编号之间还连得上就是真依赖,连不上就是假依赖。
五、一句话总结
寄存器重命名负责"挖并行":把 WAR/WAW 这类只因为共用寄存器名字的假依赖映射到不同物理寄存器,假依赖消失、指令就能并行乱序——真依赖(RAW)是乱序的硬边界,重命名消不掉它,只能等。顺序退休负责"收正确性":乱序执行完的结果全部暂存在 ROB,按程序原顺序对外生效,再配合投机执行的"猜错全作废"和精确异常的"停在出错点",单核程序无论如何都察觉不到自己被乱序了——乱序纯粹是偷偷加速,语义零改变。这两者一个是乱序能跑多快(挖并行)、一个是乱序有多安全(收口核内),是理解"乱序执行 ≠ 内存重排"的两块基石:乱序的乱序在退休时被收口在核内(单核透明),内存重排发生在退休之后的缓冲区层(跨核可见)。