寄存器重命名的原理
硬件描述 1 — 历史时刻——从4个寄存器到无限并行
1966年,Robert Tomasulo在IBM System/360 Model 91上面临一个看似无解的矛盾:ISA只定义了4个浮点寄存器,但流水线设计要求同时容纳多条浮点指令并行执行。4个名字远远不够——指令之间因为"名字冲突"而被迫串行等待。Tomasulo的方案:给每条指令的目的操作数分配一个唯一的内部标签(tag),不再受限于ISA寄存器名字的数量。这个看似简单的洞察——假依赖纯粹是名字冲突,与数据流无关——在近60年后仍然是每一颗乱序处理器的基石。从IBM 360/91的3个保留站,到Apple M4的600+物理寄存器,重命名技术的规模增长了200倍,但核心思想从未改变。
设计提示
统一视角。处理器设计的本质是在有限的晶体管预算和功耗约束下,通过投机(speculation)和并行(parallelism)的层层叠加来逼近指令吞吐率的理论上限。寄存器重命名正是释放指令级并行性(ILP)的关键一步——它消除WAR和WAW假依赖,使得原本被寄存器名字"串行化"的指令可以真正并行执行。重命名后,程序的执行顺序仅由真正的数据流(RAW依赖)决定,IPC上限由关键路径上的真依赖链长度决定(回顾第 1.0 章中IPC理论上限的讨论)。
乱序执行(Out-of-Order Execution,OoOE)是现代高性能处理器的核心技术,而寄存器重命名(Register Renaming)则是乱序执行引擎的基石。没有寄存器重命名,乱序执行就无法有效运作——大量的假相关(false dependence)将把本可并行执行的指令强行序列化,使得乱序引擎的发射窗口和执行资源被白白浪费。从1967年Robert Tomasulo在IBM System/360 Model 91上首次提出通过保留站(reservation station)隐式实现寄存器重命名,到今天ARM Cortex-X925拥有384个整数物理寄存器、Intel Lion Cove拥有280个以上物理寄存器的设计,寄存器重命名技术已经经历了近60年的演进,但其核心思想始终未变:将程序可见的有限逻辑寄存器映射到微架构中更大规模的物理寄存器,从而消除因寄存器名字不足而产生的假相关。
本章作为第五篇"乱序执行引擎"的开篇,将从数据相关性的基本概念出发,深入分析寄存器重命名的三种主流实现方式——使用ROB存储结果、扩展ARF以及使用统一的物理寄存器文件(PRF)。对于每种方式,我们不仅讨论其原理,还将深入到硬件实现层面的具体细节:映射表的组织与电路实现、物理寄存器的分配与释放时序及正确性证明、读端口和写端口的需求分析与面积量化,以及Intel P6、MIPS R10000、Intel Golden Cove、香山昆明湖等真实处理器中的具体设计选择。此外,本章还将讨论Move消除、零惯用语消除等在重命名阶段完成的指令优化技术,以及它们所需的引用计数机制。
数据相关性回顾
在顺序执行的处理器中,所有指令按照程序顺序逐条执行,每条指令在前一条完成后才开始,因此数据相关性不会造成任何困难——它自然而然地被满足了。然而,一旦处理器试图同时执行多条指令(超标量)或者以不同于程序顺序的次序执行指令(乱序),数据相关性就成为必须仔细处理的核心问题。
考虑两条指令和(在程序顺序中先于),如果它们访问了同一个寄存器,则可能存在三种数据相关性。
真相关(RAW)
真相关(True Dependence),也称为写后读(Read-After-Write,RAW)相关,是三种数据相关性中唯一不可消除的。当指令需要读取指令的计算结果时,必须等待完成执行并产生结果后才能开始执行——这是程序语义本身决定的约束,任何微架构技巧都无法绕过。
add x1, x2, x3 # I1: x1 = x2 + x3
sub x4, x1, x5 # I2: x4 = x1 - x5 (RAW: 读x1, 依赖I1)
and x6, x1, x7 # I3: x6 = x1 & x7 (RAW: 读x1, 依赖I1)在上述代码中,指令I2和I3都需要读取I1写入x1的值。在一个典型的超标量流水线中,如果I1需要1个周期执行(ALU操作),那么I2和I3最早只能在I1执行完成的下一个周期开始执行。如果没有旁路(bypass/forwarding)网络,它们还需要等待I1的结果写回寄存器文件,延迟更长。
扇出(Fan-out)与扇入(Fan-in)
真相关的拓扑结构可以从扇出和扇入两个维度来刻画,它们对性能的影响截然不同。
扇出(fan-out)指一条指令的结果被多条后续指令使用的情况。在代码 lst:ch24-raw中,I1的结果x1同时被I2和I3使用,I1的扇出度为2。更极端的例子出现在基地址计算中:
lui x10, %hi(array) # I1: x10 = 基地址高20位
addi x10, x10, %lo(array) # I2: x10 = 完整基地址 (依赖I1)
lw x11, 0(x10) # I3: 读array[0] (依赖I2)
lw x12, 4(x10) # I4: 读array[1] (依赖I2)
lw x13, 8(x10) # I5: 读array[2] (依赖I2)
lw x14, 12(x10) # I6: 读array[3] (依赖I2)
lw x15, 16(x10) # I7: 读array[4] (依赖I2)
lw x16, 20(x10) # I8: 读array[5] (依赖I2)I2的扇出度为6(I3–I8都依赖I2的结果)。从关键路径的角度来看,扇出不增加关键路径长度——I3到I8可以在I2完成后的同一周期通过旁路网络获取基地址值,并同时开始地址计算。然而,高扇出对旁路网络的物理实现构成挑战:在一个6-wide处理器中,一个执行单元的输出可能需要同时转发给其他5个执行单元的输入端口,旁路多路选择器的扇入因此增大,导致选择器延迟增加。在物理设计中,旁路线的负载电容与扇出度成正比,这可能成为关键路径的时序瓶颈。
扇入(fan-in)指一条指令依赖多个前驱指令结果的情况。大多数ALU指令的扇入度为2(两个源操作数),而某些指令(如条件移动CMOV或融合乘加FMA)的扇入度为3。扇入也不增加关键路径长度——指令只需等待其最慢的源操作数就绪即可开始执行。形式化地说,设指令依赖前驱指令和,且在周期产生结果、在周期产生结果,则最早可在周期开始执行(假设1周期旁路延迟)。
真正决定关键路径的是链式依赖(chain dependence)——每条指令只有一个输出被下一条指令的输入使用,形成首尾相连的串行链。在整数代码中,这种链式依赖通常出现在以下场景:
地址计算链(pointer-chasing):
LDADDLDADD,常见于链表遍历和指针追踪。由于Load指令的延迟通常为4–5周期(L1 cache命中),这类链的关键路径增长极快。例如,遍历一个链表节点node->next->next->next需要个周期的最小延迟(每次Load 4周期地址计算1周期),即使乱序引擎有300条指令的窗口也无法加速。循环归纳变量更新:
ADDI x1, x1, 4每次迭代产生新的循环变量值,下一次迭代的所有依赖于循环变量的指令都必须等待。这类链的延迟为每次迭代1个周期(ALU延迟),相对较短。条件标志链:在x86中,
CMPCMOVCMPCMOV形成的标志位依赖链。RISC-V没有条件标志寄存器,因此不存在此类问题,这是RISC-V在微架构实现上的一个设计优势。长延迟运算链:整数除法(
DIV,20–80周期延迟)或浮点除法/开方形成的链。虽然这类链不常见,但一旦出现,其延迟主导了整个代码段的关键路径。
依赖链与关键路径
真相关形成的依赖链(dependence chain)决定了程序执行的关键路径(critical path)。考虑以下代码:
mul x1, x2, x3 # I1: x1 = x2 * x3 (延迟3周期)
add x4, x1, x5 # I2: x4 = x1 + x5 (依赖I1, 延迟1周期)
mul x6, x4, x7 # I3: x6 = x4 * x7 (依赖I2, 延迟3周期)
sub x8, x6, x9 # I4: x8 = x6 - x9 (依赖I3, 延迟1周期)这四条指令形成了一条串行依赖链:I1I2I3I4。即使处理器拥有8-wide的发射宽度和无限多的执行单元,这条依赖链的最小执行时间仍然是个周期。假设同一代码片段中还有其他独立指令可以填充这8个周期的空闲发射槽,那么乱序执行引擎就可以发挥作用;但如果程序中大部分指令都在同一条依赖链上,IPC将受到严重限制。
性能分析 1 — 依赖链长度对IPC的影响
依赖链的长度直接决定了乱序执行引擎能够从程序中提取的指令级并行性(ILP)的上限。定义关键路径长度为从窗口中第一条指令到最后一条指令之间最长依赖链的延迟总和(以时钟周期计),窗口大小为条指令,则IPC的理论上限为:
| 关键路径 (周期) | 典型场景 | |
|---|---|---|
| 32 | 8.0 | 独立循环迭代,丰富ILP |
| 64 | 4.0 | 中等依赖链,如矩阵运算 |
| 128 | 2.0 | 长依赖链,如链表遍历 |
| 256 | 1.0 | 全串行依赖,如长精度整数运算 |
在实际处理器中,SPEC CPU 2017整数基准的典型依赖链长度约为40–80个周期(在一个256项窗口中),对应理论约3–6。这与实测的IPC范围(3–5)大致吻合,说明真相关形成的关键路径确实是IPC的主要瓶颈之一。
真相关无法通过微架构手段消除,但可以通过两种方式缓解其影响:(1)旁路网络(bypass network)使得产生结果的指令无需将结果写回寄存器文件后续指令就可以读取,将数据传递延迟从"执行+写回"缩短为"执行";(2)更大的指令窗口使乱序引擎可以跨越更长的依赖链,从更远的程序位置发现独立指令来填充流水线气泡。
假相关(WAR和WAW)
与真相关不同,假相关(False Dependence)并非由数据流的真实需求产生,而是由寄存器名字的重复使用导致的人为约束。假相关包括两种类型。
(1)写后写相关(WAW)。指令的目标寄存器与的目标寄存器相同。在顺序执行中,必须在之后写入,以确保最终寄存器中保存的是的结果。但实际上两条指令的计算之间可能完全没有数据依赖。
mul x1, x2, x3 # I1: x1 = x2 * x3 (延迟3周期)
add x1, x4, x5 # I2: x1 = x4 + x5 (WAW: 写x1, 与I1冲突)I1和I2之间没有任何数据流关系——I2不使用I1的结果,I1也不使用I2的结果。两条指令唯一的关联在于它们碰巧都要将结果写入x1。如果I2(1周期ALU操作)先于I1(3周期乘法)完成执行,它必须等待I1先写回x1,然后自己才能写回——否则x1中最终保存的将是I1的结果而非I2的结果,违反了程序语义。这种等待完全是因为两条指令使用了同一个寄存器名字"x1"。
(2)读后写相关(WAR)。指令的目标寄存器是的源寄存器。必须等到读取了该寄存器的旧值之后才能写入新值。
add x4, x1, x5 # I1: x4 = x1 + x5 (读x1)
sub x1, x6, x7 # I2: x1 = x6 - x7 (WAR: 写x1, I1还未读x1)I2的计算完全不依赖I1的结果,但如果I2先于I1写入x1,则I1将读到错误的x1值(I2的结果而非I2之前x1的原始值)。同样,这个约束完全来自两条指令复用了寄存器名字"x1"。
假相关的根本原因:命名相关。假相关的本质是命名相关(name dependence)——ISA定义的逻辑寄存器数量有限(RISC-V有32个整数寄存器,x86-64有16个通用寄存器),编译器在进行寄存器分配时不得不重复使用这些有限的寄存器名字。在长的程序窗口中,寄存器名字的复用几乎不可避免。
要深入理解这一点,需要考虑编译器的寄存器分配过程。编译器使用图着色(graph coloring)算法将程序中的变量映射到有限的逻辑寄存器。当变量数量超过可用寄存器数量时(这在任何非平凡的函数中都会发生),编译器必须让不同生命周期(live range)不重叠的变量共享同一个寄存器。从编译器的角度看,这种共享是安全的——在任何程序点上,共享同一寄存器的两个变量不会同时活跃(live)。但从乱序处理器的角度看,这些变量对应的指令可能同时存在于指令窗口中,因为乱序引擎"看到"的指令窗口远大于编译器假设的顺序执行视角。
假相关对乱序窗口利用率的影响
假相关对程序性能的影响可以从两个层面理解。第一层面是直接的序列化效应:假相关迫使本可以并行的指令串行执行,降低了IPC。第二层面更为隐蔽——假相关导致乱序窗口的有效利用率下降。考虑一个256项的ROB,如果其中50%的指令被假相关强行序列化,那么乱序引擎实际能利用的并行窗口只有128条指令,等价于将ROB容量减半。
图图 24.2展示了四条指令之间所有的数据相关性。I1I2和I3I4的RAW相关是真相关,无法消除;但I1I3和I2I4的WAW相关,以及I2I3的WAR相关,都是因为寄存器x1和x4被重复使用而产生的假相关。在一个8-wide的乱序处理器中,这些假相关可能导致I3和I4不必要地等待,浪费宝贵的发射带宽。
数据流图(DFG)分析
假相关和真相关的区别可以通过数据流图(Data Flow Graph,DFG)来可视化。DFG中的每个节点代表一条指令,有向边代表数据依赖关系。在完整的DFG中(包含RAW、WAR、WAW所有依赖),节点之间的边数量很多,并行度受限。如果我们去除WAR和WAW边(即只保留RAW边),得到的DFG反映了程序的真实数据流——这就是寄存器重命名后处理器"看到"的依赖图。
对于上述四条指令的例子:
完整DFG包含5条边:I1I2(RAW)、I3I4(RAW)、I1I3(WAW)、I2I3(WAR)、I2I4(WAW)。这形成了一条从I1到I4的关键路径,长度为4步(I1I2I3I4)。
仅RAW的DFG包含2条边:I1I2和I3I4。这形成了两条独立的关键路径,每条长度为2步。两条路径可以完全并行执行,理论执行时间缩短一半。
DFG分析不仅有助于理解重命名的收益,还是现代编译器优化的基础工具。编译器使用DFG来进行指令调度——即使在顺序处理器上,通过重排指令顺序来减少流水线停顿。而乱序处理器的硬件调度器(发射队列)本质上就是在运行时构建和执行DFG的硬件。
ISA逻辑寄存器数量的影响。逻辑寄存器越少,假相关越严重。这可以从定量的角度来理解:在一个拥有条in-flight指令的乱序窗口中,约有的比例指令写入目标寄存器,每个逻辑寄存器平均被条指令的目标寄存器所"共享"(假设目标寄存器均匀分布)。这个"共享数"直接反映了假相关的严重程度——共享数越高,同一个逻辑寄存器名字被越多条in-flight指令争用,WAW和WAR假相关越多。
| ISA | 整数逻辑寄存器 | 有效可用 | 时 | 假相关严重程度 |
| 寄存器数 | 每寄存器平均共享数 | |||
| x86-32 | 8 | 6–7 | 37–43 | 极为严重 |
| x86-64 | 16 | 14–15 | 17–18 | 严重 |
| AArch64 | 31 | 28–30 | 8.5–9.1 | 中等 |
| RISC-V | 31 | 28–30 | 8.5–9.1 | 中等 |
| IA-64 (Itanium) | 128 | 120–125 | 2.0–2.1 | 轻微 |
不同ISA的逻辑寄存器数量及其对假相关的影响
表表 24.2显示,x86-32的仅8个逻辑寄存器导致每个寄存器平均被37条以上的in-flight指令共享——这是假相关的灾难。x86-64扩展到16个后情况改善但仍然严重。RISC-V和AArch64的31个可用寄存器将共享数降到约9个,但仍然产生大量假相关。有趣的是,Intel的IA-64(Itanium)ISA拥有128个通用寄存器,几乎可以消除所有假相关——但Itanium的失败表明,仅靠增加逻辑寄存器数量并不能取代硬件寄存器重命名带来的灵活性,因为编译器在编译时无法完美预测运行时的指令窗口和分支行为。
在x86-64从x86-32扩展的过程中,假相关的减少对实际IPC的影响约为10%–15%(在其他参数相同的情况下)。这个提升来自两个方面:(1)直接减少了假相关的数量;(2)编译器可以利用更多寄存器减少溢出/填充(spill/fill)——即将寄存器值暂存到栈内存中的操作。在x86-32的严重寄存器压力下,编译器频繁生成spill/fill指令,这些指令增加了内存访问压力和指令总数。x86-64的16个寄存器使spill/fill频率降低约40%,间接提升了IPC。
量化假相关的影响。为了直观理解假相关对性能的影响,可以建立一个概率分析模型。假设一个程序中每条指令的目标寄存器从个逻辑寄存器中均匀随机选取,乱序窗口中有条in-flight指令。
WAW假相关的概率分析。窗口中任意两条指令和()因为同一目标寄存器发生WAW假相关的概率为(两条指令碰巧选择了同一目标寄存器)。窗口中共有对这样的指令,因此WAW假相关的期望总数为:
WAR假相关的概率分析。WAR假相关发生在后面的指令写入前面指令读取的寄存器。假设每条指令有个源操作数(通常),则指令的目标寄存器与指令()的某个源操作数相同的概率为。窗口中WAR假相关的期望数为:
对于,WAR假相关的数量约为WAW假相关的2倍。将两者合计,窗口中总假相关的期望数约为:
| 逻辑寄存器数 | 期望WAW假相关数 | 代表ISA |
|---|---|---|
| 8 | 1016 | x86-32 |
| 16 | 508 | x86-64 |
| 32 | 254 | RISC-V, AArch64 |
| 64 | 127 | 假设扩展ISA |
不同逻辑寄存器数量下窗口内假相关的期望数量()
表表 24.3清楚地表明,即使拥有32个逻辑寄存器,在128条指令的窗口中也期望存在254个WAW假相关——平均每条指令约2个。这些假相关如果不被消除,将严重限制指令的并行发射。注意这只是WAW假相关,WAR假相关的数量通常更多(因为大多数指令都有2个源操作数),进一步加剧了序列化。
上述均匀随机模型给出了假相关数量的理论下界。在真实程序中,由于编译器倾向于频繁使用少数几个"热"寄存器(如x10–x17用于函数参数、x1用于返回地址),寄存器使用分布远非均匀,假相关的实际数量通常比上述估算更高。
性能分析 2 — SPEC CPU基准中假相关的实测统计
通过模拟器对SPEC CPU 2017整数基准的分析表明,在一个128条指令的滑动窗口中,假相关的分布如下:
| 基准 | WAW数 | WAR数 | 总假相关 | 寄存器复用率 | 无重命名IPC |
|---|---|---|---|---|---|
| gcc | 312 | 486 | 798 | 6.2 | 1.8 |
| mcf | 185 | 298 | 483 | 3.8 | 2.1 |
| deepsjeng | 278 | 412 | 690 | 5.4 | 1.6 |
| exchange2 | 245 | 378 | 623 | 4.9 | 1.9 |
| leela | 267 | 401 | 668 | 5.2 | 1.7 |
| xz | 198 | 324 | 522 | 4.1 | 2.0 |
| 平均 | 248 | 383 | 631 | 4.9 | 1.85 |
其中"寄存器复用率"定义为窗口中使用同一逻辑寄存器作为目标的平均指令数量,表示每个逻辑寄存器平均被条in-flight指令的目标寄存器所使用。"无重命名IPC"是模拟禁用寄存器重命名后的IPC——可以看到,即使拥有32个逻辑寄存器,缺乏寄存器重命名时IPC仅为1.85左右,远低于同一处理器配置下启用重命名后的3.5–4.5的典型IPC。寄存器重命名贡献了约50%–60%的IPC提升,是乱序执行引擎中性能贡献最大的单一微架构特征。
重命名消除假相关
寄存器重命名的核心思想极为简洁:将ISA定义的逻辑寄存器(architectural register)映射到微架构中数量远大于逻辑寄存器的物理寄存器(physical register)。每条产生结果的指令都被分配一个新的物理寄存器来存储其结果,而不是覆盖逻辑寄存器的旧值。这样,不同指令写入同一逻辑寄存器时,实际上写入的是不同的物理寄存器,WAW和WAR相关自然消除。
回到图图 24.2的例子。假设处理器拥有64个物理寄存器(P0–P63),初始状态下逻辑寄存器x1映射到物理寄存器P1,x2–x8分别映射到P2–P8。重命名后的指令序列变为:
mul P32, P2, P3 # I1: P32 = P2 * P3 (x1 -> P32)
add P33, P32, P5 # I2: P33 = P32 + P5 (x4 -> P33, 读新的x1=P32)
sub P34, P6, P7 # I3: P34 = P6 - P7 (x1 -> P34)
mul P35, P34, P8 # I4: P35 = P34 * P8 (x4 -> P35, 读新的x1=P34)重命名后:
I1写入P32,I3写入P34——两个不同的物理寄存器,WAW相关消除。
I2读取P32(I1的结果),I3写入P34——不同的物理寄存器,WAR相关消除。
I2写入P33,I4写入P35——WAW相关同样消除。
仅保留了真相关:I2依赖I1(通过P32),I4依赖I3(通过P34)。
重命名后,I1/I2和I3/I4形成两条独立的依赖链,可以完全并行执行。在乱序引擎中,I3无需等待I1完成,I4也无需等待I2完成——处理器可以同时发射I1和I3到两个乘法单元,然后同时发射I2和I4到两个加法单元。
更详细的重命名示例:6条指令序列
为了更深入地理解重命名过程,下面用一个包含6条指令的代码序列进行完整的重命名追踪。这段代码来自一个简单的数组求和循环:
lw x1, 0(x10) # I1: 加载array[i]
add x3, x3, x1 # I2: sum += array[i] (RAW: x1<-I1, x3<-prev)
addi x10, x10, 4 # I3: ptr++ (RAW: x10<-prev)
lw x1, 0(x10) # I4: 加载array[i+1] (RAW: x10<-I3, WAW: x1与I1)
add x3, x3, x1 # I5: sum += array[i+1] (RAW: x3<-I2, x1<-I4, WAW: x3与I2)
addi x10, x10, 4 # I6: ptr++ (RAW: x10<-I3, WAW: x10与I3)初始映射表状态:x1P1,x3P3,x10P10。空闲列表头部依次为P40、P41、P42、P43、P44、P45。
| 指令 | 重命名后 | 源1 | 源2 | 消除的假相关 | ||
|---|---|---|---|---|---|---|
| I1 | lw P40, 0(P10) | P10 | – | P40 | P1 | – |
| I2 | add P41, P3, P40 | P3 | P40 | P41 | P3 | – |
| I3 | addi P42, P10, 4 | P10 | – | P42 | P10 | – |
| I4 | lw P43, 0(P42) | P42 | – | P43 | P40 | WAW(x1): I1I4 |
| I5 | add P44, P41, P43 | P41 | P43 | P44 | P41 | WAW(x3): I2I5 |
| I6 | addi P45, P42, 4 | P42 | – | P45 | P42 | WAW(x10): I3I6 |
6条指令重命名的逐步追踪
重命名前,这6条指令之间存在以下假相关:
WAW:I1I4(
x1)、I2I5(x3)、I3I6(x10)——共3个WAW。WAR:I1I4(I4写
x1,I2读x1——但I2在I4之前所以这不构成WAR);I2I3(无)。实际的WAR相关出现在I5读x3而I5本身写x3——这是同一条指令的内部行为,不构成指令间WAR。更关键的WAR是:I3读x10,I6写x10——但I3在I6之前,所以是I3I6的WAR。同样,I2读x1,I4写x1,这是I2I4的WAR。
重命名后,所有这些假相关都被消除。注意到I4中=P40而非P1——这是因为在I4重命名时,x1的当前映射已经是P40(由I1建立),而非原始的P1。这个细节很重要:记录的是重命名发生时刻的当前映射,不一定是初始映射。
重命名后的数据流图如图图 24.3所示。可以看到,I1/I2和I4/I5形成了两条可以流水化重叠执行的依赖链,I3和I6各自独立——6条指令的关键路径长度从重命名前的6步(完全串行)缩短为约4步(Load延迟Add延迟的链)。
寄存器重命名的本质可以从数据流图(data-flow graph)的角度理解:程序的数据流图由真相关(RAW)定义,它反映了程序中值的真实流动;而假相关(WAW/WAR)是将数据流图"串行化"到有限逻辑寄存器空间时引入的人为约束。寄存器重命名所做的就是将程序从"逻辑寄存器空间"还原到"数据流空间",恢复程序固有的并行性。从这个意义上说,乱序处理器的后端本质上就是一个数据流机器(dataflow machine),寄存器重命名是连接冯诺依曼前端与数据流后端的桥梁。
:::
从编译器视角理解重命名
编译器在寄存器分配阶段面临的核心挑战与硬件重命名解决的问题密切相关,但视角不同。编译器的寄存器分配器(如基于图着色或线性扫描的分配器)试图在编译时将无限的虚拟寄存器(即SSA形式中的变量)映射到有限的ISA逻辑寄存器。这个映射必须保证在任何程序点上,同时活跃(live)的变量不超过可用寄存器数量——否则就需要溢出到内存。
关键洞察在于:编译器的"活跃"概念基于顺序执行语义——变量在定义点和最后使用点之间被认为是活跃的。但在乱序处理器中,"活跃"的概念要宽得多——只要指令还在ROB中(尚未提交),它涉及的所有物理寄存器都被认为是"活跃的"。一个物理寄存器即使已经被后续指令的结果所覆盖(对应的逻辑寄存器已经重新映射),只要覆盖指令尚未提交,该物理寄存器仍然不能释放(因为可能需要在错误恢复时使用旧值)。
这意味着乱序执行扩大了"活跃寄存器窗口"——从编译器看到的局部代码范围扩展到了整个ROB窗口。这正是为什么物理寄存器数量需要远大于逻辑寄存器数量:必须容纳整个ROB窗口中所有"乱序活跃"的值,而非仅仅编译器静态分析中的"顺序活跃"值。
SSA与硬件重命名的类比
编译器中的静态单赋值形式(Static Single Assignment,SSA)与硬件寄存器重命名有深刻的类比关系。在SSA形式中,每个变量只被赋值一次——如果原始程序中同一个变量被多次赋值,SSA形式会创建不同的"版本"(如x_1、x_2、x_3)。这完全消除了WAW相关(因为不同版本是不同的变量),也消除了WAR相关(因为读取总是引用特定版本)。
硬件寄存器重命名做了完全相同的事情——将每次对逻辑寄存器的写入创建一个新的"版本"(即分配一个新的物理寄存器),后续读取引用特定的版本(即特定的物理寄存器编号)。从这个角度看,硬件重命名就是在运行时执行的动态SSA转换。
这个类比也揭示了一个重要区别:编译器的SSA转换在汇合点需要插入函数来合并不同控制流路径的变量版本;硬件重命名中,控制流的汇合由分支解析和映射表恢复来处理——分支预测正确时,推测路径的映射自然成为正确映射;预测错误时,通过检查点或回退恢复到正确路径的映射。
| 概念 | 编译器SSA | 硬件寄存器重命名 |
|---|---|---|
| 变量版本 | ||
| 映射关系 | SSA构建算法维护 | RAT映射表维护 |
| 资源分配 | 虚拟寄存器(无限) | 物理寄存器(有限,需空闲列表) |
| 控制流汇合 | 函数 | 分支解析 + 映射表恢复 |
| 资源回收 | 活跃性分析 + 死变量消除 | 提交时释放 |
| 执行时机 | 编译时(静态) | 运行时(动态) |
| 分支处理 | 所有路径都分析 | 仅推测一条路径 |
| 优势 | 全局视野,可优化跨基本块 | 适应运行时行为,不依赖编译器 |
编译器SSA与硬件寄存器重命名的详细类比
理解这一类比对于处理器架构师至关重要。它说明了为什么即使编译器已经进行了最优的寄存器分配,硬件寄存器重命名仍然必要——编译器的寄存器分配是基于顺序执行语义的,它无法预见乱序执行引擎将同时"看到"多少条指令。即使编译器使用了所有32个逻辑寄存器并进行了完美的分配,在一个256项ROB的乱序窗口中,32个名字仍然不够——平均每个名字被8条in-flight指令共享。硬件重命名通过在运行时动态创建更多的"版本"(物理寄存器),解决了这个编译器无法解决的问题。
寄存器重命名在硬件中的实现需要解决以下关键问题:
映射管理:如何维护逻辑寄存器到物理寄存器的映射关系?这需要一个映射表(mapping table),通常称为寄存器别名表(Register Alias Table,RAT)。RAT的实现可以基于SRAM、CAM或寄存器阵列,每种方式在面积、时序和端口需求上各有优劣。
物理寄存器分配:当一条指令需要一个新的物理寄存器来存储结果时,如何快速分配?需要一个空闲列表(free list)来管理可用的物理寄存器。空闲列表的实现方式(FIFO或位图)影响分配延迟和恢复复杂度。
物理寄存器释放:当一个物理寄存器不再被任何in-flight指令需要时,如何回收它?释放时机的确定是重命名机制中最微妙的部分——过早释放导致数据丢失,过晚释放浪费资源。
错误恢复:当分支预测错误或异常发生时,如何将映射关系恢复到正确的状态?恢复策略包括检查点快照、逐步回退和提交RAT复制,各有不同的时间-空间权衡。
同周期相关性处理:当同一个时钟周期内重命名的多条指令之间存在相关性时(如写
x3,读x3),如何确保获取的新映射而非旧映射?这需要RAT内部的旁路逻辑。
接下来的三节将分别讨论三种不同的硬件实现方式,每种方式在上述问题上都有不同的解决策略和权衡。
重命名的形式化描述
在深入具体实现之前,先给出寄存器重命名操作的形式化描述。定义以下符号:
:逻辑寄存器集合。
:物理寄存器集合。
:当前的映射函数(即RAT的内容)。
:空闲物理寄存器集合(即空闲列表的内容)。
对于一条指令,设其源操作数为和,目标寄存器为。重命名操作执行以下步骤:
源映射查询:,。
物理寄存器分配:从中取出一个物理寄存器,。
旧映射记录:。
映射更新:。
重命名后的指令变为:。
当指令提交时:(释放旧映射)。
当分支预测错误需要恢复时:(恢复映射),(恢复空闲列表)。
这个形式化描述使得三种实现方式的区别更加清晰——它们在如何实现、和结果值存储方面不同,但逻辑语义完全一致。
值得注意的是,上述形式化描述中假设每条指令只有一个目标寄存器和最多两个源操作数。在实际ISA中,大多数指令满足这一假设(RISC-V的R-type和I-type指令),但某些特殊指令例外:
无目标寄存器的指令:Store指令(
SW、SD)和分支指令(BEQ、BNE)不产生寄存器结果。对于这类指令,重命名阶段跳过步骤2(物理寄存器分配)、3(旧映射记录)和4(映射更新)。但指令仍需分配ROB表项以支持顺序提交。同时读写同一逻辑寄存器的指令:如
ADDI x1, x1, 4,x1同时是源和目标。重命名后变为,其中是x1的旧映射(用于读取源操作数),是新分配的物理寄存器(用于写入结果)。源和目标使用不同的物理寄存器——这正是重命名消除假相关的核心机制。
案例研究 1 — Tomasulo算法——寄存器重命名的先驱
1967年,IBM的Robert Tomasulo为System/360 Model 91浮点单元设计了一种动态调度算法,后来以他的名字命名为Tomasulo算法。Model 91只有4个双精度浮点寄存器(FP0–FP3),但其浮点乘法器需要2个周期、浮点加法器需要4个周期——如此有限的寄存器数量和较长的执行延迟使得假相关成为严重的性能瓶颈。
Tomasulo的解决方案是引入保留站(reservation station)。每个功能单元前面放置若干保留站条目,每个条目存储一条等待执行的指令及其操作数值。关键创新在于:当指令被分发到保留站时,如果源操作数已经可用,就直接将值复制到保留站中;如果源操作数尚未就绪,则在保留站中记录产生该操作数的功能单元的标签(tag)。当某个功能单元完成计算时,通过公共数据总线(CDB)广播结果值和标签,所有等待该标签的保留站条目同时捕获该值。
这种机制实现了隐式的寄存器重命名:保留站的标签取代了逻辑寄存器名字作为操作数的标识符,消除了WAW和WAR相关。然而,Tomasulo方案的值存储在分散的保留站中而非集中的寄存器文件中,这使得同一个值需要被复制到多个保留站,造成了存储空间和能耗的浪费。现代处理器的统一PRF方案可以看作Tomasulo思想的集中化演进——将值集中存储在PRF中,保留站中只记录物理寄存器编号而非值本身。
隐式重命名与显式重命名的对比
Tomasulo算法所采用的是隐式重命名——重命名是通过保留站标签机制自然实现的,没有显式的映射表和物理寄存器分配过程。与之对应,后来的MIPS R10000和现代处理器采用的是显式重命名——有明确的RAT映射表、空闲列表和物理寄存器分配/释放机制。两者在功能上等价(都消除了假相关),但在硬件实现上存在显著差异。
| 特征 | 隐式重命名(Tomasulo) | 显式重命名(PRF方案) |
|---|---|---|
| 映射机制 | 保留站标签隐式映射 | RAT显式映射 |
| 值存储 | 分散在保留站中 | 集中在PRF中 |
| 值复制 | 同一值可能被复制到多个保留站 | 值只存储一份,通过编号引用 |
| 发射队列条目大小 | 大(包含64位操作数值) | 小(只包含物理寄存器编号) |
| 旁路网络 | CDB广播(总线方式) | 点对点转发 |
| 结果可见性 | 仅对当前等待的保留站可见 | 写入PRF后全局可见 |
| 可扩展性 | 差(CDB带宽限制) | 好(PRF可分体优化) |
隐式重命名(Tomasulo)与显式重命名(PRF)的对比
从表表 24.7可以看出,显式重命名的核心优势在于值的集中存储。在Tomasulo方案中,如果一条指令的结果被5条后续指令使用,该结果值会被复制到5个保留站条目中——每次复制都需要64位的数据传输和存储。在显式PRF方案中,结果只存储在PRF的一个条目中,5条后续指令通过物理寄存器编号(仅8–9位)引用同一个PRF条目,在需要时才从PRF读取。这种"引用而非复制"的策略在存储效率和能耗上优势巨大,尤其是当发射队列(issue queue)的条目数量增加到64–128个时。
Tomasulo方案中另一个扩展性瓶颈是CDB的带宽限制。CDB是一条共享总线,每周期只能广播一个结果(或者说,条CDB每周期最多广播个结果)。当多条指令在同一周期完成时,CDB发生争用,部分结果需要延迟一个周期广播。在6-wide处理器中,如果每周期平均有3–4条指令完成,CDB需要至少4条——但4条64位总线的功耗和线延迟在现代处理器中已不可接受。显式PRF方案用PRF写端口替代了CDB:结果直接写入PRF对应的物理寄存器,不需要全局广播。唤醒机制也从CDB广播变为发射队列内部的标签匹配(仅广播物理寄存器编号,8–9位,而非64位数据值),能耗降低一个数量级。
使用ROB进行寄存器重命名
重排序缓冲区(Reorder Buffer,ROB)最初由James E. Smith和Andrew R. Pleszkun在1985年提出,其首要目的是支持精确异常(precise exception)——确保在异常发生时,处理器可以呈现一个所有先于异常的指令都已完成、后于异常的指令都未执行的精确状态。然而,ROB同时也可以作为寄存器重命名的载体:将指令的计算结果暂存在ROB表项中,而不是直接写入架构寄存器文件(ARF),从而实现隐式的寄存器重命名。
这种方式在早期的乱序处理器中被广泛采用,包括Intel的P6微架构(Pentium Pro/II/III)和早期的AMD K6。在理解ROB重命名方案时,需要把握一个核心要点:ROB同时承担了两个角色——作为顺序提交的缓冲区(支持精确异常),以及作为重命名的存储载体(存储未提交指令的结果值)。这种"一体两用"的设计在简化硬件的同时,也导致了ROB结构在面积和端口需求上的压力。
ROB表项中存储结果
在基于ROB的重命名方案中,每个ROB表项除了记录指令的控制信息外,还包含一个数据字段用于存储该指令的执行结果。图图 24.5展示了一个典型ROB表项的结构。
下面对每个字段进行详细的位宽分析和功能说明。
ROB表项各字段的详细位宽分析
Valid(1位):该表项是否已被分配给一条指令。当指令从解码器输出并进入乱序引擎时,处理器在ROB尾部分配一个表项,将Valid置1。当指令提交后,Valid被清0,表项回收。在刷新(flush)操作中,分支预测错误路径上所有表项的Valid位被批量清0。
Done/Complete(1位):该指令是否已经执行完成并将结果写入Value字段。分配时Done=0;指令在执行单元完成计算、结果写回ROB后,Done置1。提交逻辑通过检查ROB头部表项的Done位来决定是否可以提交——只有Done=1的表项才能被提交。
Exception(1位):指令在执行过程中是否触发了异常。如果Exception=1,提交逻辑在该表项到达ROB头部时将触发异常处理,而不是正常提交。此位与Done位独立——一条指令可能执行完成(Done=1)但产生了异常(Exception=1),例如除零操作。
ExcCode(4–5位):异常类型编码。RISC-V特权规范定义了约16种同步异常(指令地址未对齐、非法指令、断点、Load地址未对齐、Store地址未对齐、环境调用等),需要4位编码。某些实现可能扩展到5位以容纳微架构内部异常(如ROB溢出、内部奇偶校验错误等)。
Dst(5位,RV64):目标逻辑寄存器编号。位,足以索引RISC-V的32个整数逻辑寄存器。对于x86-64,由于有16个通用寄存器,Dst字段为4位。对于无目标寄存器的指令(如
SW、BEQ),此字段被标记为无效(通常通过另一个1位的HasDst标志位)。HasDst(1位):指令是否有目标寄存器。Store指令、分支指令、
NOP等不写寄存器的指令将此位置0,提交时不需要将Value写入ARF。Value(64位,RV64):指令执行的结果值。这是ROB重命名方案的核心字段——结果值直接存储在ROB中而非寄存器文件中。对于32位数据结果(如
ADDW),高32位进行符号扩展或零扩展。对于Load指令,Value字段存储从内存加载的数据。对于分支指令,Value字段通常不使用(或用于存储分支目标地址以便调试)。此字段是ROB表项中最宽的字段,也是ROB重命名方案面积开销大的主要原因。PC(64位或压缩表示):指令的程序计数器值。用于异常处理时向操作系统报告异常发生的地址,也用于调试和性能计数器。由于ROB中相邻指令的PC通常只差4(顺序指令)或已知的偏移,实际实现中常采用压缩存储:只存储PC的低位(如低12位),高位通过ROB头部的基地址推导。这将PC字段从64位压缩到12–16位。
Type(3位):指令类型编码。典型的编码方式为:000=ALU,001=Branch,010=Load,011=Store,100=Mul/Div,101=FPU,110=System,111=保留。提交阶段根据Type字段决定提交操作:ALU/Load类型将Value写入ARF;Store类型将数据从Store Queue提交到内存层次;Branch类型解除分支预测锁定。
BrMask/BrTag(4–8位):分支推测信息。在支持多个未决分支的处理器中,每条指令需要记录它在哪些未决分支的推测路径上。BrMask是一个位向量,每一位对应一个未决分支——如果第位为1,表示这条指令位于第个未决分支的推测路径上。当分支被确认预测错误时,所有BrMask第位为1的ROB表项将被清除。8位的BrMask支持最多8个同时未决的分支。
Spec Info / 其他字段(10–20位):包括Store Queue索引(用于Store指令的提交)、Load Queue索引(用于Load指令的内存序检查)、功能单元类型(用于发射调度)、以及微架构特定的控制位。
将上述所有字段汇总,一个完整的ROB表项的总位宽计算如下:
| 字段名 | 位宽 | 说明 |
|---|---|---|
| Valid | 1 | 表项有效位 |
| Done | 1 | 执行完成标志 |
| Exception | 1 | 异常标志 |
| ExcCode | 4 | 异常类型编码 |
| HasDst | 1 | 是否有目标寄存器 |
| Dst | 5 | 目标逻辑寄存器编号 |
| Value | 64 | 执行结果值 |
| PC (压缩) | 16 | 指令地址(低16位) |
| Type | 3 | 指令类型编码 |
| BrMask | 8 | 分支推测掩码 |
| SQ/LQ Index | 8 | Store/Load Queue索引 |
| FU Type | 3 | 功能单元类型 |
| 其他控制位 | 5 | 微架构特定位 |
| 总计 | 120 | 带Value字段 |
| 不含Value | 56 | PRF方案的ROB |
ROB表项位宽的详细分解(RV64,带Value字段)
从表表 24.8可以看出,Value字段占据了ROB表项总位宽的53%(64/120)。在采用统一PRF方案的处理器中,去除Value字段后ROB表项仅需约56位(实际实现中通常为60–80位,因为还会增加一些PRF相关的字段如和),面积缩减了约50%。这正是现代处理器从ROB重命名转向PRF重命名的重要动机之一。
硬件描述 2 — ROB的容量与面积
ROB通常实现为一个循环队列(circular buffer),由头指针(head)和尾指针(tail)管理。现代处理器的ROB容量举例如下:
| 处理器 | ROB表项数 | 每表项位数(估算) |
|---|---|---|
| Intel Skylake | 224 | 180 |
| Intel Golden Cove | 512 | 200 |
| AMD Zen 4 | 320 | 190 |
| ARM Cortex-X3 | 320 | 160 |
| 香山昆明湖 | 256 | 170 |
以512个表项、每表项200位计算,ROB的存储容量为位。在5nm工艺下,这约占的面积。但需要注意的是,ROB的面积瓶颈不在于存储本身,而在于其多端口设计——ROB需要支持多条指令同时写入结果(执行完成时)和多条指令同时读取操作数,端口数量直接影响面积和时序。
值得说明的是,在现代采用统一PRF方案的处理器中,ROB的Value字段通常被去除(结果存储在PRF中),ROB表项的宽度可以缩减到80–100位,显著减小了面积和功耗。Intel Golden Cove和AMD Zen 4实际上采用的是PRF方案,但ROB仍然保留用于顺序提交和精确异常。
ROB重命名的操作流程
基于ROB的寄存器重命名方案的完整操作流程可以分为四个阶段:分配、执行、结果写入和提交。图图 24.6展示了这一流程的详细时序。
(1)分配阶段。当一条指令从解码器输出并进入乱序引擎时,处理器在ROB的尾部为其分配一个新的表项。该表项的索引(例如ROB#17)就成为这条指令的重命名标签——后续指令如果依赖该指令的结果,将通过这个ROB索引来引用。分配时将Valid位置1,Done位置0,将目标逻辑寄存器编号写入Dst字段。同时,处理器更新一个查找表(通常是一个简单的寄存器阵列,按逻辑寄存器编号索引),记录"逻辑寄存器x1的最新值由ROB#17产生"。
(2)执行阶段。指令被送入发射队列(issue queue),等待其源操作数就绪后被发射到功能单元执行。关键问题在于操作数的读取:指令的源操作数可能来自两个不同的位置——
如果源逻辑寄存器的最新值已经提交到ARF中(即没有更晚的in-flight指令写该寄存器),则从ARF中读取。
如果源逻辑寄存器的最新值由某条in-flight指令产生(该指令的ROB表项已经Done=1),则从ROB中读取。
如果源逻辑寄存器的最新值由某条尚未完成执行的in-flight指令产生(Done=0),则指令必须在发射队列中等待,直到该值通过旁路网络转发或写入ROB后再读取。
从ROB读取操作数的判断逻辑
确定一个源操作数应该从ARF还是ROB读取,需要一套查找机制。具体而言,对于源逻辑寄存器r,处理器需要在所有in-flight的ROB表项中搜索:是否存在某个表项的Dst字段等于r?如果存在多个(即多条in-flight指令都写r),则应该选择最新的那一条(在程序顺序中最靠后、但仍在当前指令之前的那条)。
这个查找可以通过两种方式实现:
方式一:CAM查找。将ROB的Dst字段组织为内容可寻址存储器(CAM)。输入源逻辑寄存器编号r,CAM同时与所有ROB表项的Dst字段比较,返回匹配的表项索引。当存在多个匹配时,选择在程序顺序中最新的那个(即ROB索引最接近当前指令、但在其之前的那个匹配项)。
CAM查找的硬件代价很大:对于一个256项的ROB,每个源操作数需要256个5位比较器并行工作,然后通过优先级编码器选出最新的匹配。在6-wide处理器中,每周期有12个源操作数需要查找,总共需要个比较器——面积和功耗都不可接受。
方式二:映射表间接查找。使用一个以逻辑寄存器编号为索引的映射表,记录每个逻辑寄存器的最新值来自哪个ROB表项。这个映射表本质上就是一个简化的RAT——P6微架构中称为Register Alias Table。映射表有32个条目(对应32个逻辑寄存器),每个条目包含:
InROB(1位):该逻辑寄存器的最新值是否在ROB中(即是否有in-flight指令写该寄存器)。如果InROB=0,则从ARF读取。
ROBIdx(位):产生该逻辑寄存器最新值的ROB表项索引。对于256项ROB,需要8位。
总共每个条目位,32个条目共位——存储开销极小。读取操作数时,先查映射表获取InROB和ROBIdx,然后根据InROB选择从ARF或ROB[ROBIdx].Value读取。这种间接方式避免了CAM查找的大量比较器开销,是P6实际采用的方案。
ROB读写端口需求分析
ROB重命名方案的一个核心挑战是端口需求的快速增长。分析一个-wide的处理器在每个周期中对ROB的访问需求:
写端口(执行写回):每周期最多条指令完成执行并将结果写入ROB的Value字段。因此需要个写端口。每个写端口需要驱动ROB的64位Value字段。
读端口(操作数读取):每周期最多条指令需要从ROB读取操作数。每条指令最多2个源操作数,因此最坏情况下需要个读端口。但并非所有操作数都从ROB读取——部分来自ARF、部分通过旁路网络获取。统计表明,约40%–60%的操作数来自ROB(因为in-flight指令越多,逻辑寄存器的最新值越可能在ROB中)。
提交读端口:每周期最多条指令提交,需要从ROB读取Value字段并写入ARF。这需要额外的个读端口。但提交操作发生在流水线的最后阶段,可以与操作数读取在不同的半周期进行(利用时钟双沿技术),从而复用物理端口。
| 发射宽度 | 写端口 | 读端口(操作数) | 读端口(提交) | 总端口 |
|---|---|---|---|---|
| 3 | 3 | 6 | 3 | 12 |
| 4 | 4 | 8 | 4 | 16 |
| 6 | 6 | 12 | 6 | 24 |
| 8 | 8 | 16 | 8 | 32 |
不同发射宽度下ROB的端口需求
表表 24.10展示了端口需求随发射宽度的线性增长。对于一个256项的SRAM而言,每增加一个端口,面积大约增加15%–20%(因为每个存储单元需要额外的访问晶体管和位线)。从3-wide的12个端口增长到6-wide的24个端口,ROB的面积大约增长到倍——这还没有考虑更长的位线带来的时序恶化。这就是为什么ROB重命名方案在3-wide的P6微架构中可行,但无法扩展到6-wide以上的设计。
(3)写回阶段。指令执行完成后,将结果值写入其对应ROB表项的Value字段,并将Done位置1。同时通过公共数据总线(Common Data Bus,CDB)广播结果值和ROB索引,以唤醒发射队列中等待该结果的后续指令。
(4)提交阶段。ROB头指针按程序顺序逐条检查表项。如果头部表项的Done位为1(指令已完成),则执行提交:将Value字段的值写入ARF中Dst字段指定的逻辑寄存器,释放该ROB表项(将Valid置0),头指针前进。如果头部表项标记了异常,则触发异常处理,丢弃该表项及其后所有ROB表项中的指令。
下面通过一个具体的流水线时序图来说明三条指令在ROB重命名方案中的完整执行过程。
从图图 24.7可以观察到几个关键时序特征:(1)I2在分配阶段就知道它依赖I1(通过查询映射表发现x1的最新值由ROB#0产生),因此在发射队列中等待直到C4通过旁路获取结果;(2)I3与I1/I2没有数据依赖,可以在C2就开始读操作数并在C3开始执行,但乘法操作本身需要3个周期;(3)提交严格按程序顺序进行:即使I3在C6就已完成,它必须等到I1(C7)和I2(C8)先提交后,才能在C9提交。
ROB重命名的详细操作数读取流程
操作数读取是ROB重命名方案中最复杂的环节,值得用一个详细的示例来说明。考虑以下场景:4条指令按程序顺序进入一个4-wide的乱序处理器,初始状态下x1=100(存储在ARF中),ROB为空。
add x1, x2, x3 # I1: 写x1, ROB#0, Done=0
sub x4, x1, x5 # I2: 读x1, 依赖I1
mul x6, x7, x8 # I3: 不涉及x1, 独立
add x9, x1, x10 # I4: 读x1, 依赖I1C1——分配阶段:四条指令同时分配到ROB#0–ROB#3。映射表更新:x1的最新值来自ROB#0(I1的目标)。I2和I4的源操作数x1查询映射表,发现InROB=1、ROBIdx=0,即x1的最新值将由ROB#0产生。此时ROB#0的Done=0(I1尚未执行),因此I2和I4在发射队列中记录"等待ROB#0完成"。I3的源操作数x7和x8不在ROB中(InROB=0),从ARF读取。
C2——I1和I3发射执行:I1的源操作数x2和x3不在ROB中,从ARF读取后发射到ALU执行。I3的源操作数同样从ARF读取后发射到乘法器。I2和I4在发射队列中等待。
C3——I1完成,结果写回ROB:I1执行完成,结果值(假设为42)写入ROB#0的Value字段,Done位置1。同时,结果值42和标签ROB#0通过CDB广播。I2和I4的发射队列条目捕获到该广播,标记其x1源操作数已就绪。
C4——I2和I4发射执行:
I2需要读取x1:此时有两种获取途径。如果旁路网络将I1的结果直接转发到I2的输入端,I2可以立即发射。如果旁路窗口已过(I1在上一周期完成),I2需要从ROB#0读取Value=42。
I4同样需要x1的值,通过相同的机制获取42。
注意:I2和I4不能从ARF读取x1——ARF中的x1仍然是100(I1的结果尚未提交),而正确的值是42(I1的执行结果)。
这个示例清楚地展示了ROB方案中操作数读取的三个来源:ARF(已提交的值)、ROB(已执行但未提交的值)、旁路网络(刚刚执行完成的值)。判断从何处读取的决策逻辑是:
查映射表:如果InROB=0,从ARF读取,不需要进一步判断。
如果InROB=1,检查对应ROB表项的Done位:
如果Done=1,从ROB读取Value字段。
如果Done=0,在发射队列中等待,直到通过CDB或旁路获取值。
ROB重命名中异常处理的简洁性
ROB方案在异常处理方面具有天然优势。当ROB头部的指令标记了异常时,处理器只需:
将异常信息(异常类型、PC值)传递给异常处理逻辑。
清除ROB中异常指令之后的所有表项(将Valid位全部置0)。
回退ROB尾指针到异常指令的位置。
清空映射表中所有InROB标记(将所有逻辑寄存器的InROB位置0)。
这四步完成后,所有逻辑寄存器的值都在ARF中(因为只有已提交的值才会写入ARF),处理器状态精确地对应于异常指令之前的程序点。不需要任何额外的映射表恢复或物理寄存器回收——这是ROB方案相对于PRF方案的主要优势之一。在PRF方案中,异常恢复需要额外的提交RAT复制和空闲列表重建操作。
案例研究 2 — Intel P6微架构的ROB重命名实现
Intel P6微架构(1995年,Pentium Pro)是第一个大规模商用的x86乱序处理器,它使用了ROB重命名方案。P6微架构被广泛认为是处理器设计史上最具影响力的微架构之一,其基本思想延续至今。P6的关键参数如下:
| 参数 | 值 |
|---|---|
| ROB容量 | 40个表项 |
| ARF | 8个整数寄存器 + 8个x87浮点寄存器 |
| 重命名宽度 | 3-wide(每周期最多重命名3条op) |
| CDB宽度 | 3条结果总线 |
| 提交宽度 | 3-wide(每周期最多提交3条op) |
| ROB表项宽度 | 约120位(含控制、数据、异常信息) |
| 保留站容量 | 20个条目(整数和浮点共享) |
| 制造工艺 | 350nm,550万晶体管 |
P6的操作数读取采用了"ARF + ROB"双源策略:在重命名阶段,对于每个源操作数,首先检查是否有in-flight指令写入该逻辑寄存器(通过查找映射表);如果有,则记录相应的ROB索引,并在该ROB表项的Done位为1时从ROB读取Value;如果没有,则从ARF读取。
P6的数据通路中,ROB扮演了三个角色:(1)重命名载体——存储未提交指令的结果值;(2)顺序提交缓冲——维护程序顺序以支持精确异常;(3)旁路网络的补充——当旁路网络无法转发时(例如产生者和消费者之间间隔超过旁路窗口),结果从ROB读取。
P6的ROB仅有40个表项,以今天的标准来看极为保守。但在1995年的350nm工艺下,40个表项的ROB已经是一个不小的结构。40表项120位位的存储,加上3个读端口3个写端口3个提交读端口的多端口设计,ROB在P6的芯片面积中占据了可观的比例。
P6选择ROB重命名方案而非PRF方案,一个重要原因是1995年时x86只有8个通用整数寄存器——即使不采用PRF方案,864位的ARF也极小。ROB重命名方案的双源读取在3-wide设计中尚可接受:每周期最多6个源操作数读取(3条指令2源),ROB需要6个读端口——对于40个表项的SRAM来说,时序刚好可以在目标频率(233MHz)内满足。
但随着后续处理器发射宽度的增加(Pentium 4设计时目标为4-wide以上),ROB读端口成为不可接受的瓶颈。Intel因此在Pentium 4(NetBurst微架构,2000年)中转向了统一PRF方案——尽管NetBurst在流水线深度等其他设计决策上颇受争议,但其PRF方案的选择被后续所有Intel微架构继承。
ROB重命名的优缺点
优点:
(1)概念简单,实现直观。ROB重命名不需要独立的物理寄存器文件,也不需要复杂的物理寄存器分配/释放逻辑。ROB本身已经存在(用于精确异常和顺序提交),在其中增加Value字段就实现了重命名功能——这是一种"搭便车"的设计。
(2)异常恢复简单。当发生异常或分支预测错误时,只需将ROB中错误路径上的表项全部作废(清除Valid位),然后回退尾指针即可。由于真正的架构状态始终保存在ARF中(只有已提交的值才会写入ARF),恢复操作非常简洁——不需要额外的映射表恢复机制。
(3)物理寄存器不会耗尽。在ROB方案中,物理存储空间(即ROB表项的Value字段)与ROB表项是绑定的——分配ROB表项时自动获得存储空间,释放ROB表项时自动回收。不存在独立的物理寄存器分配和释放问题,也不会出现"ROB有空位但物理寄存器已耗尽"的情况。
缺点:
(1)操作数读取的双源问题。指令的源操作数可能来自ARF,也可能来自ROB中某个未提交表项的Value字段。这意味着每个源操作数都需要查找两个位置,硬件需要在两者之间进行选择(通过一个多路选择器)。在-wide的处理器中,每周期最多有个源操作数需要读取(假设每条指令最多2个源操作数),每个操作数都需要能够从ARF或ROB中读取,这对读端口的需求非常大。
(2)ROB读端口成为扩展瓶颈。ROB是一个大型的循环队列(数百个表项),对其进行随机访问读取(根据ROB索引读取Value字段)需要多路多端口的SRAM或寄存器阵列。对于一个6-wide处理器,每周期最多需要从ROB中读取12个操作数(6条指令2个源操作数),加上同周期可能有6个写端口(执行完成写回Value),ROB需要的端口总数为个端口——这对于一个200+表项的结构来说,面积和时序都极具挑战。
(3)提交带宽约束。在ROB方案中,每条指令的结果在提交时都必须从ROB复制到ARF。对于一个6-wide的处理器,提交带宽要求每周期6个64位写入ARF——这增加了ARF的写端口需求,但本身尚可接受。更大的问题是提交路径上的延迟:从ROB读取Value,然后写入ARF,需要经过ROB的读逻辑和ARF的写逻辑,这可能形成一个较长的关键路径。
(4)Value字段增大ROB面积和功耗。在ROB中存储64位的结果值使得每个表项的宽度显著增加。以一个512表项的ROB为例,Value字段本身就需要位的存储。每次访问ROB(读写操作数和结果)都需要驱动这些存储单元的位线和字线,导致显著的动态功耗。
(5)提交路径引入额外延迟。在ROB方案中,指令结果在提交时才从ROB复制到ARF。这个"ROB读ARF写"的路径是ROB方案独有的开销。在一个6-wide的处理器中,提交路径需要每周期从ROB读取6个64位值并写入ARF——这是6个64位的数据移动,在物理布局上需要较长的金属连线(ROB和ARF通常位于芯片的不同区域),可能需要专门的流水线寄存器来中继,增加了提交延迟。
相比之下,PRF方案的提交操作只需要更新提交RAT(32个条目9位 = 288位的寄存器写入)和释放旧物理寄存器(向空闲列表推入编号),这些都是极小的数据移动,延迟远低于ROB方案的数据复制。
ROB重命名与保留站方案的关系
ROB重命名方案通常与保留站(reservation station)发射方案配合使用,而非集中式发射队列。这是因为在ROB方案中,操作数值可能存储在ROB或ARF中的不同位置,保留站提供了一个统一的位置来存储等待执行的指令及其操作数值——无论操作数来自哪里,一旦被读取就复制到保留站条目中。
保留站方案中,每个保留站条目包含:
操作码(6位)
源操作数1的值(64位)或标签(ROB索引,8位)+ 就绪位(1位)
源操作数2的值(64位)或标签(8位)+ 就绪位(1位)
目标ROB索引(8位)
每个条目约需位。对于20个保留站条目(如P6微架构),总存储为位。这比统一PRF方案中的发射队列条目(每条目约30–40位,因为只存储物理寄存器编号而非值)大得多。保留站的值复制也是一个功耗热点——同一个值可能被复制到多个保留站条目中(当多条指令依赖同一个结果时),造成存储和能量的浪费。
统一PRF方案消除了保留站中的值存储——发射队列条目只记录物理寄存器编号(作为标签),操作数在发射时刻才从PRF中读取。这大幅减小了发射队列的面积,也消除了值复制的功耗浪费。
设计提示
ROB重命名方案在窄发射宽度(2–3 wide)的处理器中是一个实用的选择——P6微架构的成功证明了这一点。但当发射宽度增加到6-wide及以上时,ROB的多端口需求使其面积和时序迅速恶化。这正是Intel从Pentium 4的NetBurst微架构开始转向统一PRF方案的技术驱动力——尽管NetBurst在其他方面的设计决策颇受争议,但其采用PRF进行寄存器重命名的决定被后续所有Intel微架构继承。
ROB重命名方案的面积与时序量化
为了量化ROB重命名方案的扩展性问题,下面计算不同发射宽度下ROB的面积增长。
ROB的面积主要由存储阵列和端口电路决定。存储阵列的面积与成正比,其中是每个表项的位宽。端口电路的面积与端口数量的平方成近似正比(因为每个端口需要独立的位线和字线驱动器)。因此,ROB的总面积可以估算为:
其中是总端口数。
以256项、每项180位的ROB为例,端口数随发射宽度的变化如表表 24.12所示。
| 发射宽度 | 读端口 | 写端口 | 总端口 | 相对面积() | 时序退化 |
|---|---|---|---|---|---|
| 2 | 4+2=6 | 2 | 8 | 1.0 | 基线 |
| 3 | 6+3=9 | 3 | 12 | 1.9 | 5% |
| 4 | 8+4=12 | 4 | 16 | 3.2 | 12% |
| 6 | 12+6=18 | 6 | 24 | 6.7 | 25% |
| 8 | 16+8=24 | 8 | 32 | 11.3 | 40% |
ROB面积随发射宽度的增长(256项,180位/项)
从表表 24.12可以看出,从2-wide到8-wide,ROB面积增长了11.3倍,时序退化了40%——这意味着在相同的目标频率下,8-wide的ROB需要更多的流水线级数来补偿时序恶化,进一步增加了分支预测错误的惩罚。这正是ROB重命名方案在宽发射处理器中不可行的根本原因。
相比之下,PRF方案下的ROB不含Value字段(每项约80位),且不需要操作数读端口(操作数从PRF读取),端口需求大幅降低。同样的256项ROB在PRF方案下仅需要个写端口(标记Done位)和个提交端口,面积仅为ROB重命名方案的约。
将ARF扩展进行寄存器重命名
第二种重命名方案是在架构寄存器文件(ARF)的基础上进行扩展:增加物理寄存器的数量,同时维护一个映射表来跟踪每个逻辑寄存器当前映射到哪个物理寄存器。这种方案可以看作ROB方案和统一PRF方案之间的一个中间地带。
扩展寄存器文件的方式
在扩展ARF方案中,处理器的寄存器文件不再仅包含ISA定义的逻辑寄存器(如RISC-V的x0–x31),而是包含更多的物理寄存器(如64个或96个)。每个逻辑寄存器在任意时刻都映射到某一个物理寄存器,但同一个逻辑寄存器在不同的指令"版本"中可以映射到不同的物理寄存器。
扩展方式的具体实现
"扩展ARF"的核心思想是:保留一个统一的寄存器文件,但其条目数量远大于逻辑寄存器数量。以RISC-V为例,假设ISA定义了32个整数逻辑寄存器,我们将物理寄存器文件扩展到80个条目(PR0–PR79)。其中:
32个物理寄存器在任意时刻保存着32个逻辑寄存器的架构状态(即已提交的值)。但这32个物理寄存器不是固定的PR0–PR31——它们是动态分配的,可以是80个物理寄存器中的任意32个。
剩余个物理寄存器是空闲寄存器池,用于为新指令的目标寄存器分配重命名空间。
扩展ARF方案与ROB方案的本质区别在于:结果值存储在扩展后的寄存器文件中而非ROB中。这意味着操作数读取只需要访问一个统一的寄存器文件,消除了ROB方案中"ARF或ROB二选一"的双源问题。
扩展ARF方案与统一PRF方案的区别则更为微妙。从概念上看,扩展ARF方案仍然认为"架构寄存器文件"是一个物理实体,只是被扩展了;而统一PRF方案则完全将架构寄存器抽象化。但在硬件实现上,两者几乎没有区别——都使用映射表、空闲列表、和一个大的物理寄存器文件。历史上的区分主要是概念层面的,现代处理器的实现已经模糊了这两者的边界。
映射表(RAT)是扩展ARF方案的核心组件。它是一个以逻辑寄存器编号为索引的查找表,每个条目存储当前映射到的物理寄存器编号。对于RISC-V(32个整数逻辑寄存器)映射到64个物理寄存器的情况,RAT有32个条目,每个条目6位(),总容量仅为位。这是一个极其紧凑的结构,即使在早期工艺下也不构成面积压力。
扩展ARF方案还需要一个空闲列表(free list)来管理当前未被使用的物理寄存器。空闲列表记录了所有当前可被分配的物理寄存器编号。在初始状态下,32个逻辑寄存器各映射到一个物理寄存器(如x0PR0, x1PR1, , x31PR31),剩余的PR32–PR79共48个物理寄存器进入空闲列表。
空闲列表的实现方式决定了物理寄存器分配的效率和恢复的复杂度。FIFO队列方式将空闲物理寄存器编号按释放的先后顺序排列在一个循环缓冲区中。分配时从FIFO头部弹出编号,释放时将编号推入FIFO尾部。FIFO的优点是分配延迟极低——仅需读取头指针指向的条目并递增指针,约1–2级逻辑门延迟。对于-wide的处理器,每周期需要并行分配个物理寄存器,FIFO需要支持路并行出队。这可以通过维护个连续的读指针来实现:第1条指令读取位置,第2条指令读取,,第条指令读取。分配完成后,头指针前进步。这种设计的硬件开销很小,但需要注意一个边界条件:如果空闲列表中剩余的条目数少于,则只能分配可用数量的物理寄存器,多余的指令必须等待——这就是物理寄存器饥饿导致的前端暂停。
操作流程
扩展ARF方案的操作流程与统一PRF方案非常相似,主要区别在于概念层面。下面详细描述每个阶段的操作。
重命名阶段
当一条指令进入乱序引擎时,重命名逻辑执行以下操作:
(1)源操作数映射查询。对于每个源操作数的逻辑寄存器编号,查询RAT获取当前映射到的物理寄存器编号。例如,如果指令为ADD x4, x1, x2,则查询RAT[x1]和RAT[x2],假设得到P12和P7。后续操作数读取将直接从P12和P7中读取值。
(2)目标寄存器分配。从空闲列表中分配一个新的物理寄存器。例如,空闲列表头部为P55,则分配P55给x4。更新RAT:RAT[x4] P55。同时记录x4的旧映射 = RAT[x4]的旧值(假设为P4),将=P4写入ROB表项。
(3)同周期相关性处理。如果同一周期内重命名的多条指令之间存在数据相关(如写x4,读x4),需要通过旁路逻辑确保获取的新映射P55而非RAT中的旧映射P4。
执行与写回阶段
指令等待源操作数就绪后被发射执行。与ROB方案不同,操作数读取只需要访问一个位置——扩展后的物理寄存器文件,无需在ARF和ROB之间选择。这是扩展ARF方案相对于ROB方案的主要优势:数据通路更简洁,不需要双源选择器,操作数读取的延迟更低。
执行完成后,结果写入。例如,ADD x4, x1, x2的结果写入P55。写入完成后,P55中的值即可被后续依赖指令通过旁路或PRF读取获得。
提交与物理寄存器释放
当指令从ROB头部提交时,其旧的物理寄存器(被该指令覆盖的映射)可以被安全释放回空闲列表——此时没有任何更早的指令需要中的值了(因为提交是按程序顺序进行的)。
提交操作的具体步骤:
从ROB头部读取表项:Dst=x4,=P55,=P4,Done=1。
检查Done位:Done=1,可以提交。
更新提交映射表(如果维护的话):cRAT[x4] P55。
释放=P4:将P4推入空闲列表尾部。
释放ROB表项:头指针前进。
注意,在扩展ARF方案中,提交操作不涉及数据移动——P55中的结果值早已在写回阶段写入,提交只是更新控制信息(映射表和空闲列表)。这与ROB方案形成对比,ROB方案在提交时需要将64位的Value从ROB复制到ARF。
错误恢复
当发生分支预测错误时,需要将RAT恢复到分支指令之前的状态。这可以通过以下方式实现:
检查点恢复:在每个分支指令处保存RAT的完整副本(checkpoint),预测错误时直接载入对应的检查点。恢复时间为1个周期。
逐步回退:从ROB尾部向分支指令逐条回退,利用记录的信息逐步恢复RAT。恢复时间与被清除的指令数量成正比。
扩展ARF方案解决了ROB方案的双源读取问题——所有操作数都从统一的物理寄存器文件中读取。然而,它引入了一个新的概念——"架构"寄存器和"推测"寄存器混合存储在同一个物理结构中,使得架构状态的维护变得更加微妙。
架构状态与推测状态的区分
在扩展ARF方案中,80个物理寄存器中,任何时刻都恰好有32个保存着架构状态(已提交的值),其余最多48个保存着推测状态(未提交指令的结果)。但哪32个保存架构状态是动态变化的——随着指令提交和新指令进入,映射关系不断更新。
这带来了一个重要的设计考量:当发生异常需要恢复到架构状态时,处理器必须能够确定哪些物理寄存器包含架构值。这正是提交RAT(或称为架构映射表)的作用——它始终记录着架构状态下每个逻辑寄存器映射到哪个物理寄存器。
在扩展ARF方案中,恢复操作不需要从某个地方复制数据——架构值已经在物理寄存器中了,只需恢复映射表即可。这与ROB方案形成鲜明对比——ROB方案需要将ROB中的Value复制到ARF,而扩展ARF方案只需用提交映射表覆盖推测映射表,一个288位的寄存器复制操作即可完成。
错误恢复的两种策略
(1)检查点恢复(Checkpoint Recovery)。在每个分支指令处保存推测RAT的完整副本。对于32个逻辑寄存器7位物理寄存器编号()的配置,每个检查点需要位。如果维护8个检查点,总存储为位。预测错误时,直接将对应检查点的内容载入推测RAT,1个周期恢复完成。
(2)逐步回退(Walk-back Recovery)。从ROB尾部向分支指令逐条回退,利用每个ROB表项中记录的逐步恢复推测RAT。不需要检查点存储,但恢复时间与被清除的指令数量成正比。对于-wide的回退带宽,清除条指令需要个周期。
MIPS R10000选择了检查点恢复方案(4个检查点),因为在1996年的设计中,分支预测准确率相对较低(约85%–90%),频繁的预测错误要求快速恢复。检查点方案的另一个优势是空闲列表的恢复同样简单——只需在每个检查点处保存空闲列表的FIFO指针,恢复时重置指针即可,被丢弃指令分配的物理寄存器自动回到空闲列表中(因为FIFO指针回退到了分配前的位置)。
R10000的检查点限制为4个,对应最多4个同时未决的分支。在其4-wide、32项活跃列表的设计中,这通常是足够的——平均每8条指令一条分支,32项窗口中约有4条分支。但在分支密集的代码中(如高度嵌套的if-else链),4个检查点可能不够,导致前端暂停。这个限制在后续的处理器设计中被大幅放宽——现代处理器通常支持8–16个同时未决的分支,代价仅为几千位的额外检查点存储。
R10000的另一个历史贡献是证明了显式重命名在商用处理器中的可行性和性能优势。在R10000之前,学术界对于是否需要显式的映射表和物理寄存器分配机制存在争论——Tomasulo风格的隐式重命名已经在P6微架构中取得了商业成功。R10000的成功证明了显式重命名的扩展性更好——它消除了保留站中的值复制问题,为后来的大规模乱序设计铺平了道路。
MIPS R10000(1996年,SGI设计)是最早采用显式寄存器重命名(而非Tomasulo隐式方案)的商用处理器之一,其重命名方案对后续所有高性能处理器产生了深远影响。R10000的重命名参数如下:
| 参数 | 值 |
|---|---|
| 物理整数寄存器 | 64个64位(映射32个逻辑整数寄存器) |
| 物理浮点寄存器 | 64个64位(映射32个逻辑浮点寄存器) |
| 整数映射表(RAT) | 位 = 192位 |
| 浮点映射表(RAT) | 位 = 192位 |
| 解码/重命名宽度 | 4-wide |
| RAT读端口 | 8个(4条指令2源) |
| RAT写端口 | 4个(4条指令1目标) |
| 整数空闲列表 | 32个条目的FIFO() |
| 浮点空闲列表 | 32个条目的FIFO |
| 分支检查点数 | 4个 |
| 活跃列表(Active List) | 32个条目(类似ROB,但不含Value) |
R10000的重命名流程如下:在解码阶段,每条指令的源操作数通过查询RAT获取物理寄存器编号;目标寄存器从空闲列表分配新的物理寄存器,同时更新RAT。R10000的一个创新是使用活跃列表(Active List)——一个32项的循环队列,功能类似于ROB,但不包含Value字段。活跃列表的每个条目仅记录控制信息(目标逻辑寄存器、旧映射、完成标志等),约30位宽。指令结果直接写入物理寄存器文件,而非活跃列表。这使得R10000的活跃列表面积远小于P6的ROB——位 vs. P6的位。
R10000的错误恢复机制是一个教科书级的设计。每条分支指令在重命名时触发一次检查点保存:将当前RAT的全部内容(192位)和空闲列表的头指针(6位)复制到一个检查点寄存器中。R10000支持最多4个同时未决的分支,因此维护4个检查点,总存储开销为位。当第5条未决分支到达时,前端暂停直到最早的分支被解析。
R10000的物理寄存器数量(整数和浮点各64个)在当时是充足的——其活跃列表仅32项,意味着最多32条in-flight指令,加上32个逻辑寄存器,恰好等于物理寄存器总数。然而,并非所有in-flight指令都写寄存器,因此实际中物理寄存器很少耗尽。以今天的标准来看,64个物理寄存器远远不够——现代处理器的ROB容量已达256–512项,需要的物理寄存器数量是R10000的3–6倍。
:::
这一方案在历史上被一些处理器采用(如上述MIPS R10000),但在现代高性能处理器中,大多数已经演进为第三种方案——使用完全独立的统一物理寄存器文件。扩展ARF方案与统一PRF方案的本质区别在于概念上的分离程度:扩展ARF仍然将架构寄存器文件视为物理实体的一部分,而统一PRF则将架构寄存器完全抽象化——它们只是映射表中的条目,没有独立的物理存在。
设计权衡 1 — 扩展ARF方案的容量设计
扩展ARF方案的物理寄存器总数的选择需要在性能和面积之间权衡。中,个寄存器保存架构状态,剩余个作为重命名空间。重命名空间越大,越不容易发生物理寄存器耗尽导致的前端暂停。
以RISC-V()为例,不同的选择:
| 重命名空间 | 最大in-flight | 寄存器文件面积 | 评估 | |
|---|---|---|---|---|
| 48 | 16 | 16 | 基线 | 过小,频繁暂停 |
| 64 | 32 | 32 | 1.8 | 早期设计(如R10000) |
| 96 | 64 | 64 | 3.2 | 中等,适用于4-wide |
| 128 | 96 | 96 | 5.0 | 较大,适用于6-wide |
面积增长不是线性的——多端口寄存器文件的面积大致与到成正比(因为位线变长增加了字线驱动面积)。这是扩展ARF方案在大容量设计中的固有劣势,也是推动向统一PRF方案演进的因素之一——PRF方案可以更灵活地使用分体(banked)设计来缓解面积增长。
使用统一的PRF进行寄存器重命名
统一物理寄存器文件(Unified Physical Register File,PRF)方案是现代高性能处理器的主流选择。在这种方案中,逻辑寄存器和物理寄存器完全分离:ISA定义的架构寄存器没有物理实体(或者说,ARF变成了一个纯粹的映射概念),所有的值都存储在一个统一的物理寄存器文件中。重命名机制通过RAT将逻辑寄存器映射到物理寄存器。
物理寄存器文件(PRF)的概念
PRF是一个多端口的寄存器阵列,其容量远大于ISA定义的逻辑寄存器数量。PRF的容量直接决定了处理器能够支持的最大in-flight指令数量——因为每条产生结果的指令都需要占用一个物理寄存器。
硬件描述 3 — PRF容量的设计公式推导
PRF的容量的下界可以从以下推理推导。在任意时刻,物理寄存器的使用可以分为两类:
类别一:架构映射寄存器。提交RAT中记录的32个映射(对于RISC-V整数)指向32个物理寄存器。这些物理寄存器保存着已提交的架构状态,在处理器发生异常或中断时必须可用。因此,至少需要个物理寄存器永久保留给架构状态。
类别二:推测映射寄存器。每条in-flight指令(尚未提交的指令)如果有目标寄存器,则在重命名时从空闲列表分配了一个物理寄存器。这个物理寄存器在该指令提交之前不能被释放(因为如果发生错误恢复,旧的映射需要被恢复,而新分配的物理寄存器中的值将被丢弃)。设ROB中最多有条指令,其中约有的比例产生寄存器结果(即有目标寄存器),则推测映射最多占用个物理寄存器。
因此,PRF容量的基本约束为:
在典型的整数代码中,约70%–80%的指令有目标寄存器(Store和Branch指令没有),即。对于、的处理器:
这意味着至少需要224个物理寄存器才能保证256项ROB不会因PRF饥饿而成为瓶颈。实际上,的值因工作负载而异:浮点密集代码的接近0.9(几乎每条浮点指令都有目标寄存器),而控制流密集代码的可能低至0.6(大量分支指令)。保守设计应取的上界。
下面验证这个公式在实际处理器中的适用性。以Intel Skylake为例:(x86-64),,。理论最小PRF:。Skylake的实际整数PRF为180——略低于理论值,说明Skylake在某些高工作负载下可能偶尔发生PRF饥饿。这解释了为什么后续的Golden Cove将整数PRF大幅增加到280个。
实际设计中还需要考虑额外的余量:
检查点开销:如果使用空闲列表快照而非指针快照进行分支恢复,检查点期间的物理寄存器不能被回收,需要额外的个寄存器(为检查点数,为两个检查点间平均分配的物理寄存器数)。
SMT开销:如果处理器支持同时多线程(SMT),每个硬件线程需要独立的架构状态。2-way SMT需要个架构寄存器。Intel的超线程处理器通常为此增加约30%的PRF容量。
分配-释放延迟:物理寄存器从被指令使用完毕到被释放(在覆盖指令提交时)存在延迟。在这段延迟期间,物理寄存器处于"已使用完但尚未释放"的状态,增加了有效占用量。
综合考虑上述因素,实际的PRF容量设计公式为:
其中为SMT线程数(不支持SMT时为1),为设计余量(通常为10%–20%的总容量)。
| 处理器 | 整数PRF | 浮点/向量PRF | ROB容量 |
|---|---|---|---|
| Intel Skylake (2015) | 180 | 168 | 224 |
| Intel Golden Cove (2021) | 280 | 332 | 512 |
| Intel Lion Cove (2024) | 280+ | 332+ | 512+ |
| AMD Zen 4 (2022) | 224 | 192 | 320 |
| AMD Zen 5 (2024) | 240+ | 224+ | 448 |
| ARM Cortex-X3 (2022) | 320 | 256 | 320 |
| ARM Cortex-X925 (2024) | 384 | 384 | 384+ |
| 香山昆明湖 (2024) | 192 | 192 | 256 |
可以看到,现代高性能处理器的整数PRF容量在180–384个之间,约为ROB容量的50%–100%。PRF容量小于ROB容量是因为并非所有ROB中的指令都产生寄存器结果——store指令、branch指令等不写寄存器的指令不需要分配物理寄存器。
PRF在物理实现上是一个多读多写的SRAM或寄存器阵列。以一个6-wide处理器为例,每周期最多有6条指令同时读取操作数(12个读端口)和6条指令同时写回结果(6个写端口),PRF需要支持12R6W共18个端口。对于一个280项64位的PRF,这意味着每个存储单元需要连接18条位线——面积约为单端口SRAM的9–12倍,这是PRF设计中最大的物理挑战之一。
多端口SRAM单元的晶体管级分析
理解PRF面积瓶颈的根源需要深入到晶体管级别。一个标准的6T SRAM单元由两个交叉耦合的反相器(4个晶体管)和两个通过管(access transistor,2个晶体管)组成,提供1个读/写端口。要增加端口,需要为每个额外端口添加2个通过管和一对位线。
对于个读端口和个写端口,SRAM单元的晶体管数为:
其中是总端口数。对于12R6W的PRF,,每个SRAM单元需要个晶体管。作为对比,单端口SRAM单元仅需6个晶体管——40个晶体管是6个的约6.7倍。
但面积增长并非仅由晶体管数量决定。多端口SRAM的面积瓶颈在于布线:条位线需要在存储阵列中纵向排列,每条位线需要一定的最小间距(受工艺设计规则限制)。在5nm工艺中,金属最小间距约为28nm,36条位线需要的水平宽度。加上条字线的垂直排列,每个存储单元的面积约为。
对于280项64位的PRF,总存储面积为。加上地址解码器、灵敏放大器、写驱动器等外围电路(约占总面积的40%–60%),PRF总面积约。在5nm工艺的典型处理器核面积()中,这不到0.5%——PRF的面积在现代工艺下已不是主要限制,时序(位线充放电延迟和灵敏放大器建立时间)才是真正的瓶颈。
硬件描述 4 — PRF的时序分析
PRF读取的时序由以下路径决定:
地址解码:将物理寄存器编号(9位)解码为280条字线中的一条。延迟约级门延迟,实际实现中使用预解码和分层解码将其缩短到4–5级。
字线驱动:驱动选中的字线,打开该行所有存储单元的通过管。字线长度约(64位位线间距双端),驱动延迟约1–2个FO4门。
位线充放电:存储单元通过通过管驱动位线上的电荷。位线长度约(280行行间距),延迟约2–4个FO4门。这是PRF读取时序的关键路径。
灵敏放大器:在位线和位线bar之间的微小电压差(50mV)达到灵敏放大器的输入灵敏度后,放大器将其放大到全摆幅(0到)。延迟约1–2个FO4门。
总读取延迟约个FO4门(5nm)。在3GHz处理器中(),这占了一个周期的约18%–25%,留下足够的余量给操作数旁路选择和执行单元的建立时间。但在5GHz处理器中(),PRF读取可能占据一个周期的30%–42%,时序变得紧张。
为了缓解多端口带来的面积和时序压力,现代处理器采用了多种优化技术。
分体PRF(Banked PRF)
分体PRF是最常用的面积优化技术。其基本思想是将PRF按物理寄存器编号分为多个bank,每个bank独立管理一部分物理寄存器。例如,将280个物理寄存器分为4个bank,每个bank包含70个寄存器:Bank 0存储P0–P69,Bank 1存储P70–P139,Bank 2存储P140–P209,Bank 3存储P210–P279。
每个bank是一个独立的多端口SRAM或寄存器阵列,端口数量与全局端口数相同(12R+6W),但每个bank的容量只有总容量的(为bank数)。由于SRAM面积与大致成正比,将280项的12R6W PRF分为4个bank,每个bank为70项12R6W,总面积约为:
——从容量角度看面积不变。但分体带来的实际好处在于减少位线长度:每个bank的位线只有全局PRF的长度,信号传播延迟减少,充放电能量降低。位线延迟通常与长度的平方成正比(延迟),因此4-bank的位线延迟仅为全局PRF的,显著改善了时序。
分体PRF引入的额外复杂度是bank冲突(bank conflict):当同一周期内多个读或写请求指向同一bank时,可能超过该bank的端口数量。然而,由于物理寄存器编号由空闲列表按FIFO顺序分配,连续分配的物理寄存器倾向于均匀分布在各bank中(前提是初始空闲列表内容是跨bank交错排列的),bank冲突的概率较低。实测数据表明,4-bank设计的平均冲突率不到3%,对IPC的影响可忽略不计。
硬件描述 5 — 分体PRF的物理布局
在芯片布局中,分体PRF的每个bank通常放置在靠近其主要用户的位置。例如:
Bank 0和Bank 1靠近整数ALU执行单元,减少数据传输延迟。
Bank 2靠近乘法/除法单元。
Bank 3靠近Load/Store单元和数据缓存。
这种靠近使用者的布局策略使得最频繁的读写路径(操作数读取和结果写回)的金属线长度最短,进一步改善时序和降低功耗。但它也增加了跨bank数据传输的需求——当一条指令的源操作数在不同bank中时,需要跨bank的数据总线传输。现代处理器通过在bank之间铺设专用的数据总线(通常为64位端口数的宽度)来解决这个问题。
读端口缩减技术
PRF的读端口数量是面积的主要瓶颈。以6-wide处理器为例,每条指令最多2个源操作数,理论上需要个读端口。但实际中有多种情况使得PRF读取不需要发生:
旁路转发(bypass/forwarding):当一条指令的结果在执行完成后的同一周期被后续指令需要时,结果可以通过旁路网络直接从执行单元的输出端转发到后续指令的输入端,不需要先写入PRF再读出。在典型程序中,约30%–50%的操作数通过旁路获取。
操作数为立即数:许多指令的一个源操作数是立即数(如
ADDI、LW的偏移量),不需要从PRF读取。在RISC-V整数代码中,约40%的指令至少有一个立即数操作数。操作数为零寄存器:RISC-V的
x0硬连线为0,读取x0不需要访问PRF。约5%–10%的源操作数引用x0。操作数相同:如
ADD x1, x2, x2,两个源操作数引用同一物理寄存器,只需要一次PRF读取(结果被共享给两个输入端口)。
综合上述因素,每周期实际需要的PRF读取次数约为次。因此,一些处理器将PRF读端口设计为8–10个(而非理论最大的12个),并通过仲裁逻辑处理偶尔的端口冲突。当冲突发生时,被冲突的指令延迟一个周期发射。统计表明,将读端口从12减少到8,IPC损失不到1%,但PRF面积减少约20%。
| 读端口数 | 冲突率 | IPC损失 | PRF面积节省 |
|---|---|---|---|
| 12(理论最大) | 0% | 0% | 基线 |
| 10 | 1% | 0.5% | 12% |
| 8 | 2%–4% | 0.5%–1% | 22% |
| 6 | 8%–15% | 3%–5% | 30% |
PRF读端口缩减的性能与面积影响
从表表 24.16可以看出,从12个读端口缩减到8个是一个很好的性价比甜点——IPC损失不到1%,面积节省22%。进一步缩减到6个端口时,冲突率快速上升,IPC损失变得不可忽视。大多数商业处理器的实际PRF读端口数介于8–12之间。
写端口合并与流水化写入
PRF的写端口数量等于每周期的最大写回数量。在6-wide处理器中,理论上需要6个写端口(6条指令同时完成写回)。但实际中,多条指令在同一周期完成的概率取决于执行单元的延迟分布:
1周期延迟的ALU操作密集时,写回冲突频率高。
多周期延迟的Load(4–5周期)和乘法(3周期)错开完成时间,自然减少了同时写回的概率。
一种优化技术是写端口复用:将某些功能单元的写回安排在不同的半周期(利用时钟上升沿和下降沿),使得物理上每个写端口在一个时钟周期内可以被两个功能单元共享。这将有效写端口数量翻倍(从物理上的3个端口变为逻辑上的6个端口),但需要更复杂的时钟树设计和更严格的建立/保持时间约束。
逻辑寄存器到物理寄存器的映射
寄存器别名表(Register Alias Table,RAT)是PRF方案的核心控制结构。RAT维护逻辑寄存器到物理寄存器的映射关系,是重命名阶段的关键路径所在。
在统一PRF方案中,实际上存在两个映射表:
推测RAT(Speculative RAT,也称为Front-end RAT或Rename RAT):反映当前推测执行状态下的映射关系。每条新指令进入重命名阶段时,会读取并更新推测RAT。
提交RAT(Committed RAT,也称为Architectural RAT或Retirement RAT):反映已提交指令建立的映射关系,即架构可见的确定状态。只有在指令从ROB头部提交时,才会更新提交RAT。
推测RAT的硬件实现细节
推测RAT(sRAT)通常实现为一个以逻辑寄存器编号为索引的SRAM或寄存器阵列。对于RISC-V(32个整数逻辑寄存器)映射到280个物理寄存器的情况,RAT有32个条目,每个条目位,总容量为位——极其紧凑。然而,RAT的面积瓶颈不在于存储容量,而在于多端口需求和更新逻辑。
从电路级来看,sRAT的每个存储单元可以采用两种实现方式。第一种是标准的多端口SRAM单元:每个端口需要一对位线(bitline和bitline-bar)和一个访问晶体管,一个端口的SRAM单元需要个访问晶体管加上2个反相器(4个晶体管)组成的存储核心,总共个晶体管。对于12R+6W=18端口的sRAT,每个存储单元需要个晶体管。32条目9位40晶体管个晶体管——绝对数量不大,但每个单元40个晶体管的面积使得位线间距和字线间距显著增大,时序也随之恶化。
第二种是寄存器阵列(register file)实现:使用标准锁存器(latch)或触发器(flip-flop)作为存储单元,每个读端口通过独立的多路选择器(MUX)从32个条目中选择一个。写端口通过地址解码器和写使能信号控制。寄存器阵列方式的面积通常大于SRAM方式,但时序更好(因为MUX的延迟比SRAM的位线灵敏放大器延迟更可预测),在小容量结构中(如32条目的RAT)是更常见的选择。
推测RAT的端口需求如下:
读端口:在-wide的处理器中,每周期最多条指令进入重命名阶段,每条指令最多2个源操作数,因此需要个读端口来查询源操作数的映射。
写端口:每周期最多条指令更新映射,需要个写端口。
同周期相关性检测:在同一个周期中重命名的条指令之间可能存在相关性——例如指令写
x3,指令读x3。应该读取的新映射而非RAT中的旧映射。这需要在RAT的读写端口之间引入旁路逻辑(bypass),增加了关键路径延迟。
对于一个6-wide的处理器,推测RAT需要12个读端口和6个写端口,加上6路旁路逻辑——这使得RAT的访问延迟成为重命名阶段的关键路径,通常需要1个完整的流水线周期。
重命名阶段的关键路径分析
在一个6-wide处理器中,重命名阶段需要在一个时钟周期内完成以下操作的完整序列:
读取源映射(RAT读取):6条指令的12个源寄存器同时查询推测RAT,获取它们当前映射到的物理寄存器编号。这需要12个并行的RAT读端口。延迟:约2–3个FO4门延迟(寄存器阵列的多路选择器延迟)。
同周期指令间相关性检测:检查6条指令之间是否存在RAW相关——即后面的指令的源寄存器是否是前面指令的目标寄存器。这需要检测所有可能的写-读对:写 –读、写 –读、。总共个5位比较器(每条指令2个源操作数)。比较器本身延迟约1个FO4门,但优先级编码(后面的写覆盖前面的写)增加级延迟。
分配物理寄存器(空闲列表出队):为最多6个目标寄存器从空闲列表分配新的物理寄存器编号。需要从FIFO头部并行读取最多6个条目。延迟:约1–2个FO4门(FIFO读取+前缀和计算)。
旁路选择(bypass MUX):对于每个源操作数,在RAT读取结果和同周期前序指令的分配结果之间选择正确的映射。这是一个:1的多路选择器链,延迟约2–3个FO4门。
更新推测RAT(RAT写入):将最多6个目标寄存器的新映射写入RAT。延迟:约1个FO4门(写使能+数据写入)。
上述操作的关键路径是步骤124(或步骤345),总延迟约为6–8个FO4门。在5nm工艺下,1个FO4门延迟约为5–7ps,因此重命名阶段的关键路径延迟约为30–56ps。在3GHz的处理器中(时钟周期333ps),这留下了充足的余量,允许重命名阶段在一个周期内完成。但在更高频率的设计中(如5GHz,时钟周期200ps),重命名可能需要分为两个流水段:第一段进行RAT读取和空闲列表分配,第二段进行旁路选择和RAT写入。
双周期重命名的代价是增加了一个流水段——这将分支预测错误的惩罚增加1个周期(从正确路径取指到第一条指令到达执行单元的延迟增加1个周期)。在分支预测准确率为96%的处理器中,每100条指令约有2次预测错误,每次多1个周期的惩罚,总额外惩罚为2个周期/100条指令个周期/条指令。对于IPC=4的处理器,这约相当于的CPI增加——不可忽视。因此,处理器设计者通常尽力将重命名保持在单周期内完成,只有在时钟频率目标极高时才选择双周期方案。
硬件描述 6 — RAT的旁路逻辑
考虑在同一周期重命名的四条指令:
I1: add x1, x2, x3 # 写x1, 分配P40
I2: sub x4, x1, x5 # 读x1 -- 应读I1的映射P40
I3: mul x1, x6, x7 # 写x1, 分配P41
I4: add x8, x1, x9 # 读x1 -- 应读I3的映射P41I2读x1时,不能从RAT中读取(那里是旧的映射),而必须从同周期的I1获取新映射P40。I4读x1时,不能从I1获取(已被I3覆盖),而必须从I3获取P41。这需要一个优先级编码的旁路网络:对于I2的x1读取,检查I1是否写x1——是,则使用I1的新映射;对于I4的x1读取,检查I3和I1是否写x1——I3更晚,使用I3的新映射。
在硬件实现中,这个旁路逻辑通常由个比较器和优先级选择器组成。对于6-wide处理器,需要个比较器。每个比较器比较5位的逻辑寄存器编号(RISC-V),延迟约为1个逻辑门延迟。但级联的优先级选择器(后面的指令需要检查前面所有指令的写入)形成了一条的关键路径,在6-wide时约为4–5个逻辑门延迟。
以下代码展示了6-wide dispatch间RAW依赖检测矩阵的简化SystemVerilog实现。6条指令的每条源操作数需要与前面所有指令的目的操作数比较,总共需要个比较器。
module raw_detect_6wide #(
parameter W = 6, // dispatch width
parameter LREG = 5 // logical register bits (RISC-V)
)(
input logic [LREG-1:0] dst [W], // dest regs of I0..I5
input logic has_dst[W], // does instr write a dest?
input logic [LREG-1:0] src1 [W], // first source of I0..I5
input logic [LREG-1:0] src2 [W], // second source of I0..I5
output logic [W-1:0] match_src1 [W], // one-hot: which older
// instr dst matches src1?
output logic [W-1:0] match_src2 [W] // same for src2
);
// 30 comparators: for each later instr j, compare its src
// against each earlier instr i's dst (i < j)
always_comb begin
for (int j = 0; j < W; j++) begin
match_src1[j] = '0;
match_src2[j] = '0;
for (int i = 0; i < j; i++) begin
match_src1[j][i] = has_dst[i]
& (dst[i] == src1[j]);
match_src2[j][i] = has_dst[i]
& (dst[i] == src2[j]);
end
end
end
endmodule
// Priority select: pick the LATEST matching producer
module raw_priority_sel #(
parameter W = 6,
parameter PREG = 7
)(
input logic [W-1:0] match [W], // from raw_detect
input logic [PREG-1:0] new_preg[W], // newly allocated pregs
input logic [PREG-1:0] rat_rd [W], // RAT read results
output logic [PREG-1:0] resolved[W] // final physical reg
);
always_comb begin
for (int j = 0; j < W; j++) begin
resolved[j] = rat_rd[j]; // default: use RAT
// scan from j-1 down to 0, last match wins
for (int i = 0; i < j; i++) begin
if (match[j][i])
resolved[j] = new_preg[i];
end
end
end
endmodule上述实现中,raw_detect_6wide模块包含30个5位比较器(,因为每条指令有两个源操作数),raw_priority_sel模块通过for循环隐含了优先级链——综合工具会将其映射为级联MUX结构。对于6-wide处理器,优先级链深度为5级MUX(I5需要检查I0–I4的所有目的寄存器)。
提交RAT的实现。提交RAT的结构与推测RAT类似,但其更新发生在提交阶段——流水线的最后阶段,通常不在关键路径上。提交RAT的主要用途有两个:
错误恢复:当分支预测错误或异常发生时,将提交RAT的内容复制到推测RAT,瞬间恢复到已提交的架构状态。这是一个单周期操作(32条目9位 = 288位的并行复制),比逐条回退ROB快得多。
物理寄存器释放:在提交时,提交RAT记录的逻辑寄存器旧映射是可以被释放的物理寄存器(见下一节详述)。
空闲列表(Free List)。空闲列表管理所有当前未被使用的物理寄存器。它通常实现为一个FIFO队列,初始时包含所有物理寄存器编号(除去初始映射给逻辑寄存器的那些)。每当重命名阶段需要一个新的物理寄存器时,从空闲列表头部弹出一个编号;每当提交阶段释放一个物理寄存器时,将其编号推入空闲列表尾部。
当空闲列表为空时,处理器无法为新指令分配物理寄存器,必须暂停重命名——这称为PRF饥饿(PRF starvation)。PRF饥饿是独立于ROB满的另一个流水线阻塞原因。在实际处理器中,通过合理设计PRF容量(使其略大于ROB中需要目标寄存器的最大指令数),可以使PRF饥饿极少发生。
空闲列表的批量分配与释放
在-wide处理器中,空闲列表每周期需要支持最多次并行分配(重命名条指令)和最多次并行释放(提交条指令)。以6-wide处理器为例,这意味着一个时钟周期内从FIFO头部弹出6个物理寄存器编号,同时向FIFO尾部推入6个编号。
批量分配的硬件实现需要处理一个关键问题:同一周期内重命名的条指令中,有些可能没有目标寄存器(如Store、Branch指令),不需要分配物理寄存器。因此,实际分配数量,需要动态确定。
一种常见的实现方式是压缩分配(compacted allocation):首先确定条指令中哪些需要分配(通过检查HasDst位),然后将需要分配的指令压缩到连续的分配槽中。例如,如果6条指令中第1、3、4、6条需要分配(第2、5条为Store),则分配逻辑将它们映射到FIFO的位置、、、,头指针前进4步而非6步。
压缩分配的硬件实现需要一个前缀和(prefix sum)电路来计算每条指令的分配位置。以6-wide为例,定义如果第条指令需要分配,否则。第条指令的FIFO位置为。前缀和可以通过一个6输入的加法树计算,延迟约为级全加器。在现代工艺下,这大约对应0.1–0.2ns的延迟,通常不在重命名阶段的关键路径上。
硬件描述 7 — 空闲列表的硬件实现
空闲列表是PRF重命名方案中与RAT同等重要的控制结构。其硬件实现通常有两种方式:
(1)FIFO队列方式。空闲物理寄存器的编号存储在一个循环FIFO队列中,由头指针(分配端)和尾指针(释放端)管理。每周期最多从头部弹出个编号(为重命名宽度),从尾部推入个编号(为提交宽度)。FIFO方式的优势是简单、时序好;劣势是在分支预测错误恢复时,需要将已分配但未提交的物理寄存器编号重新放回队列,这需要额外的恢复逻辑。
对于6-wide处理器,FIFO需要支持6个并行出队和6个并行入队。一个包含160个条目(192个物理寄存器32个逻辑寄存器)的FIFO,每个条目8位(),总容量为位。
(2)位图(bitmap)方式。使用一个位向量表示每个物理寄存器是否空闲。位向量长度等于物理寄存器总数,每位对应一个物理寄存器:1表示空闲,0表示已分配。分配操作通过优先级编码器(priority encoder)从位向量中找到个值为1的位位置;释放操作直接将对应位置1。
位图方式的优势是恢复简单——只需根据ROB中被清除表项的将对应位置1即可。劣势是优先级编码器的延迟随物理寄存器数量增长:对于280个物理寄存器,找到6个空闲寄存器需要一个280-bit6的优先级编码器,延迟约为级逻辑门。
| 特征 | FIFO队列 | 位图 |
|---|---|---|
| 分配延迟 | 1–2级门(指针读取) | 8–10级门(优先级编码) |
| 释放延迟 | 1级门(指针写入) | 1级门(置位) |
| 分支恢复 | 复杂(需回退指针或重建) | 简单(直接置位) |
| 存储开销 | 位 | 位 |
| 面积(192 PRF) | 1280位 | 192位 + 编码器 |
现代处理器通常使用FIFO方式,并配合检查点机制在分支恢复时快速恢复空闲列表状态——在每个分支指令处保存FIFO指针的快照,预测错误时恢复指针即可。
空闲列表FIFO的分支恢复细节
空闲列表在分支恢复时的正确行为需要仔细设计。当分支预测错误发生时,需要将错误路径上分配的物理寄存器归还到空闲列表。使用FIFO方式时,最简洁的恢复方法是指针恢复:在每个分支指令处保存FIFO头指针的快照(尾指针由提交逻辑管理,不受恢复影响)。恢复时,将头指针重置为分支处保存的快照值——这样,分支后分配的所有物理寄存器编号自动"回到"FIFO中(因为头指针回退了)。
然而,指针恢复有一个微妙的正确性问题:在分支指令之后、恢复发生之前的这段时间内,提交逻辑可能已经释放了一些物理寄存器(将编号推入FIFO尾部),导致FIFO尾指针前进。如果仅恢复头指针而不处理尾指针的变化,FIFO中可能包含一些重复的编号(分支后分配又通过头指针恢复"放回"的编号,以及通过正常提交释放的编号恰好是同一个)。为了避免这种情况,需要确保分支之后通过提交释放的物理寄存器不是分支之后分配的物理寄存器——这在大多数情况下自然满足,因为提交的指令在程序顺序上先于分支。
在极端情况下(分支前的指令在分支后才提交),需要额外的检查逻辑来避免FIFO中的编号重复。一种保守但安全的做法是在恢复时暂停提交逻辑一个周期,确保恢复操作原子地完成。
另一种更激进的恢复方法是完全重建空闲列表:扫描提交RAT中所有映射到的物理寄存器,将不在提交RAT中的所有物理寄存器标记为空闲。这种方法在功能上正确,但需要位的位向量运算和优先级编码器,延迟较长(通常需要2–3个周期),不适合频繁的分支恢复。它通常作为异常恢复的后备方案。
空闲列表容量不足的处理策略
当空闲列表中的可用条目数量低于重命名宽度时,处理器面临部分暂停(partial stall)的选择:是暂停所有条指令的重命名,还是只重命名可用数量的指令、暂停其余?
全暂停策略(full stall):如果空闲列表中只有个条目,则暂停重命名阶段一个周期,等待提交释放新的物理寄存器。下一周期重试。优点是实现简单;缺点是即使可以重命名条指令(其中有条有目标寄存器、条无目标寄存器),也全部暂停,浪费了无目标寄存器指令(如Store、Branch)的处理机会。
部分重命名策略(partial rename):重命名前条需要分配物理寄存器的指令,暂停第条及之后需要分配的指令。不需要分配物理寄存器的指令(无目标寄存器)可以继续重命名。优点是充分利用可用资源;缺点是实现复杂——需要动态确定哪些指令需要分配、维护部分重命名的中间状态。
大多数商业处理器采用全暂停策略(或在检测到空闲列表剩余不足个条目时提前暂停),因为PRF饥饿在合理设计下极为罕见(0.5%的周期),部分重命名带来的性能收益不值得额外的硬件复杂度。
图图 24.14展示了统一PRF方案中所有控制和数据结构之间的交互关系。
PRF重命名的操作流程
图图 24.15展示了统一PRF方案的完整操作流程的详细时序。
PRF方案与ROB方案的流水线对比
PRF方案与ROB方案在流水线结构上的主要区别在于:PRF方案中操作数读取和结果写回都发生在统一的PRF上,而ROB方案需要在ARF和ROB之间选择。图图 24.16对比了两种方案的典型流水线结构。
从图图 24.16可以看出PRF方案的两个关键优势:(1)操作数读取阶段只需访问PRF一个位置,不需要在ARF和ROB之间通过MUX选择,简化了数据通路并减少了延迟;(2)提交阶段不需要数据复制(ROB方案需要将64位Value从ROB复制到ARF),只需更新提交RAT(9位写入)和释放旧物理寄存器(将编号推入空闲列表),操作极其轻量。
下面用一个具体的例子来详细追踪PRF重命名的全过程。假设处理器有64个物理寄存器(P0–P63),初始状态下逻辑寄存器x1映射到P1,x4映射到P4,x5映射到P5,空闲列表中下一个可用的物理寄存器是P32。
add x1, x2, x3 # I1
sub x4, x1, x5 # I2
mul x1, x6, x7 # I3I1重命名:add x1, x2, x3
查推测RAT:
x2P2,x3P3。源操作数将从P2和P3读取。分配物理寄存器:从空闲列表取出P32。
更新推测RAT:
x1P32(旧映射=P1,记录到ROB表项中)。重命名后的指令:
add P32, P2, P3。
I2重命名:sub x4, x1, x5
查推测RAT:
x1P32(已被I1更新!),x5P5。分配物理寄存器:从空闲列表取出P33。
更新推测RAT:
x4P33(=P4)。重命名后的指令:
sub P33, P32, P5。
I3重命名:mul x1, x6, x7
查推测RAT:
x6P6,x7P7。分配物理寄存器:从空闲列表取出P34。
更新推测RAT:
x1P34(=P32)。重命名后的指令:
mul P34, P6, P7。
图图 24.17用时序快照的方式展示了重命名过程中RAT和空闲列表的状态变化。
重命名完成后,RAT和ROB的状态如表表 24.18所示。
| 逻辑寄存器 | 推测RAT映射 | 提交RAT映射 |
|---|---|---|
| x1 | P34 (I3的结果) | P1 (原始值) |
| x4 | P33 (I2的结果) | P4 (原始值) |
| x2 | P2 | P2 |
| x3 | P3 | P3 |
| x5 | P5 | P5 |
| x6 | P6 | P6 |
| x7 | P7 | P7 |
PRF重命名追踪后的RAT和ROB状态
| ROB# | 指令 | Dst | Done | ||
|---|---|---|---|---|---|
| 0 | I1: add | x1 | P32 | P1 | 0 |
| 1 | I2: sub | x4 | P33 | P4 | 0 |
| 2 | I3: mul | x1 | P34 | P32 | 0 |
ROB表项中记录的旧映射信息
提交与物理寄存器释放。当I1从ROB头部提交时:
更新提交RAT:
x1P32。释放=P1到空闲列表。P1中的旧值不再被任何指令需要。
释放ROB#0。
当I2提交时:
更新提交RAT:
x4P33。释放=P4到空闲列表。
当I3提交时:
更新提交RAT:
x1P34。释放=P32到空闲列表。注意P32是I1分配的物理寄存器——I1的结果值已经被I3覆盖,I2是最后一个读取P32的指令,此时I2已经提交,所以P32可以安全释放。
物理寄存器的完整生命周期追踪
为了更清晰地理解物理寄存器的生命周期,下面用时间轴追踪上述示例中P32的完整生命。
| 时刻 | 事件 | P32状态 | 空闲列表 |
|---|---|---|---|
| 系统初始化 | 空闲 | ||
| I1重命名:x1P32 | 已分配,推测写入目标 | ||
| I1执行完成,结果写入 | 保存I1的结果值 | ||
| I2发射,从P32读取值 | 被I2读取 | ||
| I3重命名:x1P34 | 成为,等待释放 | ||
| I1提交 | 仍在使用(I2、I3尚未提交) | ||
| I2提交 | 仍在使用(I3尚未提交) | ||
| I3提交,释放P32 | 空闲 |
物理寄存器P32的完整生命周期
从表表 24.20可以看出,P32从被分配到被释放,生命周期为9个时间单位。在这期间:
–:P32已分配但尚未写入有效数据(I1正在执行)。此阶段P32的旧值已无效,新值尚未产生——如果此时发生分支恢复,P32会被归还空闲列表。
–:P32保存I1的结果值,是
x1的当前映射。所有后续读取x1的指令(如I2)都通过P32获取值。–:I3重命名后,
x1的映射变为P34,P32不再是x1的当前映射。但P32仍然保存着I1的结果值,且I2可能尚未读取该值。P32成为"等待释放"状态——它不能被分配给新指令(因为I2可能还需要读取它),但也不在空闲列表中。:I3提交时释放P32。此时I1和I2都已提交,没有任何in-flight指令需要P32的值。
这个追踪清楚地表明:物理寄存器的生命周期由两个事件界定——分配(被某条指令重命名为目标寄存器时)和释放(之后下一条写入同一逻辑寄存器的指令提交时)。生命周期的长度等于和在提交顺序中的距离,这取决于程序中同一逻辑寄存器被连续写入的频率。
物理寄存器生命周期的统计分析
物理寄存器的平均生命周期直接决定了PRF的有效利用率。定义重写距离(rewrite distance)为两条连续写入同一逻辑寄存器的指令之间的指令数(以提交顺序度量)。物理寄存器的平均生命周期等于所有逻辑寄存器的平均重写距离:
在均匀随机的目标寄存器分布下,重写距离的期望值等于(逻辑寄存器数量),因为每条指令有的概率覆盖同一个寄存器。但在实际程序中,少数"热"寄存器(如循环变量、函数返回值寄存器)被频繁重写,导致这些寄存器的远小于,而"冷"寄存器(如被调用者保存的寄存器)的远大于。
这种不均匀分布对PRF容量设计有重要影响。如果所有逻辑寄存器的重写距离都相同(均为),那么稳态下每个逻辑寄存器恰好占用一个物理寄存器(保存当前值),加上个物理寄存器处于"等待释放"状态,PRF需要个物理寄存器。但在实际中,少数热寄存器的很小(频繁释放,物理寄存器快速周转),大量冷寄存器的很大(长期占用物理寄存器)。冷寄存器对应的物理寄存器成为"长期驻留者",减少了空闲列表中可用的物理寄存器数量。因此,PRF容量的设计需要考虑最坏情况——当多个冷寄存器同时被函数调用链使用时(如深度递归),它们的物理寄存器长期不被释放,可能导致PRF饥饿。这就是为什么实际PRF容量需要在理论最小值的基础上增加20%–30%的余量。
| 寄存器类型 | 平均重写距离 | 说明 |
|---|---|---|
| x1 (ra) | 80–150 | 函数调用时重写,距离取决于函数大小 |
| x2 (sp) | 40–80 | 函数入口/出口时重写 |
| x10–x17 (a0–a7) | 8–20 | 函数参数/返回值,频繁重写 |
| x5–x7 (t0–t2) | 5–15 | 临时寄存器,最频繁重写 |
| x8–x9 (s0–s1) | 100–500 | 被调用者保存,重写罕见 |
| x18–x27 (s2–s11) | 200–1000 | 被调用者保存,重写极罕见 |
| 加权平均 | 15–30 | 以目标寄存器使用频率加权 |
SPEC CPU 2017中典型逻辑寄存器的平均重写距离
从表表 24.21可以看出,临时寄存器和参数寄存器的重写距离仅为5–20条指令,这些寄存器占了目标寄存器使用的大部分(约60%–70%)。因此,大多数物理寄存器的生命周期相对较短(15–30个提交周期),PRF的周转较快。但被调用者保存的寄存器可能有数百甚至上千条指令的重写距离——如果这些寄存器在函数入口处被保存到栈上(SD s0, offset(sp)),它们对应的物理寄存器在函数执行期间一直被占用,成为"长期驻留"的物理寄存器,降低了PRF的有效容量。
性能分析 3 — 物理寄存器周转率与PRF利用效率
物理寄存器的周转率(turnover rate)定义为每周期被释放(并可重新分配)的物理寄存器数量。周转率越高,空闲列表中可用的物理寄存器越充足,PRF饥饿的概率越低。
周转率取决于两个因素:(1)提交带宽——每周期提交的写寄存器指令数量;(2)寄存器重写间距——两条连续写入同一逻辑寄存器的指令之间的间距。
在典型的6-wide处理器中,每周期提交约4–5条指令(受限于ROB头部的连续完成指令数),其中约70%有目标寄存器,因此每周期释放约个物理寄存器。同时,重命名阶段每周期分配约个物理寄存器。分配速率大于释放速率——这是正常的,因为in-flight指令数量在增长。当ROB接近满时,提交速率和分配速率趋于平衡(稳态)。
如果PRF容量设计得当(如,空闲列表初始个条目),在稳态下空闲列表中通常保持个可用条目。对于周期(指令从分配到提交的平均延迟),空闲列表中保持约个条目——远离耗尽,PRF饥饿概率极低。
物理寄存器释放时序的正确性证明
物理寄存器释放的正确时机是PRF方案中最微妙的部分。规则是:当一条写入逻辑寄存器的指令提交时,释放该逻辑寄存器在之前的映射。
正确性证明。设是在之前最后一条写入逻辑寄存器的已提交指令,重命名时被分配的物理寄存器为。需要证明在提交时刻,没有任何指令仍需要读取中的值。
是分配的物理寄存器,在程序顺序中先于。
提交时,之前的所有指令(包括)都已经提交(因为ROB按程序顺序提交)。
需要中值的指令集合。中的所有指令在程序顺序中位于和之间。
因为已提交,中的所有指令都已提交(之前的指令都已提交)。
已提交的指令不再需要从PRF读取操作数(它们早已执行完成)。
在之后的指令如果读取,它们看到的是的映射而非(因为更新了映射)。
因此,不再被任何in-flight或已提交指令需要,可以安全释放。
反例分析——过早释放的危险。如果在执行完成(而非提交)时就释放,会发生什么?考虑以下场景:
add x1, x2, x3 # I_i: x1 -> P40, 已执行完成
beq x5, x6, label # I_br: 分支,预测为不跳转
sub x4, x1, x7 # I_k: 读x1=P40, 在I_br之后
mul x1, x8, x9 # I_j: x1 -> P41, P_old=P40, 已执行完成如果在执行完成时释放P40,而此时尚未提交(分支预测尚未确认),则P40可能被分配给一条新指令并被的结果覆盖。如果之后发现预测错误,需要恢复到之前的状态——此时=x1的正确值应该在P40中,但P40已经被覆盖,数据永久丢失。这就是为什么释放必须等到提交时刻——提交保证了之前的所有指令(包括所有分支)都已经被正确确认。
反例分析——过晚释放的代价。如果延迟释放(例如等到下一条写入的指令提交时才释放),则物理寄存器的有效占用时间延长,空闲列表中可用的物理寄存器减少,更容易发生PRF饥饿。虽然过晚释放不会导致功能错误,但会降低性能。因此,在提交时立即释放是在正确性约束下的最早释放时机——这是最优的设计选择。
设计提示
物理寄存器的生命周期可以用一个简洁的状态机描述:空闲推测写入等待释放空闲。一个物理寄存器从被分配到被释放,经历的时间等于两条连续写入同一逻辑寄存器的指令之间的"距离"(以提交为度量)。这个距离越长,物理寄存器的周转率越低,对PRF容量的压力越大。在循环代码中,如果循环变量每次迭代都被重写(如ADDI x10, x10, 4),则对应物理寄存器的生命周期等于循环体的长度——循环体越长,需要的物理寄存器越多。
错误恢复流程。当分支预测错误被检测到时(通常在执行阶段),处理器需要:
清除推测状态:丢弃分支指令之后所有in-flight指令的ROB表项和发射队列条目。
恢复推测RAT:将提交RAT的内容复制到推测RAT。这使得后续从正确路径取来的指令将使用正确的映射关系。
回收物理寄存器:将分支指令之后所有已分配但未提交的物理寄存器归还到空闲列表。这些物理寄存器可以通过遍历被丢弃的ROB表项中的字段来确定。
使用提交RAT恢复是最简单的方法,可以在一个周期内完成(288位的并行复制)。然而,它的缺点是恢复到提交点而非分支点——分支指令与已提交指令之间可能存在大量已正确执行但尚未提交的指令,这些指令的映射信息也被丢弃了,需要重新执行。更高级的恢复机制使用RAT检查点(checkpoint):在每个分支指令处保存推测RAT的快照,预测错误时恢复到分支点的快照,保留分支之前已正确执行的指令的进度。检查点机制的代价是额外的存储和管理逻辑,但在分支密集的代码中可以显著减少恢复开销。
提交RAT恢复与检查点恢复的性能对比
两种恢复方式对性能的影响差异可以用以下模型量化。设分支预测错误发生时:
错误分支到ROB头部(最早已提交指令)之间有条未提交指令。
错误分支到重命名阶段当前位置之间有条已重命名但未执行的指令。
提交RAT恢复:恢复到提交点,条指令的工作全部丢失。这些指令中,有些已经执行完成(结果在PRF中),但它们的映射关系已恢复到提交RAT的状态。后续前端需要从正确路径开始重新取指、解码、重命名和执行这条指令加上正确路径的新指令。恢复惩罚周期。
检查点恢复:恢复到分支点的检查点,只有分支之后的条指令的工作被丢失。分支之前已执行完成的指令不受影响,它们的结果仍在PRF中,映射关系由检查点正确恢复。恢复惩罚周期。
在一个典型的6-wide处理器中,约为30–60条指令(取决于ROB中已完成但等待提交的指令数量),而约为。因此,检查点恢复与提交RAT恢复之间的惩罚差异约为个周期——在分支预测错误率为5%(即每1000条指令10次错误预测)的程序中,这意味着每1000条指令节省个周期,IPC提升约。这就是为什么现代处理器普遍采用检查点恢复而非提交RAT恢复。
| 特征 | 提交RAT恢复 | 检查点恢复 |
|---|---|---|
| 恢复延迟 | 1周期(RAT复制) | 1周期(检查点载入) |
| 恢复点 | 提交点(最旧已提交指令后) | 分支点(错误分支处) |
| 被丢弃的有效工作 | 分支之前已执行的指令全部丢弃 | 仅丢弃分支之后的指令 |
| 存储开销 | 0(提交RAT本身已存在) | 位(个检查点) |
| 适用场景 | 异常恢复(罕见事件) | 分支预测错误恢复(频繁事件) |
| 前端重启延迟 | 较长(需从远处重新取指) | 较短(从分支点正确路径取指) |
两种恢复方式的详细对比
硬件描述 8 — RAT检查点的存储代价
每个RAT检查点需要存储推测RAT的完整副本。对于RISC-V(32个整数逻辑寄存器)映射到192个物理寄存器的情况,每个检查点需要位。如果处理器维护个检查点,总存储代价为位。此外还需要保存空闲列表的快照(通常只需保存FIFO指针,约20位)。
| 检查点数 | RAT存储 | 空闲列表指针 | 总计 |
|---|---|---|---|
| 4 | 1024位 | 80位 | 1104位 |
| 8 | 2048位 | 160位 | 2208位 |
| 16 | 4096位 | 320位 | 4416位 |
检查点数量限制了处理器能够同时追踪的未决分支数量。当in-flight的未决分支数量超过时,前端必须暂停,不再取新的分支指令。在典型的整数代码中,每5–7条指令就有一条分支指令。在一个6-wide处理器中,每周期可能遇到1–2条分支。因此意味着可以容忍约4–8个周期的分支堆积——对于大多数程序来说足够。提供了更大的余量,代价仅为4 KB的额外存储,在现代工艺下可以忽略不计。
现代处理器通常维护8–16个检查点。MIPS R10000使用4个检查点(受限于1996年的面积预算),香山昆明湖使用8个检查点,Intel的具体检查点数量未公开但估计在12–16个之间。
逐步回退恢复
除了检查点恢复外,还有一种逐步回退(walk-back)的恢复方式:从ROB尾部开始,逆序遍历被丢弃的ROB表项,对于每个表项,将其重新写入推测RAT的对应逻辑寄存器条目,同时将归还到空闲列表。
逐步回退的具体操作如下:设当前ROB尾指针为,分支指令的ROB索引为。回退从开始,逐条向方向处理。对于每个ROB表项(),如果,则执行:
:恢复该逻辑寄存器的旧映射。
将推入空闲列表:回收被丢弃指令分配的物理寄存器。
:作废该ROB表项。
这种方式不需要检查点存储,但恢复时间与被丢弃的指令数量成正比——如果分支预测错误后有100条指令被丢弃,以每周期回退6条计算,恢复需要约17个周期,这是不可忽略的性能代价。
逐步回退的正确性依赖于逆序遍历:如果两条被丢弃的指令和都写了同一个逻辑寄存器(在之前),逆序处理确保先恢复的,再恢复的。最终RAT[]中保存的是的旧映射——即分支指令之前的正确映射。如果正序处理,最终RAT[]中将错误地保存的旧映射(即分配的物理寄存器),这不是分支点的正确状态。
混合恢复策略
现代处理器通常采用混合恢复策略来平衡存储开销和恢复速度:
分支预测错误:使用检查点恢复(1周期恢复),因为分支预测错误频率高(每1000条指令约5–20次),恢复速度至关重要。
异常和中断:使用提交RAT恢复(1周期恢复到提交点),因为异常极其罕见(每百万条指令不到1次),且异常处理本身有较长的延迟(进入操作系统内核),多几个周期的恢复延迟可以忽略不计。
内存序违例(Load/Store重排序导致的错误):可以使用逐步回退(恢复到违例Load指令的位置),或者使用检查点(如果Load指令处有检查点)。
Intel的实现中,检查点不仅在分支处保存,还在某些"高风险"指令处保存(如可能导致异常的除法指令、系统调用指令等),以减少这些罕见事件的恢复延迟。ARM Cortex-X3及以后的设计同样采用了选择性检查点策略——不在每条分支处都保存检查点,而是只在"难以预测"的分支处保存(由分支预测器的置信度决定),从而减少检查点的管理开销。
PRF方案中的特殊情况处理
统一PRF方案在实际实现中还需要处理若干特殊情况,这些情况虽然不改变基本原理,但对硬件设计的正确性至关重要。
x0寄存器的处理
RISC-V ISA规定x0永远为零——任何写入x0的操作都被丢弃,任何读取x0的操作都返回零。在PRF方案中,x0的处理可以采用以下策略:
方式一:硬连线零寄存器。在PRF中保留一个特殊的物理寄存器(如P0),其内容硬连线为全零,写入被忽略。x0在推测RAT和提交RAT中永远映射到P0,重命名逻辑在检测到目标寄存器为x0时跳过物理寄存器分配——不从空闲列表取出新寄存器,不更新RAT。这种方式最为简洁,是大多数RISC-V处理器(包括香山)的实现方式。
方式二:消除写x0的指令。更激进的方式是在解码阶段将目标为x0的指令标记为NOP(如ADD x0, x1, x2变为NOP),不进入乱序引擎。这节省了ROB表项和发射队列条目,但需要确保该指令可能产生的副作用(如异常)仍然被正确处理。
在AArch64中,XZR(零寄存器)的语义与RISC-V的x0类似,但有一个微妙的区别:AArch64的XZR和SP(栈指针)共享同一个编码(寄存器号31),通过指令上下文区分。这要求解码器根据指令类型来决定第31号逻辑寄存器应映射到零寄存器还是SP的物理寄存器。
多结果指令的处理
某些指令产生多个结果:
x86的
DIV指令同时产生商(EAX)和余数(EDX)。AArch64的
LDP指令(Load Pair)同时加载两个寄存器。RISC-V的压缩指令扩展中,某些指令可能涉及寄存器对操作。
多结果指令在重命名阶段需要从空闲列表分配多个物理寄存器,并更新RAT中的多个条目。对于-wide处理器,如果某条多结果指令需要个目标寄存器,则该周期最多只能处理条类似指令(空闲列表的并行出队能力限制)。在实际处理器中,多结果指令通常在解码阶段被拆分为多条微操作(op),每条op只有一个目标寄存器,从而简化重命名逻辑。
条件执行指令的处理
在AArch64中,条件选择指令(如CSEL Xd, Xn, Xm, cond)根据条件标志选择Xn或Xm写入Xd。这条指令有三个源操作数(Xn、Xm、条件标志寄存器NZCV),需要3个RAT读端口。在6-wide处理器中,如果每条指令最多3个源操作数,则RAT读端口需要从增加到个——这是一个不小的增幅。
RISC-V ISA的设计有意避免了条件执行指令和条件标志寄存器,使得每条指令最多2个源操作数,简化了重命名逻辑。x86的条件移动指令(CMOV)也有类似的3源问题——在Intel处理器中,CMOV被解码为多条op以适应2源的重命名架构。
浮点与向量寄存器的重命名
现代处理器通常为整数和浮点/向量寄存器维护独立的PRF和RAT。这是因为:
整数和浮点指令的数据宽度不同(64位 vs. 128/256/512位),共享PRF会导致严重的面积浪费——512位的AVX-512值只占少数指令,为所有PRF条目预留512位不划算。
整数和浮点指令在不同的执行单元中执行,独立的PRF可以更优地放置在靠近相应执行单元的位置。
独立的RAT减少了每个RAT的端口压力。
以Intel Golden Cove为例,其整数PRF为28064位,向量/浮点PRF为332512位。向量PRF的总容量为位KB——远大于整数PRF的位KB。向量PRF的面积因此成为芯片布局中的显著结构,通常放置在向量执行单元旁边。
对于支持AVX-512的处理器,一个值得关注的设计选择是:低128位(XMM)、低256位(YMM)和全512位(ZMM)是否共享同一个物理寄存器?Intel的实现中,它们共享——一个512位的物理寄存器可以存储XMM、YMM或ZMM值。当执行128位指令时,物理寄存器的高384位被自动清零(SSE到AVX的转换规则)。这种共享方式简化了映射表管理(只需一个RAT),但增加了PRF的面积(所有条目都是512位宽,即使大多数指令只使用128位)。
硬件描述 9 — 混合宽度PRF的优化
为了减少512位全宽PRF的面积浪费,一种优化方式是混合宽度PRF:将PRF分为"窄"区域(128位$\times
N$项),窄区域用于频繁的SSE/128位操作,宽区域用于偶尔的AVX-512操作。映射表中的每个条目增加一个1位的"宽度"字段,指示该映射指向窄区域还是宽区域。
混合宽度PRF的面积节省取决于512位指令的占比。在典型的服务器工作负载中,512位指令占比不到5%。假设将332个全宽条目替换为250个128位条目82个512位条目,PRF面积从Kb减少到Kb——面积缩减57%。这种优化在AMD Zen系列处理器中有所采用(Zen系列将256位操作拆分为两个128位op,因此不需要256位以上的PRF)。
三种方式的比较
表表 24.24从多个维度全面比较了三种寄存器重命名方案。
| 特征 | ROB重命名 | 扩展ARF | 统一PRF |
|---|---|---|---|
| 结果存储位置 | ROB表项的Value字段 | 扩展后的寄存器文件 | 独立的物理寄存器文件 |
| 操作数读取源 | ARF + ROB(双源) | 扩展寄存器文件(单源) | PRF(单源) |
| ARF是否独立存在 | 是,存储已提交值 | 与扩展寄存器合并 | 概念上存在(提交RAT指向的PRF条目) |
| ROB表项大小 | 大(含64位Value) | 小(无Value) | 小(无Value) |
| 物理寄存器分配 | 自动(绑定ROB表项) | 需要空闲列表 | 需要空闲列表 |
| 物理寄存器释放 | 自动(释放ROB表项) | 提交时释放旧映射 | 提交时释放旧映射 |
| 操作数读端口 | ARF端口 + ROB端口 | 寄存器文件端口 | PRF端口 |
| 错误恢复 | 简单(回退尾指针,ARF不变) | 检查点或逐步回退 | 提交RAT复制或检查点 |
| 提交操作 | Value从ROB写入ARF | 更新提交映射,释放旧PR | 更新提交RAT,释放旧PR |
| 扩展性(6+ wide) | 差(ROB端口爆炸) | 中等 | 好(PRF端口可优化) |
| 代表性处理器 | Intel P6, AMD K6 | MIPS R10000(变体) | 现代所有高性能处理器 |
三种寄存器重命名方案的全面比较
现代处理器的选择。从2000年代中期开始,几乎所有高性能处理器都采用了统一PRF方案:
Intel:从Pentium 4(NetBurst,2000年)开始使用PRF方案,此后的Core、Sandy Bridge、Haswell、Skylake、Golden Cove、Raptor Cove、Lion Cove均延续此方案。
AMD:从Bulldozer(2011年)开始使用PRF方案,Zen系列全部采用。K7/K8微架构(1999–2003年)使用了一种混合方案——整数部分使用ROB变体,浮点部分使用独立的寄存器文件。
ARM:Cortex-A76及以后的高性能核心均使用PRF方案。Cortex-A73及以前的中等性能核心使用ROB方案。
开源处理器:BOOM(Berkeley Out-of-Order Machine)和香山处理器均使用PRF方案。
PRF方案成为主流的根本原因是扩展性:随着发射宽度从3-wide增长到6-wide甚至8-wide,ROB方案的双源操作数读取所需的端口数量呈二次方增长,面积和时序迅速恶化。而PRF方案将数据存储从ROB中完全解耦,ROB只需记录控制信息(约80–100位/表项),使得ROB和PRF各自可以独立优化。
三种方案的面积与功耗对比
为了量化三种方案的硬件代价,下面以一个具体的处理器配置进行估算:6-wide发射、256项ROB、RISC-V 32个整数逻辑寄存器、192个物理寄存器(PRF方案)、5nm工艺。
| 组件 | ROB重命名 | 扩展ARF | 统一PRF |
|---|---|---|---|
| ROB(控制+Value) | 256180b | 25680b | 25680b |
| ROB面积 | 0.025 mm | 0.012 mm | 0.012 mm |
| ARF/寄存器文件 | 3264b | 9664b | – |
| ARF/RF面积 | 0.002 mm | 0.008 mm | – |
| PRF | – | – | 19264b |
| PRF面积 | – | – | 0.015 mm |
| 映射表(RAT) | 简化映射表 | 327b RAT | 328b2 RAT |
| RAT面积 | 0.001 mm | 0.001 mm | 0.002 mm |
| 空闲列表 | 不需要 | 647b FIFO | 1608b FIFO |
| FL面积 | 0 | 0.001 mm | 0.001 mm |
| 总面积 | 0.028 mm | 0.022 mm | 0.030 mm |
| 总数据读写功耗 | 高(ROB多端口) | 中 | 中低(PRF可分体优化) |
| 可扩展性 | 差 | 中 | 好 |
三种方案的面积与功耗估算(6-wide,5nm)
从表表 24.25可以看出:
ROB重命名方案的总面积并非最大(因为不需要独立的PRF),但其ROB部分由于包含Value字段而面积最大,且其多端口需求导致面积随发射宽度增长最快。
扩展ARF方案的总面积最小,但其寄存器文件的端口需求与PRF相同,面积优势来自于不需要双RAT。
统一PRF方案的总面积略大于扩展ARF(因为需要双RAT和空闲列表),但其关键优势在于可扩展性——PRF可以通过分体和端口优化技术缩减面积,且ROB不含Value字段,面积最小。
在功耗方面,ROB重命名方案的动态功耗最高——每次操作数读取需要同时访问ARF和ROB,然后通过选择器选择,即使ARF路径的结果被丢弃,其读取能量仍然被浪费。PRF方案只需访问一个位置(PRF或旁路),能量效率更高。
从ROB方案到PRF方案的历史演进
寄存器重命名方案的演进可以用一条清晰的时间线来描绘:
1967年——隐式重命名:Tomasulo算法使用保留站实现隐式寄存器重命名。值存储分散在保留站中,没有集中的映射表。
1985年——ROB概念提出:Smith和Pleszkun提出ROB用于精确异常,ROB中的Value字段可以同时提供重命名功能。
1995年——ROB重命名商用化:Intel P6微架构(Pentium Pro)使用ROB重命名+保留站方案,3-wide设计在当时取得了巨大成功。
1996年——显式重命名出现:MIPS R10000使用显式的映射表和扩展寄存器文件,消除了ROB方案的双源读取问题,4-wide设计。
2000年——PRF方案商用化:Intel Pentium 4(NetBurst)使用统一PRF方案,首次在x86处理器中将数据存储从ROB中完全分离。尽管NetBurst的流水线设计因过长(31级)而被批评,其PRF方案的选择影响了后续所有设计。
2006年至今——PRF成为标准:Intel Core(Merom)、AMD Zen系列、ARM Cortex-A76及以后的所有高性能处理器均采用统一PRF方案。PRF容量从早期的100个逐步增长到当前的280–384个,以支持越来越大的ROB和更高的ILP开发。
PRF容量增长的驱动因素
从表表 24.15中可以看到,2015年到2024年的9年间,整数PRF容量从180个增长到384个,增幅超过2倍。这一增长背后有三个主要驱动力:
(1)ROB容量的增大。ROB从224项(Skylake)增长到512项(Golden Cove),以支持更大的乱序窗口和更高的ILP开发。PRF必须相应增大以避免物理寄存器饥饿成为新的瓶颈。
(2)SMT的支持。Intel处理器支持2-way超线程,每个硬件线程需要独立的架构状态(32个整数逻辑寄存器2线程=64个寄存器),增加了PRF的基础需求。
(3)更深的流水线和更长的指令延迟。随着缓存层次变深(L1L2L3),Load指令的平均延迟增加,依赖Load结果的指令在ROB中停留的时间更长,占用物理寄存器的时间也更长。这要求更大的PRF来维持相同的周转率。
PRF容量的增长受到面积和时序的约束。从192增长到384个条目,PRF面积增长约2.5倍(考虑位线变长带来的超线性增长)。在5nm工艺下,38464位18端口的PRF面积约0.04 mm,占die面积不到0.1%——面积已不是限制因素。真正的限制在于访问延迟:384项的PRF可能需要2个周期的读取延迟(相比192项的1周期),这要求在发射和执行之间插入额外的流水段,增加了旁路网络的复杂度。ARM Cortex-X925选择384个物理寄存器,可能正是因为其流水线设计已经为2周期PRF读取预留了空间。
设计权衡 2 — PRF容量的设计权衡
PRF的容量是一个关键的设计参数,需要在性能、面积和功耗之间权衡。
容量太小:当in-flight指令数量接近PRF容量时,空闲列表频繁耗尽,导致前端暂停(stall),IPC下降。经验法则是PRF容量应至少比ROB中需要目标寄存器的平均指令数多20%–30%,以留出足够的余量。
容量太大:每增加一个物理寄存器条目,PRF的面积增加约个晶体管(对于64位数据宽度)。更大的PRF还意味着更长的位线,增加访问延迟。如果PRF访问延迟从1周期增加到2周期,则需要在发射和执行之间插入额外的流水段,增加了旁路网络的延迟,对依赖链密集的代码造成性能损失。
| PRF容量 | 相对IPC | PRF面积 | 访问延迟 |
|---|---|---|---|
| 128 | 0.92 | 基线 | 1周期 |
| 192 | 0.97 | 1.5 | 1周期 |
| 256 | 0.99 | 2.0 | 1周期 |
| 320 | 1.00 | 2.5 | 1–2周期 |
| 384 | 1.00 | 3.0 | 2周期 |
从表中可以看出,PRF容量从128增加到256时,IPC提升显著;但从256增加到384时,IPC几乎不再提升,而面积增长了50%。大多数现代处理器选择192–320个整数物理寄存器,恰好处于性能收益递减的拐点附近。ARM Cortex-X925选择384个物理寄存器是因为其ROB也扩大到了384+项,需要匹配更大的in-flight窗口。
案例研究 4 — 香山处理器的寄存器重命名实现
香山(XiangShan)是中国科学院计算技术研究所开发的开源RISC-V高性能处理器,其第三代微架构"昆明湖"是目前公开信息最详细的高性能乱序处理器实现之一。香山的寄存器重命名采用了标准的统一PRF方案,关键参数如下:
| 参数 | 值 |
|---|---|
| 重命名宽度 | 6-wide |
| 整数PRF | 192个条目64位 |
| 浮点PRF | 192个条目64位 |
| 整数逻辑寄存器 | 32个(RV64) |
| 浮点逻辑寄存器 | 32个(RV64D) |
| 推测RAT | 32条目8位 = 256位(整数) |
| 提交RAT | 32条目8位 = 256位(整数) |
| 空闲列表 | 160条目的FIFO(19232=160) |
| ROB容量 | 256条目 |
| 分支检查点 | 8个RAT快照 |
香山的重命名阶段占用1个流水线周期。在该周期中,6条指令的源操作数映射通过推测RAT查询获得,目标寄存器的新物理寄存器通过空闲列表分配。同周期内的相关性由旁路逻辑处理(如前述的15个比较器网络)。错误恢复采用分支检查点机制,可以在1个周期内恢复到任意分支点的映射状态。
香山的重命名模块实现中有几个值得关注的设计细节:
(1)Walk-back与检查点的混合恢复。香山在分支预测错误时优先使用检查点恢复(1周期),但在异常恢复时使用逐步回退(walk-back)方式。逐步回退的带宽与重命名宽度相同(6-wide),即每周期回退6条指令的映射。对于256条ROB中最多256条指令的回退,最坏情况下需要个周期——但异常极为罕见,这个延迟可以接受。
(2)Move消除的部分实现。香山在昆明湖版本中实现了部分Move消除——对于MV rd, rs指令(ADDI rd, rs, 0),在重命名阶段检测到立即数为0且操作为ADDI时,直接将rd映射到rs当前的物理寄存器。引用计数使用3位饱和计数器(最大值7),当引用计数饱和时,该物理寄存器不再被释放(保守但安全)。
(3)向量寄存器的重命名。香山支持RISC-V V扩展(向量扩展),向量寄存器(v0–v31)有独立的PRF和RAT。由于向量寄存器的宽度可变(由VSETVL指令设置的vl和vtype决定),向量PRF的每个条目的有效宽度也是可变的。香山采用128位固定宽度的向量PRF条目,更宽的向量操作通过寄存器分组(register grouping)来处理——一个宽向量寄存器映射到多个连续的物理寄存器条目。
香山的开源代码(基于Chisel HDL)为学习和研究寄存器重命名的硬件实现提供了宝贵的参考,其重命名模块位于src/main/scala/xiangshan/backend/rename/目录下。
BOOM处理器的重命名实现
BOOM(Berkeley Out-of-Order Machine)是UC Berkeley开发的开源RISC-V乱序处理器,是学术界最广泛使用的乱序处理器参考实现。BOOM的重命名模块设计简洁,是理解PRF重命名的理想教学案例。
BOOM v3的重命名参数为:4-wide重命名、96个整数物理寄存器、64个浮点物理寄存器、32个逻辑整数寄存器、32个逻辑浮点寄存器、4个分支检查点。
BOOM的RAT实现为一个以Chisel编写的同步读异步写寄存器阵列。同步读意味着RAT的读取结果在下一个时钟沿才可用——这要求重命名阶段至少占用1个周期。异步写意味着写入在当前周期立即生效——这保证了同周期内后面的指令可以看到前面指令的映射更新。
BOOM的同周期旁路逻辑采用了一种简洁的实现方式:在Chisel中使用when链来描述优先级,综合工具自动生成对应的多路选择器和比较器。对于4-wide处理器,旁路逻辑的比较器数量为个(每对指令间需要2个比较——检查两个源操作数),总延迟约3级逻辑门。
BOOM的空闲列表使用了简单的FIFO实现,在分支恢复时通过保存和恢复FIFO头指针来回收物理寄存器。这种设计的一个已知局限是:在某些极端的分支恢复场景中,FIFO中可能出现"幽灵条目"(分支后分配又恢复的物理寄存器编号与正常提交释放的编号重叠),需要额外的去重逻辑。
中低端处理器的重命名简化
并非所有乱序处理器都需要本章描述的完整PRF重命名方案。对于面积和功耗受限的中低端设计(如ARM Cortex-A55、A510等小核),一些简化策略可以显著降低重命名的硬件开销:
(1)减少PRF容量。小核的ROB通常仅有40–80项,对应的PRF只需60–100个物理寄存器。在这个规模下,PRF面积极小(不到0.005 mm),不需要分体优化。
(2)窄发射宽度。2-wide的小核只需4个RAT读端口和2个写端口,RAT的实现可以使用简单的寄存器阵列,不需要复杂的旁路逻辑(2条指令之间的旁路只需2个比较器和一个选择器)。
(3)省略检查点。部分小核设计省略了RAT检查点,仅使用提交RAT进行恢复。在分支预测准确率较高(97%)的低频率小核中,分支恢复的额外惩罚(恢复到提交点而非分支点)对整体性能影响不大(2% IPC损失)。
(4)省略Move消除。小核通常不实现Move消除和零惯用语消除,因为这些优化的IPC收益(2%–3%)在小核的目标场景(能效优先)中不值得额外的引用计数硬件开销。
这些简化使得小核的重命名单元面积仅为大核的到,功耗仅为到,同时仍然保留了乱序执行带来的大部分性能优势。这体现了处理器设计中可伸缩性(scalability)的重要性——同一种微架构技术(PRF重命名)可以通过参数调整适应从超低功耗到最高性能的不同设计点。
| 参数 | 大核(如Cortex-X925) | 小核(如Cortex-A520) |
|---|---|---|
| 发射宽度 | 6–8 wide | 2–3 wide |
| ROB容量 | 384+ | 48–80 |
| 整数PRF | 384 | 48–64 |
| RAT读端口 | 12–16 | 4–6 |
| RAT写端口 | 6–8 | 2–3 |
| 分支检查点 | 8–16 | 0–4 |
| Move消除 | 支持 | 不支持 |
| 零惯用语消除 | 支持 | 部分支持 |
| 重命名面积(估算) | 0.03–0.05 mm | 0.002–0.005 mm |
| 重命名功耗(估算) | 20–40 mW | 2–5 mW |
大核与小核的重命名参数对比
在ARM big.LITTLE和Intel hybrid架构(如Alder Lake的P-core + E-core)中,大核和小核共存于同一芯片上。当工作负载需要高单线程性能时,任务运行在大核上,使用全部的重命名资源;当工作负载轻或需要高能效时,任务迁移到小核上,使用精简的重命名资源。操作系统的调度器根据负载特征动态选择核心类型,实现性能与能效的最优平衡。
未来趋势:更大的重命名窗口
随着半导体工艺继续微缩和处理器对更高ILP的追求,寄存器重命名的参数还将继续增长。从近年的趋势可以推断:
(1)PRF容量将突破400个。ARM Cortex-X925已经达到384个整数物理寄存器。随着ROB容量向512–768项增长(以支持更深的乱序窗口和更长的Load延迟),PRF容量需要相应增加到400–500个。但PRF容量的增长受到访问延迟的限制——超过384项后,PRF读取延迟可能从1周期增加到2周期,需要在流水线中增加一个"寄存器读取"流水段。
(2)重命名宽度将增加到8-wide。苹果的M系列处理器(如M4的高性能核心Doubloon)已经实现了8-wide甚至更宽的解码和重命名。8-wide重命名需要16个RAT读端口和8个写端口,以及个旁路比较器——这对RAT的时序提出了更高要求,可能需要将重命名拆分为2个流水段。
(3)SMT线程数的增加。为了提高芯片的总吞吐率,未来处理器可能支持4-way甚至8-way SMT。每个SMT线程需要独立的架构寄存器映射,PRF的项需求将相应增加。4-way SMT的(324),占PRF总容量的比例增大,推动PRF容量进一步增长。
案例研究 5 — Intel Golden Cove的寄存器重命名
Intel Golden Cove微架构(2021年,用于第12代Alder Lake处理器的P-core)采用了大规模的统一PRF设计:
| 参数 | 值 |
|---|---|
| 重命名宽度 | 6-wide op |
| 整数PRF | 280个条目64位 |
| 向量/浮点PRF | 332个条目512位(支持AVX-512) |
| 整数逻辑寄存器 | 16个GPR + 内部临时寄存器 |
| ROB容量 | 512条目 |
| op Cache | 4K op |
| Move消除 | 支持(寄存器-寄存器MOV不分配PRF) |
| 零惯用语消除 | 支持(XOR reg, reg等不分配PRF) |
| 超线程 | 2-way SMT,PRF在两个线程间动态共享 |
Golden Cove的PRF设计有几个值得关注的细节:
(1)PRF在SMT线程间的共享。280个整数物理寄存器在两个超线程之间动态共享——不是静态分区(各140个),而是根据每个线程的实际需求动态分配。这允许单线程运行时使用全部280个物理寄存器,在双线程运行时根据需求比例分配(如一个线程使用200个,另一个使用80个)。动态共享通过空闲列表实现——两个线程共享同一个空闲列表,谁先重命名就先从空闲列表分配。
动态共享的一个潜在风险是一个线程可能"饿死"另一个线程——如果一个线程的in-flight指令非常多(占用了大部分物理寄存器),另一个线程可能频繁因PRF饥饿而暂停。为了缓解这一问题,Golden Cove为每个线程保留了一个最低保证量——至少个物理寄存器(为最小重命名空间,通常约20–30个),确保每个线程都能维持基本的指令推进。
(2)x86的内部临时寄存器。x86的CISC指令在解码为op时,某些复杂指令需要内部临时寄存器来保存中间结果(如PUSH指令先将SP减4到临时寄存器,再将数据写入临时寄存器指向的内存地址)。这些临时寄存器对程序员不可见,但在重命名时需要像普通逻辑寄存器一样分配物理寄存器。Golden Cove的16个GPR加上约8–10个内部临时寄存器,使得有效的逻辑寄存器数约为24–26个,RAT需要相应的条目数。
Golden Cove的一个显著特征是Move消除(Move Elimination)和零惯用语消除(Zero-Idiom Elimination),这两项技术都在重命名阶段实现,不消耗执行单元和PRF资源。
Move消除的实现机制
Move消除(Move Elimination)是现代处理器在重命名阶段完成的一项重要优化。其基本原理是:对于寄存器到寄存器的MOV指令(如x86的MOV EAX, EBX,RISC-V中等价于ADDI rd, rs, 0),处理器在重命名阶段直接将目标逻辑寄存器映射到源操作数已有的物理寄存器,而不分配新的物理寄存器。
具体实现步骤如下:
模式检测:解码器或重命名逻辑识别出MOV模式。在x86中,这包括
MOV reg, reg、MOVZX reg, reg等。在RISC-V中,MV rd, rs被编码为ADDI rd, rs, 0,检测逻辑需要识别"ADDI指令且立即数为0"的模式。这需要一个比较器检查立即数字段是否为零,以及操作码字段是否匹配。跳过分配:不从空闲列表分配新的物理寄存器。目标逻辑寄存器直接映射到源逻辑寄存器当前映射的物理寄存器。例如,如果
rs当前映射到P42,则执行MV rd, rs后,rd也映射到P42。跳过发射和执行:指令直接在重命名阶段完成,标记为Done=1,不进入发射队列,不占用执行单元。这使得MOV指令的延迟为零周期。
然而,Move消除引入了一个微妙的问题:多个逻辑寄存器可能同时映射到同一个物理寄存器。在上述例子中,rs和rd都映射到P42。这意味着当某条后续指令写入rs时(为rs分配新的物理寄存器P50),P42不能被释放——因为rd仍然映射到P42。反之亦然。
为了正确管理这种"共享映射",处理器需要为每个物理寄存器维护一个引用计数(reference count)。引用计数记录当前有多少个逻辑寄存器映射到该物理寄存器:
初始状态:每个已分配的物理寄存器的引用计数为1。
Move消除时:源物理寄存器的引用计数加1(因为多了一个逻辑寄存器映射到它)。
映射被覆盖时(即需要被释放时):的引用计数减1。只有当引用计数降为0时,物理寄存器才真正被释放回空闲列表。
引用计数的位宽取决于最多有多少个逻辑寄存器能同时映射到同一个物理寄存器。理论上,32个逻辑寄存器都可以通过一连串MOV指令映射到同一个物理寄存器,因此引用计数需要位。但在实际程序中,同一物理寄存器的引用计数很少超过3–4,某些实现使用饱和计数器(如3位,饱和值为7)来减少存储开销——当引用计数饱和时,该物理寄存器永不释放(被标记为"永久占用"),这是一个罕见但安全的降级。
硬件描述 10 — 引用计数的存储开销
对于280个物理寄存器、每个5位引用计数的PRF,引用计数存储的总开销为位字节。这个开销极小——不到PRF数据存储(位 KB)的8%。
引用计数的更新逻辑需要与重命名阶段同步:每周期最多条指令可能触发引用计数的增加(Move消除)或减少(映射覆盖),需要个增量端口和个减量端口。对于6-wide处理器,这意味着12个并发更新端口——但由于引用计数只有5位宽,这些端口的物理面积很小。
零惯用语消除的实现
零惯用语消除(Zero-Idiom Elimination)是Move消除的一个特例。编译器和程序员经常使用特定的指令模式来将寄存器清零:
x86:
XOR EAX, EAX、SUB EAX, EAX、PXOR XMM0, XMM0RISC-V:
ADDI rd, x0, 0(即LI rd, 0),或者XOR rd, rs, rsAArch64:
MOV Xd, XZR
处理器在解码或重命名阶段检测到这些模式后,将目标逻辑寄存器映射到一个硬连线零寄存器——一个物理寄存器位置被永久设置为全零值,其内容不可写入。这个零寄存器不占用空闲列表资源(它从不被分配也从不被释放),所有映射到它的逻辑寄存器共享同一个物理位置。
在RISC-V中,ISA本身定义了x0永远为零,因此硬连线零寄存器可以与x0的物理实现合并。任何写x0的指令都可以被消除(因为结果被丢弃),x0的映射永远指向硬连线零寄存器。
零惯用语消除的性能收益来自两个方面:(1)减少PRF分配压力(不消耗物理寄存器);(2)降低执行单元占用(清零操作不需要实际执行)。在SPEC CPU 2017整数基准中,零惯用语大约占指令总数的1%–3%,性能收益虽小但实现成本几乎为零——只需在解码器中增加模式检测逻辑。
其他可消除的指令模式
除了Move消除和零惯用语消除外,现代处理器还可以在重命名阶段识别并消除其他指令模式:
(1)自恒等操作。如OR rd, rs, rs(结果等于rs本身)、AND rd, rs, rs(同理)。这些指令等价于MV rd, rs,可以通过Move消除机制处理。检测条件是操作码为OR/AND且两个源操作数寄存器编号相同。
(2)符号/零扩展消除。在x86-64中,32位操作自动零扩展到64位(如MOV EAX, EBX会将RAX的高32位清零)。如果编译器生成了显式的零扩展指令(如MOVZX RAX, EAX),处理器可以在重命名阶段检测到这是一个冗余操作并消除它——前提是RAT中记录了相应的位宽信息。
(3)NOP消除。NOP指令(或编码等价的指令如ADDI x0, x0, 0)显然不需要执行。处理器在解码阶段就将其标记为NOP,不分配ROB表项、不分配物理寄存器、不进入发射队列。NOP消除对于经过编译器对齐填充的代码段特别重要——这些填充NOP不应占用任何后端资源。
(4)常数折叠。某些处理器可以在重命名阶段完成简单的常数运算。例如,如果ADDI rd, x0, imm的x0映射到硬连线零寄存器,处理器知道结果就是立即数imm,可以将该值直接写入目标物理寄存器而不发射到ALU执行。这种优化在ARM Cortex-X3及以后的处理器中有报道,但具体实现细节未公开。
性能分析 4 — Move消除对IPC的影响
Move消除对IPC的提升取决于程序中MOV指令的比例。在x86代码中,MOV指令占比约为30%–35%的总指令数(含Load/Store形式的MOV)。其中寄存器到寄存器的MOV约占5%–8%。
| 基准 | reg-reg MOV占比 | Move消除的IPC提升 |
|---|---|---|
| gcc | 8.2% | 3.1% |
| mcf | 4.5% | 1.8% |
| xalancbmk | 7.6% | 2.9% |
| deepsjeng | 6.1% | 2.4% |
| exchange2 | 3.8% | 1.5% |
Move消除的IPC提升虽然不大(平均约2%–3%),但其实现代价极低——仅需在重命名逻辑中增加一个比较器来检测MOV模式,以及引用计数逻辑来管理多个逻辑寄存器映射到同一物理寄存器的情况。在RISC-V中,MV rd, rs实际上是ADDI rd, rs, 0,同样可以在重命名阶段被识别并消除。
Move消除的收益还有一个间接效应:消除的MOV指令不消耗执行单元带宽,等价于增加了"有效发射宽度"。在6-wide处理器中,如果每周期平均有0.4条MOV被消除,则有效发射带宽增加到6.4——这对IPC的提升约,这个间接效应比直接的延迟消除效应更大。但这个增益在实际中被其他瓶颈(如后端执行单元不足、缓存miss等)部分抵消。
Move消除的引用计数详细工作流程
为了更具体地理解引用计数机制,下面用一个完整的示例来追踪其工作过程。
add x1, x2, x3 # I1: x1 -> P40 (P40引用计数=1)
mv x4, x1 # I2: x4 -> P40 (消除!) (P40引用计数=2)
mv x5, x1 # I3: x5 -> P40 (消除!) (P40引用计数=3)
add x1, x6, x7 # I4: x1 -> P41 (P40引用计数=3->2, 旧映射)
add x4, x8, x9 # I5: x4 -> P42 (P40引用计数=2->1)
add x5, x10, x11 # I6: x5 -> P43 (P40引用计数=1->0, 释放!)逐步追踪P40的引用计数变化:
| 指令 | 操作 | P40引用计数 | 说明 |
|---|---|---|---|
| I1 | 分配P40给x1 | P40首次分配 | |
| I2 | x4映射到P40(Move消除) | x4和x1共享P40 | |
| I3 | x5映射到P40(Move消除) | x1、x4、x5都指向P40 | |
| I4 | x1映射到P41,I4的=P40 | 暂不变 | 等I4提交时减1 |
| I5 | x4映射到P42,I5的=P40 | 暂不变 | 等I5提交时减1 |
| I6 | x5映射到P43,I6的=P40 | 暂不变 | 等I6提交时减1 |
| I4提交 | 释放=P40 | x4和x5仍指向P40 | |
| I5提交 | 释放=P40 | x5仍指向P40 | |
| I6提交 | 释放=P40 | 无引用,P40归还空闲列表 |
Move消除中P40引用计数的变化追踪
从表表 24.31可以看出:
引用计数在Move消除时增加(因为多了一个逻辑寄存器映射到P40),在覆盖指令提交时减少(因为少了一个逻辑寄存器映射到P40)。
P40只有在引用计数降为0时才被释放——此时确实没有任何逻辑寄存器或in-flight指令需要它了。
如果没有Move消除,I2和I3各自会分配新的物理寄存器(如P44和P45),I1的P40在I4提交时就会被释放(引用计数从1变为0),整个过程消耗了3个物理寄存器(P40、P44、P45)。使用Move消除后,只消耗了1个物理寄存器(P40),节省了2个——这是Move消除的另一个重要收益。
设计提示
引用计数机制增加的硬件开销很小(每个物理寄存器5位引用计数、一个加1/减1电路),但它使得Move消除在功能上完全正确。一个值得注意的设计简化是:如果处理器不实现Move消除,则不需要引用计数——每个物理寄存器在任何时刻最多被一个逻辑寄存器映射,在覆盖指令提交时可以无条件释放。因此,引用计数是Move消除的"代价",但这个代价极小(位字节),远低于Move消除带来的性能和资源节省。
本章要点回顾。
本章从数据相关性的基础概念出发,系统地介绍了寄存器重命名的三种硬件实现方案,现将核心要点总结如下:
数据相关性分为真相关(RAW)和假相关(WAR/WAW)。真相关由程序的数据流语义决定,不可消除;假相关由逻辑寄存器名字的重复使用导致,可以通过寄存器重命名完全消除。在典型的SPEC CPU 2017整数基准中,128条指令窗口内的假相关数量超过600个(WAW + WAR),寄存器重命名可以贡献约50%–60%的IPC提升。
真相关的拓扑可以用数据流图来分析。扇出(一条指令的结果被多条后续指令使用)和扇入(一条指令依赖多条前驱指令)都不增加关键路径长度。真正决定关键路径的是链式依赖——串行的RAW依赖链,其长度直接限制了IPC上限。
ROB重命名将指令结果存储在ROB表项的Value字段中,概念简单、异常恢复容易,但操作数需要从ARF和ROB两处读取,在宽发射处理器中端口需求急剧增长,扩展性差。每个ROB表项约120–180位(含64位Value),对于6-wide处理器需要24个以上的ROB端口。此方案适用于窄发射宽度(2–3 wide)的设计,Intel P6微架构(1995年)是经典代表。
扩展ARF方案增加物理寄存器数量并通过映射表管理,解决了双源读取问题。操作数读取只需访问一个统一的寄存器文件。MIPS R10000(1996年)是采用此方案的代表性处理器,拥有64个物理整数寄存器、4个RAT检查点。
统一PRF方案将逻辑寄存器完全抽象化,所有值存储在独立的物理寄存器文件中。通过推测RAT和提交RAT的双表结构实现快速错误恢复。PRF容量由公式确定。此方案是现代所有高性能处理器(Intel、AMD、ARM、RISC-V)的标准选择。
PRF方案的关键控制结构包括:
推测RAT(重命名阶段查询和更新),32条目9位,需要个读端口和个写端口,加上的旁路逻辑。
提交RAT(提交阶段更新,错误恢复时复制到推测RAT),同样32条目9位。
空闲列表(物理寄存器的分配与回收),可用FIFO(低延迟分配)或位图(简单恢复)实现。
检查点(分支指令处保存RAT快照以加速恢复),通常8–16个检查点,每个256位。
物理寄存器的释放时机必须严格遵循规则:当写入逻辑寄存器的指令提交时,释放在之前的映射。这是正确性约束下的最早释放时机。过早释放(如执行完成时释放)在分支预测错误恢复时会导致数据永久丢失。
现代处理器利用Move消除和零惯用语消除等技术,在重命名阶段直接完成特定指令的操作。Move消除需要引用计数机制来管理多个逻辑寄存器映射到同一物理寄存器的情况,每个物理寄存器需要5位引用计数。
PRF的面积优化主要通过分体(banked)设计实现:将PRF分为4个或更多bank,每个bank的位线长度缩短为原来的,延迟降为。读端口可以从理论最大的缩减到约(利用旁路转发和立即数优化),进一步减小面积。
重命名阶段的关键路径包括RAT读取、同周期相关性检测、旁路选择和RAT写入,总延迟约6–8个FO4门。在5nm工艺下约30–56ps,通常可以在一个时钟周期内完成。
本章所涵盖的内容可以用一个简洁的概念框架来总结:寄存器重命名的本质是在运行时执行动态SSA转换,将逻辑寄存器空间中的假相关消除,恢复程序固有的数据流并行性。三种实现方案(ROB重命名、扩展ARF、统一PRF)在结果值存储位置、操作数读取方式和错误恢复机制三个维度上各有不同的设计选择,但都遵循相同的逻辑语义:映射查询物理寄存器分配映射更新提交时释放旧映射。统一PRF方案因其优越的可扩展性成为现代处理器的标准选择,其关键设计参数——PRF容量、RAT端口数、检查点数量、空闲列表实现方式——决定了重命名阶段的性能、面积和功耗。
设计权衡 3 — 前向桥接——从原理到电路
本章建立了寄存器重命名的基本原理和三种实现方案的设计空间。然而,一个核心的工程问题尚未深入展开:在6-wide超标量处理器中,RAT每周期需要18次访问(6读src1 + 6读src2 + 6写dst),这使其成为端口密度最高的结构之一。如何在物理上实现这样的多端口存储?SRAM和CAM两种RAT方案在面积和时序上有何具体差异?物理寄存器文件的端口数又如何随发射宽度增长?这些电路级的设计挑战将在第 25.0 章中逐一展开。