超标量处理器的寄存器重命名
硬件描述 1 — 并行中的串行——最精巧的电路设计之一
6条同周期指令之间可能存在RAW依赖——I3的源操作数可能就是I1的目的寄存器。重命名不是6条独立的并行查表,而是一条有数据依赖的串行链嵌套在并行操作之中。在一个时钟周期(200 ps@5 GHz)内,必须同时完成:(1)6次RAT读取;(2)30个比较器的依赖检测;(3)优先级选择与旁路转发;(4)6次RAT写入。将这"既并行又串行"的操作压缩到单周期内,是超标量处理器中最精巧的组合逻辑设计之一。
设计提示
统一视角。处理器设计的本质是在有限的晶体管预算和功耗约束下,通过投机和并行的层层叠加来逼近指令吞吐率的理论上限。同周期RAW检测正是"并行内部的串行"——6-wide dispatch是并行的,但依赖链使其内部包含串行约束。30个比较器的增长揭示了一个基本矛盾:dispatch宽度每翻倍,依赖检测硬件增长4倍。这一矛盾在第 24.0 章讨论的重命名原理和第 25.0 章讨论的RAT/PRF端口需求中已有铺垫,本章将展示它如何在超标量重命名中达到顶峰。
在第 24.0 章中我们介绍了寄存器重命名的基本原理——通过将逻辑寄存器(ISA寄存器)映射到更大的物理寄存器文件,消除WAR和WAW假相关性,使指令能够乱序执行。第 25.0 章则讨论了重命名映射表(RAT)和空闲物理寄存器管理的具体硬件结构。然而,前面两章的讨论隐含了一个简化假设:每个周期只重命名一条指令。在真实的超标量处理器中,这个假设不成立。
一个6发射的超标量处理器每周期需要同时重命名最多6条指令。这些指令之间可能存在RAW真相关性(后面的指令读取前面指令的目的寄存器)和WAW假相关性(多条指令写同一个逻辑寄存器),需要在重命名阶段就正确处理。此外,当分支预测失败或发生异常时,重命名映射必须能够快速恢复到正确的状态。这些问题构成了超标量寄存器重命名的核心工程挑战。
本章将依次讨论:(1)如何在重命名阶段解决同一周期内多条指令之间的RAW相关性;(2)如何处理WAW相关性以及旧物理寄存器的释放时机;(3)预测失败或异常后重命名状态的三种恢复方式——Checkpoint、WALK和Architecture State;(4)重命名后指令进入分发(Dispatch)阶段的操作;(5)向量寄存器和特殊寄存器(标志寄存器、谓词寄存器)重命名所面临的额外挑战。
解决RAW相关性
RAW(Read After Write)是真相关性,不能通过重命名消除——后面的指令确实需要读取前面指令的计算结果。在超标量处理器中,当多条指令在同一个周期进入重命名阶段时,后面的指令可能依赖前面指令刚刚建立的新映射。重命名逻辑必须在同一个周期内检测到这种依赖,并将最新的物理寄存器编号正确地传递给后续指令。
同一周期内多条指令的RAW检测
考虑一个4发射处理器在某个周期同时重命名以下4条RISC-V指令:
add x3, x1, x2 # I0: 写 x3
sub x5, x3, x4 # I1: 读 x3 (依赖I0)
or x7, x5, x6 # I2: 读 x5 (依赖I1)
and x8, x3, x7 # I3: 读 x3 (依赖I0), 读 x7 (依赖I2)在这组指令中,存在一条依赖链:。如果每条指令都独立查询RAT获取源操作数的物理寄存器编号,那么I1查询x3时得到的是重命名之前的旧映射,而不是I0刚刚分配的新物理寄存器——这将导致I1读取错误的数据。
解决这个问题的关键在于:重命名阶段必须在同一个周期内检测到后面指令的源寄存器与前面指令的目的寄存器之间的匹配关系,并将前面指令新分配的物理寄存器编号转发给后面的指令。
具体而言,如图图 26.1所示,对于一组在同一周期重命名的条指令,需要进行以下检测:
对于第条指令()的每个源寄存器,逐一检查前面所有指令的目的寄存器是否与匹配。
如果存在匹配,则不使用RAT中查询到的旧映射,而是使用匹配的那条指令新分配的物理寄存器编号。
如果有多条前面的指令都写了同一个逻辑寄存器(WAW情况),则应该选择程序序中最后一条——即编号最大的那条。
设处理器发射宽度为,每条指令最多有2个源寄存器和1个目的寄存器,则RAW检测所需的比较器数量为:
对于一个6发射处理器,个比较器。每个比较器比较两个逻辑寄存器编号(RISC-V为5位),硬件开销并不大。但比较器的结果需要驱动多路选择器来选择正确的物理寄存器编号,而选择器的扇入随着发射宽度的增加而增长,这才是时序上的瓶颈。
表表 26.1列出了不同发射宽度下RAW检测所需的比较器和选择器规模。
| 发射宽度 | 比较器数量 | 最大选择器扇入 | 相对延迟 |
|---|---|---|---|
| 2 | 2 | 2选1 | 1.0 |
| 4 | 12 | 4选1 | 1.4 |
| 6 | 30 | 6选1 | 1.7 |
| 8 | 56 | 8选1 | 2.0 |
| 10 | 90 | 10选1 | 2.2 |
不同发射宽度下RAW检测的硬件规模
可以看到,比较器的数量以增长,而最大选择器扇入以线性增长。在8发射以上的设计中,RAW检测逻辑的延迟和面积开始变得非常显著,这也是超宽发射处理器(10-wide及以上)在实践中很少见的原因之一。
图图 26.2以矩阵形式展示了6-wide重命名中30个RAW比较器的排列。矩阵的每一行对应一条指令的源操作数,每一列对应一条更早指令的目的操作数。半三角结构反映了因果性约束:指令只能依赖程序序中更早的指令。
6发射处理器的比较器矩阵详解
为了更精确地理解6发射处理器中RAW检测的硬件结构,我们详细绘制其比较器矩阵。设6条指令为I0I5,每条指令有源寄存器rs1、rs2和目的寄存器rd。对于第条指令的每个源寄存器,需要与I0I的全部目的寄存器进行比较:
图图 26.3展示了完整的比较器矩阵。矩阵呈下三角形状:I1需要与I0比较(2个比较器),I2需要与I0、I1比较(4个比较器),以此类推,I5需要与I0I4比较(10个比较器)。总计个比较器。
每个比较器比较两个RISC-V逻辑寄存器编号(5位宽),需要5个XNOR门和一个5输入AND门。单个比较器的门数约为:
30个比较器总共约420门等价(gate equivalents, GE)。在7nm工艺下,1 GE 0.5 m,因此比较器阵列的面积约为210 m——面积本身不是瓶颈。真正的瓶颈在于比较器结果驱动的优先级选择器链。
比较器的门级实现
每个5位比较器的门级结构如下:两个输入和逐位进行XNOR运算,产生5个"位匹配"信号,然后通过一个5输入AND门生成最终的"完全匹配"信号:
在标准单元库中,XNOR2的延迟约为1个FO4(fan-out-of-4反相器延迟),5输入AND门的延迟约为2个FO4(通常分解为AND2+AND3的级联或使用NAND+NOR)。因此单个比较器的延迟约为个FO4。
对于6发射的比较器矩阵,所有30个比较器可以完全并行工作——它们之间没有数据依赖。因此比较操作的总延迟仅为一个比较器的延迟(3个FO4),不随发射宽度增加。
硬件描述 2 — 考虑rd_valid门控的比较器
在实际设计中,并非每条指令都有目的寄存器。例如store指令、分支指令等没有写回目的寄存器,它们的rd_valid信号为0。比较器需要将rd_valid作为门控条件:
// Single comparator with rd_valid gating
// Compares rs_addr of later instruction with rd_addr of earlier instruction
// Output: match signal (1 if bypass needed)
assign match = rd_valid_earlier &
(rs_addr_later[4:0] == rd_addr_earlier[4:0]);综合为门级电路后,rd_valid信号作为AND门的一个额外输入,增加约0.5个FO4的延迟。对于没有目的寄存器的指令,match信号始终为0,后续的优先级选择器将直接使用RAT的查询结果。
旁路优先级MUX链的门级实现
比较器矩阵产生的match信号需要驱动多路选择器(MUX),为每个源操作数选择正确的物理寄存器编号。对于第条指令的源寄存器,需要一个选1的优先级MUX——在个前面指令的旁路结果和1个RAT查询结果中选择。优先级规则是:编号最大的匹配指令优先(最近的写者优先)。
以I5的rs1为例,它需要在以下6个候选中选择:
| 优先级 | 来源 | 物理寄存器 | 选择条件 |
|---|---|---|---|
| 最高 | I4旁路 | new_prd[4] | match[4][5][0] == 1 |
| I3旁路 | new_prd[3] | match[3][5][0] && !match[4][5][0] | |
| I2旁路 | new_prd[2] | match[2][5][0] && 无更高匹配 | |
| I1旁路 | new_prd[1] | match[1][5][0] && 无更高匹配 | |
| I0旁路 | new_prd[0] | match[0][5][0] && 无更高匹配 | |
| 最低 | RAT查询 | rat_result[5][0] | 无任何匹配 |
I5的rs1旁路优先级选择
这个优先级选择器可以用两种方式实现:
方式一:优先级编码器 + MUX。先用优先级编码器(priority encoder)从match信号中提取最高优先级的匹配位置,再用该位置驱动一个6选1 MUX选择对应的物理寄存器编号。优先级编码器的延迟约为个FO4,6选1 MUX的延迟约为3个FO4,总延迟约6个FO4。
方式二:级联MUX链(cascaded MUX chain)。从RAT查询结果开始,依次用2选1 MUX检查I0、I1、...、I4的match信号。每级MUX由一个match信号控制:如果match为1,则选择该指令的新物理寄存器;否则保持前一级的结果。最后一级的输出就是正确的选择。
图图 26.4展示了级联MUX链的结构。这种实现的优势在于结构规整、布局友好,每级MUX的延迟约1.5个FO4(一个2选1 MUX在7nm工艺下的延迟),5级总延迟约7.5个FO4。
两种方式的延迟对比:
| 实现方式 | 延迟(FO4) | 面积(GE/位) | 布线规整度 |
|---|---|---|---|
| 优先级编码器+MUX | 6 | 8 | 中等 |
| 级联MUX链 | 7.5 | 6 | 好 |
优先级选择器两种实现方式的对比
对于7位物理寄存器编号,每个2选1 MUX需要7个2选1位MUX(每个约4 GE),因此一条MUX链约 GE。I5有2个源操作数,需要两条MUX链,共280 GE。整个6发射处理器的所有MUX链的总面积为:
加上比较器的420 GE,整个RAW检测与旁路网络约需1260 GE 630 m(7nm),面积可控。但关键路径延迟是比较器(3 FO4)+ 最长MUX链(7.5 FO4)= 10.5 FO4,这需要叠加在RAT读取延迟之后,使整个重命名阶段的关键路径达到约2025个FO4。
设计提示
在实际实现中,级联MUX链的关键路径可以通过树形MUX结构来缩短。将5级串联MUX改为分组并行比较+2级MUX树:第一级并行从(I3,I4)和(I0,I1,I2)中分别选出最高优先级匹配,第二级在两组的胜者之间再选一次。这将延迟从降低到,代价是多路选择器的面积增加约30%。对于8发射处理器,树形结构可以将MUX链延迟从7级降低到3级,显著改善时序。
RAW旁路的完整时序分析
将RAW检测的各个步骤整合,重命名阶段的关键路径如下:
RAT读取:逻辑寄存器编号送入SRAM-RAT,读出物理寄存器编号。SRAM读取延迟约812个FO4(取决于RAT的容量和端口数量)。
比较器:并行比较所有指令的源-目的寄存器对。延迟约3个FO4。注意比较器的输入(逻辑寄存器编号)在解码阶段末尾就已经可用,可以与RAT读取并行启动。
MUX选择:根据比较结果,选择正确的物理寄存器编号。延迟约68个FO4。
RAT写回:将新的映射写入RAT。SRAM写入延迟约68个FO4。
由于步骤2(比较)可以与步骤1(RAT读取)并行,关键路径实际上是:
在7nm工艺下,1 FO4 34 ps,因此24个FO4 7296 ps。如果目标频率为5 GHz(周期200 ps),重命名阶段可以在单个流水线级内完成。但对于更高频率(如6 GHz以上)或更宽发射宽度(8发射以上),可能需要将重命名拆分为两级流水线。
性能分析 1 — RAW检测的时序影响
在一个6发射处理器中,第6条指令(I5)需要检查前面5条指令的目的寄存器——这意味着I5的每个源操作数都需要经过一个6选1的多路选择器(5条前面指令的旁路加上RAT的原始查询结果)。对于8发射处理器,这个选择器变成8选1。
多路选择器的延迟大约与扇入成正比。从4选1到8选1,延迟增加约50%。这使得RAW检测逻辑成为重命名阶段关键路径的一部分,可能限制了处理器的最高工作频率。
在实际设计中,如果重命名阶段的时序无法在单个周期内完成,可以将其拆分为两个流水线阶段:第一级完成RAT查询和比较操作,第二级完成选择和写入。这种做法增加了一级流水线延迟,但放宽了时序约束。
旁路信息的传递
在重命名阶段内部的旁路(bypass)与执行阶段的数据旁路在概念上类似,但传递的不是计算结果,而是物理寄存器编号。我们可以将这个过程称为"重命名旁路"(rename bypass)。
重命名旁路的工作过程如下:
并行查询RAT:在周期开始时,所有条指令同时将它们的源寄存器编号送入RAT进行查询。RAT返回每个逻辑寄存器当前对应的物理寄存器编号。这一步忽略了同一周期内其他指令的影响。
并行分配物理寄存器:同时,空闲列表为所有有目的寄存器的指令分配新的物理寄存器编号。例如I0分配P41,I1分配P42,依此类推。
依赖检测:每条指令的源寄存器编号与前面所有指令的目的寄存器编号进行比较。这些比较操作是并行的——所有比较器同时工作。
选择最终映射:根据比较结果,通过优先级选择器确定每个源操作数应该使用的物理寄存器编号。优先级规则是:如果存在匹配,选择程序序中最近的那条匹配指令的新物理寄存器;如果不存在匹配,使用RAT的查询结果。
下面用一个具体的例子来说明。假设当前RAT中的映射为:,。空闲列表将依次分配P41、P42、P43、P44。
| 指令 | 操作 | RAT查询结果 | 旁路修正后 |
|---|---|---|---|
| I0 | add x3, x1, x2 | rs1: x1P5, rs2: x2P8 | 无需修正;rd: x3P41 |
| I1 | sub x5, x3, x4 | rs1: x3P10, rs2: x4P11 | rs1: x3P41(来自I0);rd: x5P42 |
| I2 | or x7, x5, x6 | rs1: x5P12, rs2: x6P14 | rs1: x5P42(来自I1);rd: x7P43 |
| I3 | and x8, x3, x7 | rs1: x3P10, rs2: x7P20 | rs1: x3P41(I0),rs2: x7P43(I2);rd: x8P44 |
重命名旁路的具体操作示例
表表 26.4清楚地展示了旁路修正的过程。没有旁路机制,I1会使用P10(x3的旧映射),导致I1读取了错误的值。旁路确保了I1使用P41——这是I0为x3建立的最新映射。
设计提示
重命名旁路的关键约束在于它必须在一个时钟周期(或半个周期)内完成全部操作:RAT查询、比较、选择。这意味着重命名旁路网络不能太深——如果发射宽度过大(例如8发射或更宽),旁路网络的延迟可能成为整个流水线的频率瓶颈。一些处理器选择将重命名拆分为两级流水线来缓解这个问题,但这会增加分支预测失败的惩罚(因为流水线更深了)。
从硬件实现的角度看,旁路网络的结构取决于RAT的实现方式:
SRAM-RAT:RAT以SRAM实现,按逻辑寄存器编号索引。旁路在SRAM读出结果之后执行。每条指令的源操作数首先从SRAM读出旧的物理寄存器编号,然后与前面指令的写入结果进行比较和选择。SRAM-RAT的旁路相对简单,因为比较的是逻辑寄存器编号(位宽较窄,RISC-V为5位)。
CAM-RAT:RAT以CAM实现,按物理寄存器编号索引。CAM-RAT的旁路更为自然——当I0将
x3P41写入CAM时,I1对x3的CAM搜索天然地可以匹配到I0的写入。但这要求CAM的写入和搜索能够在同一周期内完成(write-before-read语义),或者通过额外的旁路逻辑来模拟这种语义。
在大多数现代处理器中,SRAM-RAT因其更好的面积和功耗特性而被优先选择,旁路通过显式的比较器和多路选择器实现。
空闲列表的多端口分配
在旁路网络正确工作的前提下,空闲列表需要在同一周期内为最多条指令各分配一个物理寄存器。对于6发射处理器,空闲列表需要每周期输出最多6个不同的物理寄存器编号。
如果空闲列表采用FIFO实现,这等效于一次出队6个元素。FIFO的多出队操作可以通过以下方式实现:
多头指针FIFO:维护一个读指针,每周期将读指针推进个位置(是实际需要分配的数量)。读出的个元素分别在FIFO的偏移位置0, 1, ..., 处。这需要个独立的读端口或一个-wide的读窗口。
预取缓冲区:在FIFO前方放置一个项的预取缓冲区(prefetch buffer),每周期从FIFO中填充到项。分配时从预取缓冲区中取,不直接访问FIFO。这将多端口的读操作转化为单端口的FIFO和一个简单的移位寄存器。
预取缓冲区方案更为常用,因为它避免了FIFO的多读端口开销。分配逻辑只需从缓冲区的前项读取,然后将缓冲区左移位,下一周期从FIFO补充项到缓冲区尾部。
分配的物理寄存器编号需要在比较器启动之前就绪(因为后面的指令可能通过旁路读取前面指令刚分配的物理寄存器编号),因此空闲列表的读取延迟必须短于或等于RAT的读取延迟。预取缓冲区方案的读取延迟仅为12个FO4(简单的多路选择器),远短于RAT的812个FO4,因此不在关键路径上。
下面的SystemVerilog代码展示了一个4发射处理器中RAW旁路的核心逻辑:
// 4-wide RAW bypass logic for rename stage
// rs_addr[i][j]: source j of instruction i (j=0,1)
// rd_addr[i]: destination register of instruction i
// rd_valid[i]: instruction i has a destination register
// rat_result[i][j]: RAT lookup result for source j of inst i
// new_prd[i]: newly allocated physical register for inst i
// final_prs[i][j]: final physical register for source j of inst i
always_comb begin
for (int i = 0; i < 4; i++) begin
for (int j = 0; j < 2; j++) begin
// Default: use RAT lookup result
final_prs[i][j] = rat_result[i][j];
// Check all prior instructions for bypass
// Later instructions have higher priority
for (int k = 0; k < i; k++) begin
if (rd_valid[k] &&
rs_addr[i][j] == rd_addr[k]) begin
final_prs[i][j] = new_prd[k];
end
end
end
end
end注意上述代码中内层循环for (int k = 0; k < i; k++)的写法:k从0递增到i-1,这意味着如果有多条前面指令写同一个寄存器,后面的赋值会覆盖前面的——自然实现了"选择程序序中最近的匹配"的语义。在综合为硬件时,这个循环会被展开为一组并行的比较器和一个优先级编码器驱动的多路选择器。
重命名阶段的Move消除
Move消除(move elimination)是现代处理器在重命名阶段执行的一种重要优化。当检测到一条寄存器到寄存器的move指令(如mv rd, rs,即addi rd, rs, 0)时,不为其分配新的物理寄存器,而是直接将rd映射到rs当前对应的物理寄存器。这样move指令在重命名阶段就完成了,不需要进入执行阶段。
Move消除的硬件实现
Move消除的核心逻辑如下:
检测move指令:解码阶段标记指令是否为纯寄存器move(无立即数加减、无移位等)。在RISC-V中,
addi rd, rs, 0和add rd, rs, x0都是寄存器move。跳过物理寄存器分配:move指令不从空闲列表分配新的物理寄存器。
复制映射:在RAT中,将rd的映射设为rs当前的物理寄存器编号。
增加引用计数:由于rd和rs现在共享同一个物理寄存器,该物理寄存器的引用计数需要加1。只有当所有引用该物理寄存器的逻辑寄存器都被重新映射后,才能释放它。
// Move elimination in rename stage
always_comb begin
for (int i = 0; i < DISPATCH_WIDTH; i++) begin
if (is_move[i]) begin
// Don't allocate new physical register
new_prd[i] = final_prs[i][0]; // Use source's phys reg
alloc_en[i] = 1'b0; // Skip free list allocation
// Mark as eliminated - won't enter execution
eliminated[i] = 1'b1;
end else begin
new_prd[i] = freelist_alloc[i];
alloc_en[i] = rd_valid[i];
eliminated[i] = 1'b0;
end
end
endMove消除的收益与限制
Move消除带来三重收益:
节省物理寄存器:每消除一条move,就少分配一个物理寄存器。在典型程序中,move指令约占5%15%(RISC-V中由于ABI调用约定,函数入口/出口的参数搬运产生大量move),因此move消除可以有效减少物理寄存器的压力。
节省执行带宽:消除的move不需要占用功能单元,释放了ALU的执行槽位。
降低延迟:move指令的结果在重命名阶段就已可用,后续依赖指令可以更早被唤醒。
但move消除也引入了复杂性:
引用计数管理:共享物理寄存器的引用计数增加了释放逻辑的复杂度。需要为每个物理寄存器维护一个引用计数器(通常23位),每次move消除时加1,每次旧映射被覆盖时减1。只有引用计数为0时才能释放。
与推测恢复的交互:当分支预测失败恢复到Checkpoint时,引用计数也需要恢复。这要求Checkpoint中额外保存所有物理寄存器的引用计数——对于128个物理寄存器、每个2位计数,额外增加256位/Checkpoint。
同周期内的move依赖链:如果同一周期内有多条move指令形成依赖链(如
mv x3, x1;mv x5, x3),旁路网络需要正确传递move消除后的物理寄存器编号。
案例研究 1 — Intel Ivy Bridge的Move消除
Intel从Ivy Bridge微架构(2012年)开始引入move消除。在Ivy Bridge中,寄存器到寄存器的MOV指令(整数和浮点)在重命名阶段被消除,不占用执行端口。据Intel报告,move消除在典型工作负载中可以减少约5%8%的微操作数量,相应地提高了有效IPC。
后续的Haswell、Skylake等微架构进一步扩展了move消除的适用范围,增加了对零扩展move(MOVZX)的支持。AMD从Zen微架构开始也支持move消除。
零寄存器与零值优化
RISC-V架构将x0硬连线为零——读取x0始终返回0,写入x0不产生任何效果。这个设计在重命名阶段有两个重要影响:
x0的重命名处理
读取x0:当源寄存器为x0时,不需要查询RAT——结果始终为物理寄存器P0(假设P0被永久分配给x0,其值固定为0)。这可以在RAT查询之前就确定,减少RAT的读端口负载。
写入x0:当目的寄存器为x0时,指令的写操作实际上不需要执行。重命名阶段可以将此类指令标记为"无目的寄存器"(rd_valid = 0),不为其分配物理寄存器,也不更新RAT。这等效于将写x0的指令在重命名阶段消除——类似move消除,但更简单。
// x0 optimization in rename stage
always_comb begin
for (int i = 0; i < DISPATCH_WIDTH; i++) begin
// Source x0 -> physical register P0 (hardwired zero)
for (int j = 0; j < 2; j++) begin
if (rs_addr[i][j] == 5'd0)
final_prs[i][j] = PHYS_REG_ZERO; // P0
end
// Destination x0 -> no allocation, no RAT update
if (rd_addr[i] == 5'd0) begin
rd_valid_renamed[i] = 1'b0;
alloc_en[i] = 1'b0;
end
end
end零值传播优化
除了x0之外,处理器还可以检测运行时产生零值的指令。例如xor x3, x5, x5(自身异或)、sub x3, x5, x5(自身减法)始终产生零。重命名阶段可以识别这些"零值生成器",将其目的寄存器映射到P0(零寄存器),无需分配新的物理寄存器,也无需执行。
这种零值传播(zero-idiom elimination)在x86处理器中非常常见。编译器经常使用XOR EAX, EAX来清零寄存器,Intel从Sandy Bridge开始就在重命名阶段消除这类指令。据统计,零值传播在SPECint基准测试中可以额外消除约1%3%的微操作。
解决WAW相关性
WAW(Write After Write)是假相关性——两条指令写同一个逻辑寄存器,但它们之间没有真正的数据流依赖。寄存器重命名通过为每条指令分配不同的物理寄存器来消除WAW。但当同一周期内有多条指令写同一个逻辑寄存器时,需要特殊处理以确保RAT中只保留正确的映射。
同一周期内多条指令写同一寄存器
考虑以下指令序列在同一周期进入重命名:
add x3, x1, x2 # I0: 写 x3 -> 分配 P41
mul x5, x3, x4 # I1: 读 x3, 写 x5 -> 分配 P42
sub x3, x6, x7 # I2: 写 x3 -> 分配 P43
or x8, x3, x9 # I3: 读 x3 -> 应该用 P43这里I0和I2都写x3,构成WAW关系。重命名后,I0将x3映射到P41,I2将x3映射到P43。关键问题是:重命名结束后,RAT中x3应该映射到哪个物理寄存器?
答案是:只有程序序中最后一条写该寄存器的指令建立有效映射。在上例中,I2是最后一条写x3的指令,所以RAT中x3的最终映射应该是P43。I0虽然也写了x3P41,但这个映射在I2的写入之后就被覆盖了。
在硬件实现中,处理WAW的方式取决于RAT的更新策略:
顺序写入法:在重命名阶段的末尾,按照程序序依次将每条指令的目的寄存器映射写入RAT。自然地,后面的写入会覆盖前面的写入。这种方法在逻辑上最简单,但如果发射宽度为,则RAT的写操作需要串行化次,时序上不可接受。
并行写入+优先级仲裁:所有指令的目的寄存器映射同时写入RAT,但当多条指令写同一个逻辑寄存器时,通过优先级逻辑仅让程序序中最后一条指令的写入生效。这需要一个写冲突检测电路,检查所有指令对的目的寄存器是否相同,并屏蔽(mask)被覆盖指令的写使能信号。
先仲裁再写入:在写入RAT之前,先检测所有WAW冲突,然后只生成有效的写入。对于每个目的寄存器,在所有写同一逻辑寄存器的指令中,只有最后一条的写使能为有效。这种方法更为常用。
WAW检测所需的比较器数量为:
对于6发射处理器,个比较器。注意这与RAW检测的比较器是不同的——RAW比较的是源寄存器与目的寄存器,WAW比较的是目的寄存器与目的寄存器。
硬件描述 3 — WAW写冲突仲裁逻辑
以下SystemVerilog代码展示了一个4发射处理器的WAW写冲突仲裁逻辑。对于每条指令,只有在它是最后一条写该逻辑寄存器的指令时,其RAT写使能才为有效。
// 4-wide WAW arbitration
// rd_valid[i]: instruction i has a destination register
// rd_addr[i]: logical register number of instruction i's rd
// wr_en[i]: RAT write enable after arbitration
logic [3:0] overridden;
always_comb begin
overridden = 4'b0000;
// Check if any later instruction writes the same register
for (int i = 0; i < 4; i++) begin
for (int j = i + 1; j < 4; j++) begin
if (rd_valid[i] && rd_valid[j] &&
rd_addr[i] == rd_addr[j]) begin
overridden[i] = 1'b1; // i is overridden by j
end
end
end
// Only the last writer gets write enable
for (int i = 0; i < 4; i++) begin
wr_en[i] = rd_valid[i] && !overridden[i];
end
end需要注意的是,即使I0的x3P41映射被I2覆盖而不写入RAT,P41仍然被分配给I0作为其目的寄存器。I1在旁路网络中获得的x3映射应该是P41(而不是P43),因为在程序序中,I1在I2之前,它需要的是I0的结果。只有I3才应该使用P43。换言之,WAW仲裁只影响RAT的最终更新,不影响旁路网络中对RAW的正确转发。
这一点看似简单,但在硬件实现中容易出错。RAW旁路逻辑中的优先级选择必须选择程序序中最近的匹配(而不是"最后的匹配"),而WAW仲裁选择的是程序序中最后的写入者。对于I3的源寄存器x3:
RAW旁路需要选择I2(最近的写x3的指令)的新物理寄存器P43——正确。
如果错误地选择了I0的P41,则I3会读到I0的结果而非I2的结果。
对于I1的源寄存器x3:
RAW旁路需要选择I0(I1之前最近的写x3的指令)的新物理寄存器P41——正确。
如果错误地选择了I2的P43,则I1会读到I2的结果,违反了程序序语义。
因此,RAW旁路和WAW仲裁虽然使用类似的比较器结构,但它们的优先级方向和作用完全不同,必须独立实现。
WAW写仲裁的门级实现
WAW仲裁逻辑的门级实现涉及两个层面:(1)比较器矩阵检测哪些指令对写同一逻辑寄存器;(2)优先级编码器确定哪些指令的RAT写入应该被屏蔽。
对于6发射处理器,WAW比较器矩阵是一个上三角矩阵,共个比较器。每个比较器的结构与RAW比较器相同(5位XNOR+AND),总面积约 GE。
关键的优先级编码逻辑为每条指令生成一个"被覆盖"(overridden)信号。对于指令,如果存在任何使得,则,该指令不写入RAT。
图图 26.6展示了WAW仲裁的逻辑结构。I0的overridden信号需要一个5输入OR门(因为I1I5的任何一条都可能覆盖I0),I1需要4输入OR,以此类推。I5的overridden始终为0——作为程序序中最后一条指令,它不会被任何后续指令覆盖。
WAW仲裁的关键路径为:比较器(3 FO4)+ 最大OR门(5输入OR 2 FO4)+ 写使能AND门(1 FO4)= 6 FO4。这条路径与RAW旁路的MUX链是并行的,通常不在关键路径上。
RAW旁路与WAW仲裁的协同工作
RAW旁路和WAW仲裁在硬件上共享比较器的结果。具体而言,RAW比较器检测的是"后面指令的源 == 前面指令的目的",而WAW比较器检测的是"两条指令的目的 == 目的"。虽然比较的对象不同,但底层的逻辑寄存器编号比较器可以复用——只要正确地连接输入端。
在实际设计中,一种常见的优化是将RAW和WAW的比较器统一为一个更大的比较器矩阵:
对于6发射处理器,总共需要的比较器为:RAW比较器(30个)+ WAW比较器(15个)= 45个5位比较器。
部分比较操作可以复用。例如检测I0.rd == I2.rs1(RAW)和I0.rd == I2.rd(WAW)时,I0.rd只需要被扇出到两个比较器即可。
总面积约 GE 315 m(7nm),仍然很小。
释放旧的物理寄存器
每当一条指令将逻辑寄存器x3从旧的物理寄存器P10重新映射到新的物理寄存器P41时,P10就成为了一个"旧映射"。这个旧的物理寄存器P10最终需要被释放回空闲列表,以供后续指令使用。但释放的时机非常关键——如果过早释放,可能有尚未执行的指令仍然需要读取P10中的值。
安全释放的条件:旧物理寄存器可以被释放,当且仅当满足以下两个条件:
建立新映射的指令已经提交(commit/retire)——这意味着该指令已经正确执行完毕,不会被撤销。在提交之前,如果发生分支预测失败,这条指令可能被撤销,x3的映射需要恢复为P10,此时P10不能被释放。
所有可能读取的指令都已经读取完毕——实际上,由于乱序执行的存在,一些在程序序中位于重命名指令之前、但尚未执行的指令可能仍然需要读取。
在使用统一PRF的处理器中,第二个条件通常通过以下方式隐式保证:在提交时释放旧映射。因为一条指令只有在前面所有指令都已提交的情况下才能提交(ROB按序提交),而一条指令提交意味着它已经执行完毕。在它之前的所有指令也都已经执行完毕,自然已经读取了所需的物理寄存器值。因此,在指令提交时释放该指令覆盖的旧物理寄存器是安全的。
实现上,旧物理寄存器的编号需要在重命名阶段被记录下来。当指令进入重命名时,I0将x3从P10重新映射到P41,此时P10(旧映射)被保存到ROB的对应表项中。当I0最终提交时,ROB读取该表项,将P10释放回空闲列表。
安全释放条件的形式化推导
物理寄存器释放的安全性可以用以下定理精确描述:
定理(物理寄存器安全释放条件):设指令在重命名时将逻辑寄存器的映射从旧物理寄存器更改为新物理寄存器。可以安全释放,当且仅当以下两个条件同时满足:
已经提交(确认为非推测性的、不会被撤销的指令)。
所有在程序序中位于之前且源操作数映射到的指令都已经读取了的值。
下面通过两个反例说明违反任一条件的后果。
反例一:过早释放——违反条件C1
考虑以下场景:
add x3, x1, x2 # I0: x3 P10->P41 (P10变为旧映射)
beq x4, x0, target # I1: 分支指令 (推测执行)
sub x5, x3, x6 # I2: 读 x3->P41 (在推测路径上)假设I0在执行完毕后(但尚未提交),就将旧映射P10释放回空闲列表。此时如果I1的分支预测被确认为错误:
处理器需要恢复到I1之前的状态,即x3应该映射回P10(I0之前的状态)。
但如果使用Checkpoint恢复到I1之前的时间点(即I0也要被撤销),则x3需要恢复到I0重命名之前的映射P10。
此时P10已经被释放并可能被后续指令分配使用——P10中的旧值已被覆盖!
恢复后的程序读取P10将获得错误的数据,导致程序逻辑错误。
这个反例说明:在指令提交之前释放旧映射是不安全的,因为推测执行的撤销可能需要恢复到旧映射。只有当指令提交(确认不会被撤销)后,旧映射才不再被需要。
反例二:过晚释放——资源浪费
过晚释放不会导致正确性问题,但会造成严重的资源浪费:
add x3, x1, x2 # I0: x3 P10->P41, 提交于周期 100
# ... 500条指令后 ...
sub x3, x4, x5 # I500: x3 P41->P80, 提交于周期 300如果P10的释放被推迟到I500提交(周期300),而非I0提交(周期100),则P10在周期100到300之间被白白占用了200个周期。在此期间,P10中保存的值对任何指令都没有用处(因为I0之前所有读x3的指令都在I0提交前已经完成),但它无法被重新分配给新指令使用。
如果处理器有128个物理寄存器、32个逻辑寄存器,可用的"自由"物理寄存器为个。如果平均每个物理寄存器因过晚释放而多占用50个周期,在高吞吐场景下可能导致空闲列表枯竭,进而触发前端暂停。
量化分析:假设指令提交率为每周期4条,每条指令平均占用1个物理寄存器。在稳态下,空闲列表中需要维持的最小物理寄存器数等于从重命名到提交的流水线深度乘以提交宽度:
其中是从重命名到提交的典型延迟(包括发射等待、执行、写回等),通常为2050个周期。如果过晚释放导致每个物理寄存器多占用个周期,等效于需要额外个物理寄存器,直接增加PRF面积。
设计权衡 1 — 提前释放物理寄存器
等到提交时才释放旧映射是最保守、最安全的策略,但它也意味着物理寄存器被占用的时间最长。如果ROB很深(例如512项),一个物理寄存器可能被占用数百个周期才释放,这要求PRF拥有大量的物理寄存器。
一些激进的设计尝试提前释放物理寄存器。例如,当确定所有读取该物理寄存器的指令都已经执行完毕时,就可以释放它——即使写入该物理寄存器的指令尚未提交。这需要维护每个物理寄存器的引用计数(reference count),记录还有多少未执行的指令需要读取它。当引用计数降为零且新映射已经建立时,就可以安全释放。
引用计数机制可以显著减少所需的物理寄存器数量(减少约15%25%),但增加了硬件复杂度。每次重命名时需要增加相关物理寄存器的引用计数,每次指令读取操作数时需要减少计数。在宽发射处理器中,这些操作需要大量的读写端口,可能反而成为面积和时序的瓶颈。因此,大多数商业处理器仍然采用在提交时释放的保守策略。
引用计数的硬件开销分析
引用计数方案的硬件开销可以精确计算。对于128个物理寄存器的引用计数表:
计数器位宽:每个物理寄存器的引用计数需要记录有多少未执行的指令读取它。在最坏情况下,一个物理寄存器可能被ROB中所有指令的源操作数引用(极端情况)。但实际中,引用计数通常不超过(6发射、每条指令2个源),因此4位计数器足够(最大值15)。总存储量为位。
写端口需求:每周期最多有条指令被重命名(需要增加计数)和条指令读取操作数(需要减少计数),加上条指令被提交(可能触发释放)。引用计数表需要个增端口和个减端口(读取+提交),总共个写端口。对于6发射处理器,这是18个写端口——这对于一个512位的SRAM来说是极其昂贵的。
面积估算:18端口的512位SRAM面积约为单端口的倍,即约 m 0.05 mm。这比保守策略(无引用计数)增加的面积,可能超过了节省的15%25%物理寄存器所减少的PRF面积。
这个面积分析解释了为什么引用计数在商业处理器中很少被采用:它用一个多端口结构替代了一些简单的单端口存储,总面积反而可能增加。只有在物理寄存器非常昂贵(如宽向量PRF)的场景下,引用计数才可能带来净面积收益。
对于同一周期内WAW冲突中被覆盖的映射(如上例中I0的x3P41),情况更为微妙。I0分配了P41,但I2随后又将x3映射到P43。P41在I0写回结果后仍然有用——I1需要从P41读取数据。P41的释放时机仍然遵循上述规则:当I0提交时(此时I1必然已经执行完毕),将I0覆盖的旧映射P10释放。I2提交时,将I2覆盖的旧映射P41释放。
在这个例子中,物理寄存器的生命周期如下:
P10(x3的原始映射):在I0重命名时变为旧映射,在I0提交时被释放。
P41(I0为x3分配的新映射):在I2重命名时变为旧映射(因为I2也写x3),在I2提交时被释放。注意P41虽然在RAT的最终状态中不是x3的映射,但它仍然被I0使用,因此不能提前释放。
P43(I2为x3分配的新映射):在下一条写x3的指令被重命名时变为旧映射,生命周期更长。
由此可见,物理寄存器的生命周期从被分配开始,到被某条后续指令覆盖且该后续指令提交时结束。生命周期越长,PRF需要的物理寄存器数量就越多。
寄存器重命名过程的恢复
在超标量处理器中,当发生分支预测失败、异常或中断时,处理器需要撤销错误路径上所有指令的效果,恢复到正确的程序状态。对于寄存器重命名来说,这意味着必须将RAT恢复到错误指令之前的状态——即撤销错误路径上所有指令建立的映射。
重命名状态的恢复是超标量处理器设计中最关键的问题之一,因为恢复的速度直接影响分支预测失败的惩罚。恢复越快,处理器从错误路径恢复到正确路径的延迟越小,性能损失越小。
本节讨论三种主要的恢复方式:Checkpoint(检查点)、WALK(逐步回退)和Architecture State(使用提交映射表恢复)。
使用Checkpoint
Checkpoint(检查点)的思想非常直接:在每个可能需要恢复的点(主要是分支指令处),保存当前RAT的一个完整快照。当需要恢复时,直接将对应的快照加载回RAT,立即恢复到正确状态。
Checkpoint的操作流程:
保存快照:当一条分支指令进入重命名阶段时,将当前的推测RAT(Speculative RAT)的全部内容复制到一个空闲的Checkpoint槽中。同时保存空闲列表的状态(哪些物理寄存器是空闲的)。这个保存操作需要在重命名的同一个周期内完成。
正常推进:分支指令之后的指令继续在推测RAT上进行重命名,修改RAT中的映射。Checkpoint中保存的快照不受影响。
恢复(预测失败时):当后端确认某条分支预测失败时,处理器将该分支对应的Checkpoint快照直接复制回推测RAT。这个恢复操作通常只需要1个周期。同时,空闲列表也从Checkpoint中恢复。恢复完成后,前端可以立即从正确的目标地址开始取指和重命名。
释放Checkpoint:当一条分支指令从ROB提交时(意味着它的预测被确认正确或该分支已经不再是推测性的),对应的Checkpoint可以被释放,供后续分支指令使用。
案例研究 2 — MIPS R10000的Checkpoint机制
MIPS R10000是最早采用Checkpoint恢复的商业处理器之一(1996年)。它维护了4个RAT快照(对应最多4条未决分支指令),每个快照保存全部32个整数逻辑寄存器到物理寄存器的映射。当一条分支指令进入重命名时,占用一个空闲快照槽;当该分支提交时,释放该快照槽。如果4个快照槽全部被占用(即有4条未决分支),则前端暂停,直到最早的分支提交释放一个槽。
R10000的Checkpoint还保存了空闲列表的头指针和活跃列表(Active List,即ROB)的尾指针,使恢复后能够正确回退到分支点的状态。恢复操作在1个周期内完成,是当时速度最快的恢复机制。
Checkpoint的硬件开销:
Checkpoint的主要开销是存储快照所需的SRAM。设处理器有个逻辑寄存器,物理寄存器编号为位,需要个Checkpoint槽,则Checkpoint缓冲区的总容量为:
以RISC-V为例,32个整数逻辑寄存器、128个物理寄存器(7位编号)、8个Checkpoint槽:
如果加上32个浮点寄存器的快照,容量翻倍为448字节。对于现代处理器来说,这个面积开销是可以接受的。但Checkpoint的面积随着发射宽度、逻辑寄存器数量和需要支持的未决分支数量的增加而线性增长。对于支持向量扩展的处理器(例如32个向量逻辑寄存器,每个映射到更多的物理寄存器),Checkpoint的面积可能变得显著。
Checkpoint的另一个开销是保存带宽。在一个6发射处理器中,一个周期内可能有多条分支指令进入重命名。如果每条分支都需要保存一个Checkpoint,则Checkpoint缓冲区需要多个写端口。在实践中,通常限制每周期最多保存12个Checkpoint,当同一周期内有更多分支时,暂停后续指令。
下面通过一个时序图展示Checkpoint的保存和恢复过程。
如图图 26.10所示,在C2周期I3(分支指令)进入重命名阶段时,当前的RAT快照被保存。此后的指令I6I14在推测路径上继续重命名。当后端在C6周期确认I3预测失败时,处理器立即恢复快照S,在C7周期就可以从正确的目标地址开始重命名新的指令T0T2。恢复的延迟仅为1个周期(不计从后端传到前端的信号传播延迟),这就是Checkpoint恢复的核心优势。
Checkpoint的空闲列表恢复问题
恢复RAT快照只是Checkpoint恢复的一半工作。另一半是恢复空闲列表——即将错误路径上分配的物理寄存器全部回收。如果不恢复空闲列表,这些物理寄存器将永久泄漏,最终耗尽所有物理寄存器资源。
空闲列表的Checkpoint有两种实现方式:
方式一:保存空闲列表的完整快照。将整个空闲列表的位图复制到Checkpoint中。对于128个物理寄存器,每个Checkpoint需要额外保存128位的位图。恢复时直接加载位图。这种方式恢复速度快(1周期),但Checkpoint的面积增加位。
方式二:保存空闲列表的头指针。如果空闲列表采用FIFO实现,只需保存FIFO的头指针(位)。恢复时将头指针回退到Checkpoint保存的位置。这种方式面积极小,但要求空闲列表是严格的FIFO,且恢复时需要同时丢弃FIFO中已经出队但未写回的物理寄存器。大多数商业处理器采用这种方式。
| 方式 | 每个Checkpoint额外位数 | 8个槽总面积 | 恢复复杂度 |
|---|---|---|---|
| 完整位图 | 128位 | 1024位 = 128B | 简单(1周期加载) |
| 头指针 | 7位 | 56位 = 7B | 需要FIFO语义保证 |
两种空闲列表Checkpoint方式的对比(128个物理寄存器、8个Checkpoint槽)
Checkpoint的端口与带宽挑战
Checkpoint缓冲区的读写带宽是一个容易被忽视的实现难点。在一个6发射处理器中:
保存带宽:每周期最多可能有6条分支指令同时进入重命名。每条分支都需要保存一个Checkpoint。如果每周期只能保存1个Checkpoint,则其余分支必须等待——这等效于将分支的处理限制为每周期1条。在实践中,一个周期内出现超过2条分支的概率极低(程序中分支密度约15%25%,6条指令中平均12条分支),因此大多数设计只支持每周期保存12个Checkpoint。
恢复带宽:恢复操作通常只需1个读端口(因为同一时刻只恢复1个Checkpoint)。但恢复信号需要在1个周期内驱动整个RAT的所有位——对于32个逻辑寄存器7位物理寄存器编号 = 224位,这是一个扇出较大的数据路径。
释放带宽:当分支提交时,对应的Checkpoint槽需要被释放。如果处理器每周期提交6条指令,最多有6个Checkpoint槽同时需要释放。Checkpoint缓冲区需要支持每周期释放多个槽。
综合考虑保存、恢复和释放的带宽需求,Checkpoint缓冲区在物理设计上通常实现为具有2个写端口(保存)、1个读端口(恢复)和多个释放标记端口的专用SRAM结构。
使用WALK
WALK(逐步回退)是一种不需要保存完整RAT快照的恢复方式。其基本思想是:利用ROB中记录的重命名信息,从恢复点开始逐条反向遍历指令,撤销每条指令建立的映射——将每条指令的目的寄存器从新映射恢复为旧映射。
WALK的操作流程:
当检测到分支预测失败时,确定该分支在ROB中的位置。
从ROB的尾部(最新的指令)开始,向分支点方向逐条读取ROB表项。
对于每条指令,如果它有目的寄存器,则将RAT中该逻辑寄存器的映射从"新物理寄存器"恢复为"旧物理寄存器"。例如I5将x9的映射从P45恢复为P15。
同时,将新分配的物理寄存器(P45)释放回空闲列表。
逐条处理直到到达分支指令的位置,此时RAT已恢复到分支指令执行前的状态。
WALK的恢复延迟取决于分支点和ROB尾部之间的指令数量。设分支之后有条错误路径上的指令已经进入ROB,每个周期WALK处理器能够回退条指令(取决于ROB的读端口数),则恢复时间为个周期。在最坏情况下,如果ROB几乎被填满,恢复可能需要数十甚至上百个周期。
例如,如果ROB有256项,错误路径上有128条指令,WALK每周期处理4条,则恢复需要32个周期——这比Checkpoint的1周期恢复慢了32倍。
WALK的硬件优势:
不需要Checkpoint缓冲区的SRAM面积。
不限制未决分支指令的数量——因为不需要为每条分支分配Checkpoint槽。
ROB中本来就需要记录旧映射信息(用于提交时释放旧物理寄存器),WALK只是额外利用了这些信息。
WALK的劣势:
恢复延迟大且不确定——取决于错误路径的长度。
在恢复期间,前端和重命名阶段必须暂停(stall),因为RAT处于不一致状态。
WALK过程中ROB需要额外的读端口来支持逐条回退,这增加了ROB的端口复杂度。
设计提示
WALK恢复的一个实际优化是增加WALK的带宽——每周期回退多条指令。如果WALK的宽度与发射宽度相同(例如6条),则恢复延迟可以大幅降低。但这需要ROB提供6个额外的读端口,面积和功耗开销显著。另一种折中方案是仅在关键路径上使用Checkpoint(例如为最近的4条分支保存快照),更早的分支使用WALK恢复。这种混合策略兼顾了恢复速度和硬件开销。
案例研究 3 — Intel P6微架构的WALK恢复
Intel P6微架构(Pentium Pro/II/III, 19951999)采用了WALK方式进行重命名恢复。P6的ROB有40个表项(后期型号增至126项),每个表项记录了指令的目的寄存器、旧的和新的物理寄存器编号等信息。
当分支预测失败时,P6从ROB尾部开始逐条回退。由于P6的发射宽度为3,WALK每周期处理3条指令。对于一个40项ROB中有20条错误路径指令的典型情况,WALK需要约7个周期——加上从执行阶段传回预测失败信号的延迟,总的分支预测失败惩罚约为1115个周期。
尽管WALK恢复的延迟不如Checkpoint,P6通过其高效的分支预测器(当时精度领先)和较短的流水线来弥补这一不足。Intel从Nehalem微架构开始逐步引入Checkpoint机制以降低恢复延迟。
WALK恢复还有一个需要注意的细节:WALK的方向必须是从ROB尾部向分支点(而不是从分支点向尾部),因为需要以反向程序序撤销映射变更。如果从分支点正向扫描到尾部,则无法正确恢复——因为一个逻辑寄存器可能被多条指令连续重写,正向扫描时前面恢复的映射会被后面的指令再次覆盖为错误值。反向扫描保证每个逻辑寄存器的最终恢复值就是分支点处的正确映射。
WALK恢复的完整工作示例
下面通过一个完整的例子展示WALK恢复的逐周期过程。初始RAT状态为:x3P10, x5P12, x7P20, x9P15。
| 周期 | 处理的指令 | RAT恢复操作 | 空闲列表回收 |
|---|---|---|---|
| 检测到I2预测失败 | (WALK启动) | — | |
| I5: xor x9 (P15P45) | x9: P45P15 | 回收P45 | |
| I4: and x3 (P41P44) | x3: P44P41 | 回收P44 | |
| I3: or x7 (P20P43) | x7: P43P20 | 回收P43 | |
| (到达I2分支点,WALK结束) | — | — |
WALK恢复逐周期操作(,每周期回退2条)
表表 26.6展示了WALK的逐周期操作。在周期检测到预测失败,周期开始WALK,每周期回退2条指令(从I5到I4,再到I3)。总共需要2个周期完成WALK。WALK结束后,RAT恢复到I2分支指令之前的状态:x3P10(注意:I4将x3从P41恢复为P41,但在周期I4先被处理——这里的"旧映射"P41是I4进入ROB时记录的值;随后I3的旧映射在正确路径的I0中建立——这取决于ROB中记录的是哪个"旧映射"。在完整的实现中,ROB中每条指令记录的旧物理寄存器是该指令重命名时从RAT中替换出来的值)。
注意WALK期间前端完全暂停——不取指、不解码、不重命名。这2个周期加上从后端到前端的信号传播延迟(通常12周期)和前端重新填充流水线的延迟(通常35周期),总的分支预测失败惩罚约为69个周期。相比之下,Checkpoint方式只需将信号传播和前端重填延迟(47个周期)作为惩罚,节省了WALK的2个周期。
WALK的ROB端口需求分析
WALK恢复需要从ROB中读取每条被撤销指令的重命名信息(逻辑寄存器编号、旧物理寄存器编号、新物理寄存器编号)。这些读取操作使用的ROB端口与正常提交使用的端口不同——提交从ROB头部读取,WALK从ROB尾部读取。
设WALK的宽度为(每周期回退的指令数),则ROB需要额外个读端口用于WALK操作。每个端口的读出宽度包括:
逻辑寄存器编号:5位(RISC-V)
旧物理寄存器编号:位(例如7位)
新物理寄存器编号:7位
rd_valid标志:1位
合计:位/表项
对于,ROB需要4个额外的20位读端口。由于ROB通常已经有个写端口(分发)和个读端口(提交),额外的WALK端口使ROB的总端口数增加约30%50%。多端口SRAM的面积大约按增长(其中和分别是读写端口数),因此WALK端口的面积开销不可忽视。
一种优化是复用提交端口:在WALK期间,ROB不需要提交(因为前端已经暂停),可以将提交端口临时复用为WALK端口。这避免了额外端口的面积开销,但需要在正常操作和WALK操作之间增加多路选择器来切换端口的地址和方向。
WALK与空闲列表的回收
在WALK过程中,除了恢复RAT,还需要将错误路径上分配的物理寄存器释放回空闲列表。WALK逐条处理时,每条指令的"新物理寄存器"(new_prd)需要被归还到空闲列表中。
如果空闲列表是FIFO结构,回收操作只需将这些物理寄存器重新推入FIFO尾部。但这里有一个微妙的问题:WALK回退的顺序是从ROB尾部到分支点(反向程序序),而FIFO的push操作是追加到尾部。这意味着回收后的FIFO顺序与原始分配顺序不同——最后分配的物理寄存器最先被回收,变成了FIFO中最先被分配的。
这种顺序不一致在功能上不影响正确性(任何空闲的物理寄存器都可以被分配给任何指令),但可能影响物理寄存器的时间局部性——如果某些程序模式倾向于重用最近释放的物理寄存器,乱序回收可能导致PRF的功耗和cache行为略有变化。在实践中,这个影响非常小,通常被忽略。
使用Architecture State
Architecture State(架构状态恢复)方式利用一张独立维护的提交映射表(Committed RAT,简称CRAT或Retirement RAT)来恢复推测RAT。CRAT始终反映已提交指令的映射状态——即非推测性的、确定正确的映射。
Architecture State恢复的操作流程:
处理器维护两张RAT:推测RAT(SRAT)用于重命名阶段,由推测性指令更新;提交RAT(CRAT)由ROB在指令提交时更新,始终保持已提交状态的映射。
当发生需要恢复的事件(分支预测失败、异常等)时,处理器首先暂停前端取指,然后等待ROB中在错误分支之前的所有指令都完成执行并提交。这个过程称为ROB排空(drain)。
ROB排空完成后,CRAT中的映射精确反映了错误分支执行前一刻的正确状态。此时将CRAT的全部内容复制到SRAT,恢复完成。
恢复完成后,前端从正确的目标地址重新开始取指。
Architecture State恢复的延迟是所有方式中最大的。它的延迟由两部分组成:
其中是等待ROB排空的时间(可能非常长,取决于ROB中待执行指令的数量和执行延迟),是将CRAT复制到SRAT的时间(通常12个周期)。
这种方式的核心瓶颈在于。如果ROB中有一条cache miss的load指令,它的执行延迟可能长达数百个周期,在此期间处理器完全停顿。
需要注意,Architecture State方式实际上不是精确恢复到分支点——它恢复的是最后一条已提交指令之后的状态。如果分支之前有一些指令尚未执行完毕(这在乱序处理器中很常见),则需要等待它们全部执行完毕并提交。这意味着处理器不能简单地丢弃错误路径上的指令——它必须继续执行正确路径上尚未完成的指令,同时暂停取指,直到所有正确路径指令都提交。
一种优化是"选择性排空"(selective drain):不是等待ROB中所有指令排空,而是只等待分支之前的指令执行完毕,同时立即丢弃分支之后的错误路径指令。但这仍然无法避免等待正确路径指令完成执行的延迟。
Architecture State的优势:
硬件开销最小——只需要一张额外的CRAT表(大小与SRAT相同),不需要Checkpoint缓冲区。
实现简单——CRAT的更新逻辑与ROB的提交逻辑紧密耦合,只需在指令提交时将新映射写入CRAT。
可以处理任何需要恢复的事件——不仅限于分支预测失败,也适用于精确异常和中断的处理。
Architecture State的劣势:
恢复延迟极大——需要等待ROB完全排空。
在等待排空的过程中,处理器完全不做有用功,浪费了大量的计算周期。
CRAT的更新逻辑与时序
CRAT的更新发生在指令提交时。对于一个6-wide的提交逻辑(每周期最多提交6条指令),CRAT需要6个写端口。但与SRAT不同,CRAT的写入不需要旁路网络(因为提交是按序的,不存在同周期内的RAW问题)。
CRAT的更新逻辑非常简单:
// CRAT update at commit
always_ff @(posedge clk) begin
for (int i = 0; i < COMMIT_WIDTH; i++) begin
if (commit_valid[i] && commit_rd_valid[i]) begin
crat[commit_rd_lreg[i]] <= commit_new_preg[i];
end
end
end与SRAT的更新类似,当同一周期内多条提交的指令写同一个逻辑寄存器时,需要WAW仲裁确保只有最后一条的映射被写入CRAT。但由于提交是按程序序进行的(不像重命名阶段需要处理旁路),CRAT的WAW仲裁逻辑与26.2.1 节节的逻辑完全相同,但位于提交通路而非重命名通路。
CRAT到SRAT的复制操作需要一个宽数据通路:位需要在1个周期内从CRAT传输到SRAT。在物理设计中,如果CRAT和SRAT距离较远(它们分别靠近ROB和重命名逻辑),这个224位的数据传输可能需要额外的中继寄存器或专用的长距离走线,增加约12个FO4的延迟。
案例研究 4 — Alpha 21264的双RAT设计
DEC Alpha 21264(1998年)采用了SRAT + CRAT的双RAT设计来支持Architecture State恢复。它维护两个完全独立的整数RAT(各31项,因为r31固定为零):
前端RAT(Front-end RAT):由重命名阶段更新,包含推测性映射。
退休RAT(Retire RAT):由退休逻辑更新,只包含已提交指令的映射。
当分支预测失败时,Alpha 21264需要等待错误分支之前的所有指令提交,然后将退休RAT复制到前端RAT。这个过程的延迟通常为715个周期(如果没有长延迟操作),但在最坏情况下可能达到数十个周期。
尽管Architecture State恢复的延迟较大,Alpha 21264通过其他设计选择(如激进的分支预测器、大容量的IQ)来弥补这一不足,在当时仍然达到了非常高的性能水平。
三种恢复方式的比较
三种恢复方式各有优劣,适用于不同的设计场景。表表 26.7从多个维度进行了对比。
| 特性 | Checkpoint | WALK | Architecture State |
|---|---|---|---|
| 恢复延迟 | 1个周期(最快) | 个周期(可变) | (最慢) |
| 额外硬件 | Checkpoint缓冲区(个快照),面积较大 | ROB额外读端口 | 一张CRAT表,面积最小 |
| 未决分支限制 | 受Checkpoint数量限制 | 无限制 | 无限制 |
| 恢复精度 | 精确恢复到分支点 | 精确恢复到分支点 | 恢复到最后提交点,需等待排空 |
| 适用事件 | 分支预测失败 | 分支预测失败 | 分支失败、异常、中断 |
| 实现复杂度 | 中等 | 较低 | 最低 |
| 典型处理器 | MIPS R10000, Apple M系列 | Intel P6微架构 | Alpha 21264 |
三种重命名恢复方式的比较
从表表 26.7可以看出,三种方式本质上是在恢复速度和硬件开销之间做权衡:
Checkpoint是恢复最快的方式,但需要额外的SRAM面积来存储快照,并且限制了未决分支的数量。它最适合高性能处理器,尤其是分支预测失败代价高昂的宽发射设计。
WALK不需要额外的存储面积,但恢复延迟不确定且可能很大。它适合面积受限的中等性能处理器,或作为Checkpoint的补充方式。
Architecture State实现最简单,但恢复延迟最大。它适合对面积和功耗要求严格的低功耗设计,或用于处理稀少但必须精确恢复的事件(如异常)。
设计权衡 2 — 混合恢复策略
现代高性能处理器通常不会只使用单一的恢复方式,而是采用混合策略:
Checkpoint + Architecture State:为最近的条分支维护Checkpoint,用于快速恢复最常见的预测失败。对于Checkpoint不覆盖的早期分支(已超过条),或者异常/中断等非分支事件,使用Architecture State恢复。这种混合策略在Apple的A系列/M系列处理器中被广泛使用。
Checkpoint + WALK:为最近的条分支维护Checkpoint,对于更早的分支使用WALK恢复。这种策略在恢复速度和面积之间取得了较好的平衡。
选择性Checkpoint:不是为每条分支都保存Checkpoint,而是只为低置信度(可能预测失败)的分支保存。这需要分支预测器提供一个置信度估计(confidence estimation),只有当置信度低于阈值时才分配Checkpoint槽。这可以减少Checkpoint的使用率,从而减少所需的Checkpoint数量。
为了更直观地理解三种方式的恢复延迟差异,考虑以下场景:一个256项ROB的处理器,在ROB中有条错误路径指令,WALK每周期处理4条,ROB排空需要个周期(取决于最长延迟的指令)。
| 场景 | Checkpoint | WALK | Arch State |
|---|---|---|---|
| , | 1 | 2 | 11 |
| , | 1 | 8 | 21 |
| , | 1 | 32 | 51 |
| , (含cache miss) | 1 | 32 | 201 |
不同场景下三种恢复方式的延迟(周期数)
表表 26.8清楚地展示了Checkpoint在恢复延迟上的绝对优势。特别是在错误路径较长或存在cache miss的情况下,Checkpoint的1周期恢复与Architecture State的数百周期恢复之间有量级的差距。这就是为什么高性能处理器几乎都采用Checkpoint作为分支预测失败的主要恢复机制。
三种恢复方式的面积量化对比
为了更精确地比较三种方式的硬件开销,我们以一个具体的处理器配置进行量化分析:6发射、32个整数逻辑寄存器、128个物理寄存器(7位编号)、256项ROB、最多8个未决分支。
Checkpoint的面积:
RAT快照:位
空闲列表头指针:位
Checkpoint有效位和标签:位
总计:位 字节
考虑多读写端口(2写1读),SRAM面积约 m(7nm)
WALK的额外面积:
ROB中本来就存储旧/新物理寄存器编号,不需要额外存储
额外的WALK控制逻辑:状态机+地址计算 200 GE m
如果复用提交端口:额外MUX逻辑 300 GE m
如果增加专用WALK端口:ROB面积增加约15%20%
总计(复用端口方案):约250 m
Architecture State的额外面积:
CRAT表:位SRAM(单写单读端口)
CRAT面积约 m
复制逻辑(CRATSRAT):位的MUX 150 GE m
总计:约243 m
| 方式 | 存储位数 | 估算面积(m) | 恢复延迟 | 面积延迟积 |
|---|---|---|---|---|
| Checkpoint (8槽) | 1920 | 2880 | 1周期 | 2880 |
| WALK (复用端口) | 0* | 250 | 264周期 | 25016000 |
| Architecture State | 224 | 243 | 10200+周期 | 243048600 |
三种恢复方式的量化面积对比(7nm工艺估算)
*WALK不需要额外存储,ROB中已有所需信息。
表表 26.9引入了"面积延迟积"作为综合评价指标。Checkpoint的面积最大但延迟最小,其面积延迟积在所有情况下都是最优的。WALK在错误路径较短时(28周期恢复)综合表现接近Checkpoint,但在最坏情况下急剧恶化。Architecture State在面积上最优,但延迟上的劣势使其综合评分最差。
恢复延迟对IPC的影响估算
分支预测失败的性能影响可以用以下公式量化:
其中是分支指令频率(典型值15%20%),是分支预测精度(典型值95%98%),是预测失败的惩罚周期数。
以6发射处理器为例,(受限于依赖和功能单元),,:
- Checkpoint(周期,含前端refill延迟):
- WALK(周期,含平均WALK时间):
- Architecture State(周期,含排空时间):
Checkpoint方案比WALK方案高出约27%的IPC,比Architecture State高出约81%。这一巨大差距解释了为什么现代高性能处理器几乎都采用Checkpoint作为主要恢复机制——2880 m的额外面积换来的IPC提升是非常划算的。
混合恢复策略的实现细节
现代处理器最常用的混合策略是Checkpoint + CRAT:
分支预测失败:使用Checkpoint快速恢复(1周期)。如果对应的分支已超过Checkpoint覆盖范围(所有个槽被更新的分支占据),则使用CRAT + ROB排空恢复。
精确异常:始终使用CRAT恢复。异常处理不需要极低延迟(异常处理程序本身就需要数十个周期来建立上下文),因此CRAT的排空延迟可以接受。
中断:同精确异常,使用CRAT恢复。
这种混合策略的控制逻辑需要一个状态机来判断恢复事件的类型,并选择对应的恢复路径:
// Recovery control state machine
typedef enum logic [1:0] {
REC_IDLE = 2'b00,
REC_CKPT = 2'b01, // Checkpoint restore
REC_DRAIN = 2'b10, // Waiting for ROB drain
REC_CRAT = 2'b11 // CRAT copy
} recovery_state_t;
always_comb begin
case (recovery_state)
REC_IDLE: begin
if (branch_mispredict && ckpt_valid[branch_ckpt_id])
next_state = REC_CKPT; // Fast path
else if (branch_mispredict || exception || interrupt)
next_state = REC_DRAIN; // Slow path
else
next_state = REC_IDLE;
end
REC_CKPT: begin
// Checkpoint restore completes in 1 cycle
next_state = REC_IDLE;
end
REC_DRAIN: begin
if (rob_empty)
next_state = REC_CRAT;
else
next_state = REC_DRAIN; // Continue draining
end
REC_CRAT: begin
// CRAT copy completes in 1 cycle
next_state = REC_IDLE;
end
endcase
end分发(Dispatch)
完成寄存器重命名后,指令进入分发(Dispatch)阶段。分发是连接前端(取指、解码、重命名)和后端(发射、执行、写回)的桥梁——它将重命名后的指令写入后端的各个队列和缓冲区,使指令准备好被乱序发射执行。
分发的功能
分发阶段需要将每条指令的信息写入以下三个主要的后端结构:
重排序缓冲区(ROB):ROB按照程序序记录所有in-flight的指令。分发时,需要在ROB尾部为每条指令分配一个表项,写入指令类型、目的逻辑寄存器编号、新/旧物理寄存器映射、分支预测信息等。ROB用于维护精确异常和按序提交。
发射队列(Issue Queue, IQ):IQ保存等待发射的指令。分发时,将指令的操作码、源/目的物理寄存器编号、操作数就绪状态等写入IQ的空闲表项。指令在IQ中等待其所有操作数就绪后被发射到功能单元执行。
Load/Store队列(LSQ):对于访存指令(load和store),还需要在LSQ中分配表项。Load Queue记录未完成的load操作,Store Queue记录未完成的store操作及其数据。LSQ用于维护内存操作的顺序一致性和实现store-to-load转发。
分发阶段的操作可以与重命名在同一级流水线完成(合并为一个Rename/Dispatch级),也可以拆分为单独的流水线级。合并的方式减少了流水线深度但增加了该级的时序压力;拆分则放松时序但增加了分支预测失败的惩罚。在现代高性能处理器中,重命名和分发通常被拆分为23级流水线。
表表 26.10列出了分发时需要写入各后端结构的具体信息。
| 目标结构 | 字段 | 说明 |
|---|---|---|
| ROB | 指令类型 | ALU/分支/Load/Store/系统指令等 |
| 目的逻辑寄存器 | 用于提交时更新CRAT | |
| 旧物理寄存器 | 提交时释放回空闲列表 | |
| 新物理寄存器 | 用于WALK恢复 | |
| 分支预测信息 | 预测方向、目标地址、BHR快照 | |
| 异常信息 | 解码阶段检测到的异常(如非法指令) | |
| IQ | 操作码 | 功能单元选择和操作控制 |
| 源物理寄存器编号 | prs1, prs2(重命名后的) | |
| 操作数就绪位 | 该源操作数是否已在PRF中就绪 | |
| 目的物理寄存器编号 | 用于唤醒后续依赖指令 | |
| ROB索引 | 用于将执行结果写回ROB | |
| LSQ | 访存类型 | Load/Store,字节宽度 |
| ROB索引 | 用于按序判定和异常处理 | |
| Store数据来源 | Store指令的数据寄存器编号 |
分发阶段写入各后端结构的信息
从表表 26.10可以看出,ROB的每个表项需要存储的信息最多——这也是ROB成为面积大户的原因之一。在一个6发射处理器中,每周期需要向ROB写入6个表项,每个表项约4060位,意味着ROB每周期需要接受约240360位的写入数据。
资源检查与流水线暂停
分发阶段需要同时向ROB、IQ和LSQ写入数据。如果这些结构中的任何一个已满(没有空闲表项),则分发必须暂停(stall),等待空间释放。
可能导致分发暂停的资源不足:
ROB满:ROB的所有表项都被占用,意味着有太多in-flight的指令尚未提交。这通常发生在后端执行遇到长延迟操作(如cache miss)导致提交速度下降时。
IQ满:发射队列没有空闲表项。这意味着已经有太多指令在等待操作数就绪。可能由执行单元的吞吐不足或操作数的长延迟等待引起。
LSQ满:Load Queue或Store Queue没有空闲表项。当程序中访存指令密度很高,或存在大量cache miss导致load/store长时间驻留在LSQ中时,会出现这种情况。
空闲物理寄存器不足:这实际上发生在重命名阶段而非分发阶段,但效果相同——如果空闲列表中没有足够的物理寄存器可供分配,整个前端(包括分发)都必须暂停。
Checkpoint槽不足:如果使用Checkpoint恢复机制,当所有Checkpoint槽都被占用且当前周期需要为新的分支保存快照时,前端需要暂停。
性能分析 2 — 分发暂停对性能的影响
分发暂停(dispatch stall)是超标量处理器性能的主要瓶颈之一。当分发暂停时,前端的取指、解码、重命名也都暂停——因为它们之间通过流水线寄存器紧密耦合。这意味着分发暂停会导致整个前端停顿,浪费大量的取指和解码带宽。
减少分发暂停的方法:
增加ROB、IQ、LSQ的容量——但面积和功耗增加。
增加物理寄存器的数量——但PRF的面积和读写端口增加。
提高后端的执行吞吐和提交速率——减少指令在各队列中的驻留时间。
在前端和后端之间加入解耦缓冲区(如指令队列/skid buffer)——允许前端在后端暂停时继续工作一段时间。
在典型的服务器工作负载(如SPECint)中,分发暂停导致的周期浪费约占总执行周期的10%20%。这个比例随着ROB和IQ容量的增加而下降,但永远不会降为零——因为长延迟事件(如L3 cache miss或TLB miss)总是会导致后端堵塞。
在实现上,分发阶段在每个周期开始时检查所有相关资源的可用性:
// Dispatch stall condition check
logic dispatch_stall;
always_comb begin
dispatch_stall = 1'b0;
// Check ROB availability
if (rob_free_entries < dispatch_width)
dispatch_stall = 1'b1;
// Check IQ availability
if (iq_free_entries < dispatch_width)
dispatch_stall = 1'b1;
// Check LSQ availability (for memory instructions)
if (lq_free_entries < num_loads_in_bundle ||
sq_free_entries < num_stores_in_bundle)
dispatch_stall = 1'b1;
// Check free physical registers
if (freelist_count < num_dests_in_bundle)
dispatch_stall = 1'b1;
end注意,上述检查中使用了dispatch_width作为ROB和IQ空闲表项的阈值——即使当前周期的指令束中实际需要的表项可能少于dispatch_width,使用固定阈值可以简化比较逻辑并提前一个周期生成暂停信号。一些设计使用精确的指令计数来减少不必要的暂停,但这增加了组合逻辑的延迟。
各暂停条件的检测逻辑详解
下面分别分析每种暂停条件的具体检测电路。
(1)ROB满检测:ROB通常实现为循环缓冲区(circular buffer),用头指针(head)和尾指针(tail)管理。空闲表项数为:
这个计算可以简化为,其中count是一个独立维护的计数器。检测逻辑为:
// ROB availability check
// rob_count: number of occupied ROB entries (maintained by up/down counter)
// Stall if fewer than dispatch_width entries available
wire rob_stall = (rob_count > (ROB_SIZE - DISPATCH_WIDTH));计数器rob_count每周期的更新规则:分发条指令时加,提交条指令时减。净更新量为,需要一个加减计数器。计数器的位宽为位(256项ROB需要8位计数器)。比较器将计数器与阈值比较,只需8位比较器。
(2)IQ满检测:IQ的空闲表项管理比ROB更复杂,因为IQ的释放不是按序的——指令从IQ发射后释放表项,而发射顺序是乱序的。IQ空闲表项通常用空闲位向量(free bitvector)管理:
// IQ availability using free bitvector
// iq_free_vec: bit vector, 1 = entry is free
// Count number of free entries using population count
wire [$clog2(IQ_SIZE):0] iq_free_count;
assign iq_free_count = $countones(iq_free_vec);
wire iq_stall = (iq_free_count < DISPATCH_WIDTH);人口计数(population count)电路的延迟与IQ大小的对数成正比。对于96项IQ的7位人口计数,延迟约为个FO4。这在时序上可能成为瓶颈。
优化方案:不使用精确的人口计数,而是维护一个近似的计数器(每次分配减、每次发射加),容忍偶尔的不精确导致的多余暂停(保守策略)或少量过度分发(需要额外的溢出保护)。
(3)LSQ满检测:LSQ分为Load Queue(LQ)和Store Queue(SQ),分别独立检测。检测逻辑类似ROB,使用计数器:
// LSQ availability check
// Need to count loads and stores in current dispatch bundle
wire [$clog2(DISPATCH_WIDTH):0] num_loads, num_stores;
// Count loads in the dispatch bundle
always_comb begin
num_loads = 0;
num_stores = 0;
for (int i = 0; i < DISPATCH_WIDTH; i++) begin
if (is_load[i]) num_loads = num_loads + 1;
if (is_store[i]) num_stores = num_stores + 1;
end
end
wire lq_stall = (lq_free_count < num_loads);
wire sq_stall = (sq_free_count < num_stores);注意这里的比较使用了精确的load/store计数(而非固定阈值),因为一个6条指令的bundle中load和store的数量变化很大(0到6条),使用固定的dispatch_width作为阈值会导致过多的不必要暂停。这个精确计数需要一个6输入的人口计数电路(仅3位输出),延迟很小(约4个FO4)。
(4)空闲物理寄存器不足:空闲列表的可用寄存器检测与LSQ类似,需要计算当前bundle中需要目的寄存器的指令数:
// Free list availability check
wire [$clog2(DISPATCH_WIDTH):0] num_dests;
always_comb begin
num_dests = 0;
for (int i = 0; i < DISPATCH_WIDTH; i++) begin
if (rd_valid[i]) num_dests = num_dests + 1;
end
end
wire freelist_stall = (freelist_count < num_dests);(5)Checkpoint槽不足:只需检测当前bundle中是否有分支指令,以及是否有空闲的Checkpoint槽:
// Checkpoint availability check
wire has_branch = |is_branch; // Any branch in bundle?
wire ckpt_stall = has_branch && (ckpt_free_count == 0);暂停信号的合成与优化
所有暂停条件通过OR门合成最终的暂停信号:
这6个暂停条件的关键路径各不相同。最长的通常是IQ满检测(需要人口计数),最短的是Checkpoint槽检测(仅需1个比较器)。整个暂停信号的延迟取决于最慢的那条路径。
| 暂停条件 | 延迟(FO4) | 关键路径 |
|---|---|---|
| ROB满 | 6 | 计数器比较 |
| IQ满 | 16 | 人口计数 + 比较 |
| LQ满 | 10 | bundle内load计数 + 比较 |
| SQ满 | 10 | bundle内store计数 + 比较 |
| 空闲列表空 | 10 | bundle内dest计数 + 比较 |
| Checkpoint槽空 | 4 | 分支检测 + 比较 |
| 合成OR | 1 | 6输入OR |
| 总延迟 | 17 | IQ满路径为关键路径 |
各暂停条件的延迟估算
表表 26.11显示IQ满检测的人口计数是暂停信号生成的关键路径。在7nm工艺下,17个FO4 5168 ps,对于5 GHz处理器(200 ps周期)来说占用了约25%34%的周期时间。这也是为什么许多设计使用提前一拍预测式暂停来替代精确检测。
设计提示
分发暂停信号的生成时序非常关键。暂停信号需要在当前周期的前半段就确定下来,以便在后半段阻止流水线寄存器的更新。如果暂停信号生成过晚,流水线可能已经将指令写入了满的队列中,导致数据覆盖或状态不一致。
一种常见的优化是提前一拍生成暂停信号:不是在当前周期检查"资源是否够用",而是在上一个周期预测"下一个周期资源是否够用"。具体做法是维护每个队列的空闲计数器,每次分发时减少、每次提交/发射时增加,并检查计数器是否将在下一周期降到分发宽度以下。这种预测式暂停可以显著放松时序,代价是偶尔会产生不必要的暂停(当资源恰好在边界时)。
分发宽度的选择
分发宽度(dispatch width)是指每个周期最多能够分发多少条指令。在理想情况下,分发宽度应该与解码/重命名的宽度相同,以避免分发成为流水线的瓶颈。
分发宽度 = 解码宽度:这是最常见的设计选择。6发射处理器的解码宽度为6,分发宽度也为6。这意味着ROB、IQ、LSQ都需要每周期能够接受6条指令的写入——需要6个写端口,面积开销显著。
分发宽度 解码宽度:如果ROB或IQ的写端口过于昂贵,可以选择较窄的分发宽度,通过在重命名和分发之间加入一个缓冲区(dispatch buffer/skid buffer)来解耦。例如,解码宽度为6但分发宽度为4,中间用一个8项的FIFO缓冲。在稳态下,如果前端平均每周期产生4条有效指令(由于分支、cache miss等原因),4宽的分发不会成为瓶颈。但在突发高吞吐段,缓冲区会被填满,前端需要暂停。
分发宽度 解码宽度:这种情况比较少见,但在某些微架构中,为了在暂停恢复后快速清空分发缓冲区中积压的指令,分发宽度可以暂时大于稳态的解码宽度。
分发宽度的选择本质上是一个面积-性能的权衡。更宽的分发需要更多的写端口(影响ROB、IQ、LSQ的面积和时序),但提供了更高的指令吞吐。在现代处理器中,分发宽度通常等于解码宽度,范围从4(中等性能核心)到8(高性能核心)不等。
| 处理器 | 解码宽度 | 分发宽度 | ROB容量 | IQ容量 |
|---|---|---|---|---|
| ARM Cortex-A77 | 4 | 4 | 160 | 120 |
| AMD Zen 4 | 6 | 6 | 320 | 424 |
| Intel Golden Cove | 6 | 6 | 512 | 97+48 |
| Apple M3 Firestorm | 8 | 8 | 600 | 160 |
典型处理器的分发宽度配置
表表 26.12展示了几款典型处理器的分发宽度配置。可以看到,所有这些处理器的分发宽度都等于解码宽度,且随着分发宽度的增加,ROB和IQ的容量也相应增大——更宽的前端需要更大的后端缓冲区来容纳更多的in-flight指令。
分发宽度还受到后端结构端口数量的限制。一个-wide的分发需要ROB、IQ各具备个写端口。多端口SRAM的面积大约按端口数的1.52次方增长。因此,从4-wide到8-wide分发,ROB的面积可能增长34倍。这是超宽发射处理器设计中面积增长最快的部分之一。
部分分发策略
当后端某个资源不足以容纳整个分发bundle时,有两种处理策略:
(1)全部暂停(all-or-nothing dispatch):如果资源不足以分发全部条指令,则一条也不分发,整个前端暂停。这种策略实现最简单——暂停信号直接冻结所有流水线寄存器。但缺点是资源利用率低:即使ROB还剩5个空位、但当前bundle有6条指令,也无法分发任何一条。
(2)部分分发(partial dispatch):分发bundle中的前条指令(),将无法分发的后条指令保留在分发缓冲区中,下一周期继续分发。这种策略提高了资源利用率,但增加了控制逻辑的复杂度。
部分分发的实现需要额外的硬件支持:
分发缓冲区(dispatch buffer/skid buffer):一个项的FIFO,保存未能分发的指令。下一周期优先分发缓冲区中的指令,然后才接受来自重命名阶段的新指令。
部分写使能:ROB、IQ、LSQ的写端口需要支持选择性写入——只有前条指令的写使能为有效。这需要为每个写端口增加一个使能门。
空闲列表的部分回收:如果在重命名阶段已经为6条指令分配了物理寄存器,但只分发了4条,则后2条指令占用的物理寄存器需要回收。这增加了空闲列表管理的复杂度。
部分分发策略在Apple的A系列/M系列处理器中被广泛使用,其8发射的宽前端需要部分分发来避免在后端资源暂时不足时完全浪费前端带宽。大多数其他商业处理器(如Intel Core、AMD Zen系列)采用全部暂停策略,通过足够大的后端缓冲区来降低暂停的频率。
重命名与分发的流水线拆分
在高频处理器设计中,重命名和分发的全部操作通常无法在单个流水线级中完成。现代处理器将重命名/分发拆分为23级流水线,每级负责不同的子操作。
| 流水线级 | 操作 | 时序估算(FO4) |
|---|---|---|
| R1(重命名1) | RAT读取 + 比较器启动 + 空闲列表读取 | 12 |
| R2(重命名2) | MUX选择 + RAT写入 + WAW仲裁 | 10 |
| D1(分发) | 资源检查 + ROB/IQ/LSQ写入 | 14 |
典型的重命名/分发流水线拆分方案
表表 26.13展示了一个典型的3级拆分方案。R1级完成RAT的并行读取和比较器的计算(两者可以并行);R2级完成MUX选择和RAT写入;D1级完成资源检查和后端结构的写入。
这种拆分方案意味着从指令进入重命名到离开分发需要3个周期。这3个周期是分支预测失败惩罚的一部分——当分支预测失败被检测到时,这3级中的所有指令都需要被丢弃。如果前端取指解码需要4个周期、重命名/分发需要3个周期、后端到分支解析需要若干周期,则总的分支预测失败惩罚约为个周期。
性能分析 3 — 流水线深度与分支惩罚的权衡
更深的重命名/分发流水线允许更高的时钟频率,但也增加了分支预测失败的惩罚。以下是典型的权衡数据:
2级重命名/分发(如ARM Cortex-A77):时钟频率受限于单级约15 FO4的延迟,但分支惩罚较小(约1113周期)。
3级重命名/分发(如Intel Golden Cove):每级约1012 FO4,支持更高的时钟频率(56 GHz),但分支惩罚增加到约1518周期。
4级重命名/分发(某些POWER架构处理器):每级约810 FO4,支持极高频率,但分支惩罚可达20+周期。
选择流水线深度时需要综合考虑目标频率、分支预测器的精度和程序的分支密度。对于分支密集型工作负载(如数据库查询),较浅的流水线(2级)可能比更高频率但更深的流水线(4级)有更好的总体性能。
操作数就绪状态的确定
分发阶段除了将指令写入IQ,还需要确定每个源操作数是否已经就绪(即对应的物理寄存器中是否已经有有效的值)。这个信息直接决定了指令进入IQ后能否立即被发射。
操作数就绪的判定逻辑:
查询就绪位表(ready bit table):一张以物理寄存器编号为索引的位向量,每位表示对应物理寄存器是否已经写入了有效值。刚分配的物理寄存器的就绪位为0(尚未写入结果),指令写回时将对应的就绪位置为1。
同周期内的旁路考虑:如果一条指令的源操作数通过rename bypass获得了一个"刚分配"的物理寄存器(来自同周期的前面指令),那么这个物理寄存器的就绪位在就绪位表中尚未设置(因为前面的指令还没执行)。因此,旁路得到的物理寄存器应该被标记为"未就绪"。
特殊情况——move消除和零值生成器:如果一条指令被move消除或识别为零值生成器,其目的物理寄存器的就绪位应该立即设为1(因为不需要执行即可获得结果)。后续依赖这个物理寄存器的指令在进入IQ时就可以被标记为操作数就绪。
// Operand ready determination at dispatch
always_comb begin
for (int i = 0; i < DISPATCH_WIDTH; i++) begin
for (int j = 0; j < 2; j++) begin
if (final_prs[i][j] == PHYS_REG_ZERO) begin
// x0 is always ready
src_ready[i][j] = 1'b1;
end else if (bypassed[i][j]) begin
// Bypassed from same-cycle instruction: check if
// that instruction is a move-eliminated or zero-idiom
src_ready[i][j] = eliminated[bypass_src[i][j]];
end else begin
// Normal case: check ready bit table
src_ready[i][j] = ready_table[final_prs[i][j]];
end
end
end
end就绪位表的读取需要个读端口(每条指令2个源操作数)。对于6发射处理器,这是12个读端口。但由于就绪位表很小(128个1位表项),即使12端口的面积也非常小(约100200 m),不构成面积瓶颈。
向量寄存器与特殊寄存器的重命名
前面的讨论主要针对标量整数和浮点寄存器的重命名。在现代处理器中,还有多种特殊类型的寄存器需要参与重命名,它们各自带来了独特的挑战。
宽向量寄存器的重命名挑战
现代处理器的向量扩展——x86的AVX-512(512位ZMM寄存器)、ARM的SVE/SVE2(最大2048位向量)、RISC-V的V扩展(可配置宽度VLEN)——引入了极宽的向量寄存器。重命名这些宽寄存器面临两个核心挑战:PRF面积和寄存器分组。
PRF面积爆炸:向量PRF的面积与寄存器宽度成正比。一个128个物理寄存器的标量PRF(64位宽)仅需位 1 KB。但如果将其替换为512位的向量PRF,容量变为位 KB——增大了8倍。对于2048位的SVE向量,面积则增大32倍,达到32 KB。更严重的是,PRF需要大量的读写端口(典型的6发射处理器需要12个读端口和6个写端口),多端口SRAM的面积随端口数的平方增长,使得宽向量PRF的面积成为整个处理器核心中最大的单一结构之一。
| 向量扩展 | 寄存器宽度 | PRF容量(128项) | 相对标量PRF |
|---|---|---|---|
| 标量整数/浮点 | 64位 | 1 KB | 1 |
| SSE / NEON | 128位 | 2 KB | 2 |
| AVX2 | 256位 | 4 KB | 4 |
| AVX-512 | 512位 | 8 KB | 8 |
| SVE (512位) | 512位 | 8 KB | 8 |
| SVE (2048位) | 2048位 | 32 KB | 32 |
不同向量宽度下128个物理寄存器的PRF容量需求
表表 26.14直观展示了PRF面积随向量宽度增长的趋势。需要强调的是,这里只计算了存储容量——实际的面积增长还要叠加多端口SRAM的端口开销,使得真实面积增长比容量增长更加剧烈。
缓解PRF面积的策略:
将宽向量拆分为多个窄物理寄存器:一个512位的ZMM寄存器可以拆分为4个128位的物理寄存器。这种方式复用了较窄的PRF,但增加了重命名逻辑的复杂度——每个向量逻辑寄存器需要映射到多个物理寄存器,RAT的每个表项需要存储多个物理寄存器编号。这种方案在Intel的AVX-512实现中被广泛采用:微架构内部将每个512位ZMM操作拆分为两个256位的微操作,复用256位的PRF。
分层PRF:类似cache的分层思想,频繁使用的向量物理寄存器保存在小而快的L0 PRF中,不频繁的溢出到大而慢的L1 PRF(可能是普通SRAM甚至eDRAM)。读取时先查L0,miss后访问L1。这种设计可以显著减少高速PRF的面积需求。
零寄存器优化:在许多向量程序中,大量的向量物理寄存器被初始化为零向量。如果能够检测到这种情况,可以让多个逻辑寄存器共享一个物理的"零寄存器",避免分配独立的物理寄存器。这种技术称为零值优化(zero-value optimization),可以减少10%20%的向量PRF占用率。
RISC-V V扩展的LMUL分组:RISC-V V扩展引入了LMUL(Length Multiplier)概念,允许将个向量寄存器分组为一个操作数。例如,当LMUL=4时,一条向量加法指令vadd.vv v8, v12, v16实际上操作4个连续的向量寄存器组:(v8-v11) = (v12-v15) + (v16-v19)。
LMUL分组对重命名带来了额外的挑战:
一条指令的源和目的可能涉及多个连续的逻辑寄存器。当LMUL=4时,一条指令最多涉及个逻辑寄存器(2个源组 + 1个目的组),而标量指令通常只涉及23个。
物理寄存器的分配需要保证连续性(如果PRF采用物理连续的分组方案)或者需要更复杂的间接映射表。
LMUL值是动态可变的(由
vtypeCSR控制),这意味着同一个逻辑寄存器编号在不同的LMUL设置下可能对应不同数量的物理寄存器,增加了RAT管理的复杂度。
设计提示
对于RISC-V V扩展的LMUL分组重命名,一种实用的设计方案是:以单个向量寄存器为粒度进行重命名(不考虑LMUL分组),将LMUL=4的指令在重命名阶段拆分为4条微操作,每条微操作重命名一个向量寄存器。这种方式复用了标量重命名的逻辑,但可能降低LMUL>1时的吞吐率。另一种方案是让RAT支持变长映射——当LMUL=4时,一个RAT表项映射到4个连续的物理寄存器——但这显著增加了硬件复杂度。
LMUL分组映射的三种方案
RISC-V V扩展的LMUL分组对重命名的核心挑战是:一条向量指令可能同时操作1、2、4或8个连续的逻辑向量寄存器。重命名逻辑必须能够处理这种可变粒度。下面详细分析三种实现方案。
方案一:微操作拆分(op cracking)
将一条LMUL=的向量指令在重命名阶段拆分为条微操作,每条微操作重命名一个向量寄存器。例如LMUL=4的vadd.vv v8, v12, v16被拆分为:
uop0: vadd v8, v12, v16 # 重命名 v8, v12, v16
uop1: vadd v9, v13, v17 # 重命名 v9, v13, v17
uop2: vadd v10, v14, v18 # 重命名 v10, v14, v18
uop3: vadd v11, v15, v19 # 重命名 v11, v15, v19| LMUL | 微操作数量 | 重命名周期(6-wide) | 吞吐率下降 |
|---|---|---|---|
| 1 | 1 | 1条/周期 | 无 |
| 2 | 2 | 2条占2个槽 | 中等 |
| 4 | 4 | 4条占4个槽 | 显著 |
| 8 | 8 | 需要2个周期 | 严重 |
微操作拆分方案的开销分析
优点:重命名逻辑与标量完全一致,无需修改RAT结构。缺点:LMUL=8时单条指令需要8个重命名槽(超过6发射宽度),必须跨周期拆分,严重降低吞吐率。同时,拆分后的微操作占用更多的ROB表项和IQ表项。
方案二:分组RAT(group RAT)
让RAT的每个表项支持变长映射。当LMUL=1时,一个表项映射到1个物理寄存器;当LMUL=4时,一个表项映射到4个连续的物理寄存器。这要求物理寄存器分配时保证连续性。
这种方案的核心难点在于空闲列表的管理。分配连续的物理寄存器需要在空闲列表中搜索个连续的空闲槽——这本质上是一个内存分配问题(类似于物理内存的buddy分配)。对于LMUL=8,需要找到8个对齐到8边界的连续物理寄存器,可能产生外部碎片——虽然空闲寄存器总数足够,但找不到满足对齐要求的连续块。
方案三:间接映射表(indirection table)
引入一层间接映射。RAT的每个表项不直接存储物理寄存器编号,而是存储一个"向量寄存器组ID"。再用另一张表将组ID映射到实际的物理寄存器编号。这种二级映射可以消除连续性约束——同一个组中的物理寄存器不需要物理连续。
RAT:逻辑向量寄存器 组ID(例如6位)
组映射表:组ID (, , ..., ),其中由LMUL确定
物理PRF:按单个物理寄存器编号索引
这种方案的灵活性最高,但增加了一级间接访问的延迟(额外的表查找 810 FO4),且组映射表本身需要多读写端口。
| 特性 | 微操作拆分 | 分组RAT | 间接映射表 |
|---|---|---|---|
| RAT修改 | 无 | 表项宽度可变 | 增加一级间接表 |
| 空闲列表 | 不变 | 需要连续分配 | 不变 |
| 重命名延迟 | 不变 | 不变 | +810 FO4 |
| 吞吐率(LMUL1) | 下降倍 | 不变 | 不变 |
| 碎片问题 | 无 | 外部碎片 | 无 |
| 实现复杂度 | 最低 | 中等 | 最高 |
三种LMUL重命名方案的对比
大多数RISC-V向量处理器的早期实现(如SiFive X280、XuanTie C910)采用方案一(微操作拆分),因为它最容易实现且复用现有的标量重命名逻辑。方案二适合追求高向量吞吐率的高端处理器。方案三目前更多出现在学术研究中。
向量PRF的分库设计
无论采用哪种LMUL重命名方案,向量PRF的面积和端口压力都是核心挑战。一种广泛使用的缓解策略是将向量PRF分为多个bank(库),每个bank独立拥有读写端口。
设向量宽度VLEN = 512位,将其分为4个bank,每个bank宽128位。一条LMUL=1的向量指令需要同时读取所有4个bank的同一行;LMUL=4的指令跨4个逻辑寄存器,读取4个bank的4行。
| 配置 | 每bank宽度 | 每bank端口 | 估算面积 |
|---|---|---|---|
| 1个bank, 12R6W | 512位 | 12R6W | 2.5 mm |
| 4个bank, 12R6W各 | 128位 | 12R6W | 0.8 mm |
| 4个bank, 6R3W各 | 128位 | 6R3W | 0.3 mm |
向量PRF分库设计的面积对比(128个物理寄存器、VLEN=512位)
分库后面积大幅降低的原因有二:(1)每个bank的位宽降低倍,面积相应降低倍;(2)如果不同bank的端口可以独立服务不同的指令请求,则每个bank的端口数可以少于总的端口需求——通过bank间的时间复用或仲裁来服务冲突的请求。
分库的代价是bank冲突(bank conflict):当多条指令同时访问同一个bank时,需要仲裁,可能引入额外的延迟。在向量代码中,如果操作数的物理寄存器编号恰好映射到同一个bank,就会发生冲突。空闲列表的分配策略可以通过轮转(round-robin)分配不同bank的物理寄存器来减少冲突概率。
标志寄存器的重命名
x86架构的EFLAGS寄存器是标志寄存器重命名最具挑战性的案例。EFLAGS包含6个算术标志位(CF、PF、AF、ZF、SF、OF),大多数算术指令会修改其中的部分或全部标志位。问题在于:不同指令修改的标志位子集不同,而条件跳转指令通常只测试其中几个标志位。
部分更新问题:考虑以下x86指令序列:
ADD EAX, EBX // 修改 CF, PF, AF, ZF, SF, OF
SHL ECX, 1 // 只修改 CF, OF; PF, AF, ZF, SF 不变
JZ target // 测试 ZFSHL指令只修改CF和OF,不影响ZF。JZ需要读取ZF的值,这个值来自ADD指令。如果将整个EFLAGS作为一个整体进行重命名,那么SHL将EFLAGS映射到一个新的物理寄存器时,需要先将旧的EFLAGS值读出、修改其中的CF和OF、再写入新的物理寄存器——这引入了对旧EFLAGS的一个人为的RAW依赖,即使SHL本身并不依赖旧的标志值。
拆分重命名:为了解决部分更新问题,一些处理器将EFLAGS拆分为多个独立的"标志组",每个组独立参与重命名:
组1:CF(进位标志)——由移位、旋转、加法等指令修改。
组2:ZF、SF、PF、OF(算术标志)——由大多数算术指令修改。
组3:AF(辅助进位标志)——主要由BCD运算使用,现代代码中极少涉及。
每个标志组有独立的RAT条目和物理寄存器。这样,SHL只需要重命名组1(CF),不影响组2(ZF所在的组)。JZ读取组2的映射,直接获得ADD的ZF结果,不需要等待SHL。
拆分重命名消除了不同标志组之间的伪依赖,但引入了额外的RAT表项和物理寄存器需求。从面积角度看,标志位的物理寄存器很小(每个只有14位),因此PRF的额外开销很小。RAT的额外表项也仅有23个。总体而言,拆分重命名是处理标志寄存器的高效方案。
标志合并问题:拆分重命名引入了一个新问题——当一条指令需要读取完整的EFLAGS(如PUSHF、LAHF)时,需要将多个独立重命名的标志组合并为一个完整的值。这个合并操作需要额外的微操作或特殊的硬件路径,可能引入额外的延迟。在现代x86处理器中,这类指令非常少见,因此合并操作的性能影响很小。
标志拆分重命名的时序示例
通过一个具体的x86指令序列来展示拆分重命名如何消除伪依赖:
ADD EAX, EBX // I0: 写CF,PF,AF,ZF,SF,OF -> 组1(CF):P60, 组2(PZSFO):P61, 组3(AF):P62
SHL ECX, 1 // I1: 只写CF,OF -> 组1:P63 (组2和组3不变)
TEST EDX, EDX // I2: 写PF,ZF,SF,OF(不写CF) -> 组2:P64 (组1和组3不变)
JZ target // I3: 只读ZF -> 读组2:P64 (来自I2, 不依赖I1!)
ADC EAX, ESI // I4: 读CF -> 读组1:P63 (来自I1)如果不拆分重命名(将EFLAGS作为整体),I1写入整个EFLAGS会建立I0I1的RAW依赖(I1需要读旧的PF,ZF,SF值来合并)。I2同样需要读I1写的CF值来合并。这形成了一条I0I1I2I3的长依赖链。
拆分重命名后,I1只写组1(CF),不影响组2(ZF所在组)。I2只写组2,不影响组1。因此I3(读ZF)和I4(读CF)可以独立调度,不存在串行依赖。在4-wide的乱序处理器中,I3和I4甚至可以在I1和I2完成之前就被发射——只要各自依赖的标志组就绪即可。
标志合并的微操作实现:当遇到需要读取完整EFLAGS的指令(如PUSHF)时,处理器在重命名阶段插入一条"标志合并"微操作。这条微操作读取组1、组2、组3三个物理寄存器的值,将它们拼接为一个完整的EFLAGS值,写入一个新的物理寄存器。后续的PUSHF指令从这个新物理寄存器读取完整标志值。
标志合并微操作的执行延迟约1个周期(仅需简单的位拼接,无算术运算),但它增加了一条额外的微操作、占用一个ROB表项和一个IQ表项。由于PUSHF和LAHF在现代代码中极为罕见(出现频率0.01%),这个开销完全可以忽略。
案例研究 5 — RISC-V与x86标志寄存器对比
RISC-V的ISA设计有意避免了x86 EFLAGS的部分更新问题。RISC-V没有集中的标志寄存器——条件分支指令(如beq、blt)直接比较两个通用寄存器的值,不依赖任何标志位。这意味着RISC-V处理器在重命名阶段完全不需要处理标志寄存器的重命名,简化了硬件设计。
相比之下,ARM的AArch64架构保留了NZCV(Negative/Zero/Carry/Overflow)标志,但其设计比x86更为规整:大多数指令不修改标志(只有带S后缀的变种才修改),且NZCV总是作为整体被修改(不存在部分更新)。这使得ARM处理器可以将NZCV作为一个整体进行重命名,不需要拆分。
这是RISC架构在微架构实现上的一个显著优势——简洁的ISA设计直接减少了重命名硬件的复杂度和面积。
谓词寄存器的重命名
谓词寄存器(predicate register)或掩码寄存器(mask register)用于控制向量操作中每个元素是否参与计算。ARM SVE/SVE2使用16个谓词寄存器(P0P15),RISC-V V扩展使用v0寄存器作为掩码,x86 AVX-512使用8个掩码寄存器(k0k7)。
谓词寄存器的重命名与标量整数寄存器的重命名在原理上完全相同——为每个逻辑谓词寄存器维护一个RAT条目,将其映射到一个物理谓词寄存器。不同之处在于以下几点:
位宽较小:谓词寄存器的位宽与向量中的元素数量相关。对于256位的向量和32位的元素,谓词寄存器只有8位宽。即使对于SVE的最大2048位向量和8位元素,谓词寄存器也只有256位。因此,谓词PRF的面积远小于向量PRF。
独立的PRF:谓词寄存器通常使用独立的物理寄存器文件,与整数/浮点/向量PRF分离。这是因为谓词寄存器的位宽、读写模式和使用频率都与数据寄存器不同。
与向量指令的耦合:谓词寄存器通常与向量数据寄存器同时使用——一条向量指令可能同时读取两个向量源寄存器和一个谓词寄存器。这意味着向量重命名逻辑的RAT读端口需要额外增加用于谓词寄存器的端口。
硬件描述 4 — 谓词寄存器的重命名端口需求
考虑一个支持ARM SVE的4发射处理器。每条向量指令可能读取:2个向量源寄存器 + 1个谓词寄存器(掩码)+ 1个可选的merging谓词。同时写1个向量目的寄存器 + 可能1个谓词目的寄存器(比较指令的结果)。
对于4条同时重命名的向量指令:
向量RAT需要:个读端口 + 个写端口
谓词RAT需要:个读端口(掩码+merging)+ 个写端口
虽然谓词RAT的表项较少(ARM SVE只有16个逻辑谓词寄存器),但读写端口的数量与向量RAT相同。因此,谓词重命名的端口需求不容忽视。
一种优化方案是共享端口:由于不是每条指令都使用谓词寄存器,可以在多条指令之间复用谓词RAT的读写端口。这需要额外的仲裁逻辑,但可以减少谓词RAT的端口数量,降低面积。
RISC-V V扩展的掩码重命名特殊性:RISC-V V扩展只使用v0作为掩码寄存器,不像ARM SVE有16个独立的谓词寄存器。这简化了掩码重命名——只需要为v0维护一个映射。但它也引入了一个新的瓶颈:所有需要掩码的向量指令都读取v0,如果多条连续的向量指令使用不同的掩码值,则必须反复修改v0,形成通过v0的RAW依赖链。
考虑以下RISC-V V扩展代码:
vmseq.vi v0, v8, 0 # 生成掩码: v8[i]==0
vadd.vv v4, v4, v12, v0.t # 使用掩码 v0
vmseq.vi v0, v9, 1 # 生成新掩码: v9[i]==1 (RAW on v0!)
vsub.vv v5, v5, v13, v0.t # 使用新掩码 v0第三条指令vmseq写v0,第四条指令vsub读v0——这是一条RAW依赖。如果ARM SVE代码使用不同的谓词寄存器(如p1和p2),这两个掩码生成操作就可以并行,不存在依赖。RISC-V V扩展的单一v0掩码在这种场景下成为性能瓶颈。
设计权衡 3 — 谓词寄存器数量的权衡
ARM SVE的16个谓词寄存器 vs RISC-V V扩展的单一v0掩码体现了一个经典的ISA设计权衡:
更多谓词寄存器(ARM SVE):编译器可以将不同的掩码值同时保存在不同的谓词寄存器中,避免反复写
v0的瓶颈。但这增加了谓词RAT和谓词PRF的面积,也增加了指令编码中谓词寄存器字段的位数。单一掩码寄存器(RISC-V V):硬件极其简化,重命名只需跟踪一个映射。但在需要频繁切换掩码的代码中,
v0成为性能瓶颈。编译器需要更努力地调度指令以减少v0的写-读冲突。x86 AVX-512的8个掩码寄存器(k0k7)是一个中间方案,在硬件复杂度和编程灵活性之间取得了平衡。
| 寄存器类型 | 逻辑数量 | 典型位宽 | PRF压力 | 主要挑战 |
|---|---|---|---|---|
| 整数 | 32 | 64位 | 低 | 基本RAW/WAW处理 |
| 浮点 | 32 | 64位 | 低 | 与整数PRF统一或分离的选择 |
| SIMD/向量 | 32 | 1282048位 | 高 | PRF面积爆炸、端口需求 |
| 标志 | 13组 | 18位 | 极低 | 部分更新引起的伪依赖 |
| 谓词/掩码 | 116 | 8256位 | 低 | 与向量指令的端口耦合 |
各类寄存器重命名的特性对比
表表 26.18总结了各类寄存器重命名的关键特性。可以看到,向量寄存器是PRF面积压力最大的类型,而标志寄存器虽然面积很小,但其部分更新的语义给微架构设计带来了额外的复杂性。
总结而言,特殊寄存器的重命名遵循与标量整数寄存器相同的基本原则——通过物理寄存器文件和RAT消除假相关性——但每种寄存器类型都有其独特的约束和优化空间。处理器设计者需要为每种寄存器类型选择合适的PRF容量、RAT结构和端口配置,在面积、功耗和性能之间取得最佳平衡。
重命名的高级优化技术
本节讨论几种在现代高性能处理器中使用的重命名阶段高级优化技术。
分簇重命名架构
当发射宽度超过68时,集中式的重命名逻辑(单一RAT、单一空闲列表、集中式旁路网络)的面积和时序都变得难以承受。一种解决方案是分簇重命名(clustered renaming),将宽发射分为多个窄的"簇"(cluster),每个簇独立进行重命名,簇间通过少量的同步信号保持一致性。
例如,一个8发射处理器可以分为两个4-wide的簇:簇A负责I0I3的重命名,簇B负责I4I7的重命名。每个簇有独立的RAT副本和旁路网络。
分簇重命名的关键挑战在于簇间RAW依赖:如果I3(簇A)写x5,I4(簇B)读x5,簇B需要知道簇A为x5分配的新物理寄存器。这要求簇A在当前周期内将I0I3的目的寄存器映射传递给簇B,而簇B在接收到这些信息后更新其RAT副本或旁路结果。
簇间同步的两种方式:
前瞻式同步:簇A和簇B并行工作,但簇B额外接收簇A的目的寄存器编号和新物理寄存器编号,在自己的旁路网络中增加对簇A指令的检查。这等效于将旁路网络从4-wide扩展到8-wide,但比较器只增加了簇间的部分(4条2个源4个目的 = 32个比较器)。延迟增加约12个FO4(额外的比较和MUX级数)。
流水线式同步:簇A先完成重命名,下一周期簇A的映射更新传播到簇B。簇B在下一个周期使用更新后的RAT。这种方式不增加单周期的延迟,但引入了1周期的流水线气泡——如果I3和I4之间有RAW依赖,I4的重命名会被延迟1个周期。
在实践中,簇间RAW依赖在连续的4条指令之间出现的概率约为15%25%。如果采用流水线式同步,这部分依赖会引入气泡,降低有效IPC约3%5%。前瞻式同步避免了这个问题,但增加了时序压力。大多数超宽处理器采用前瞻式同步。
重命名的功耗优化
重命名阶段的功耗主要来自三个方面:RAT的读写操作、比较器矩阵的翻转活动、空闲列表的读取。在移动和嵌入式处理器中,这些操作的功耗需要仔细优化。
RAT的门控时钟优化
SRAM-RAT每周期被个源操作数读取和个目的寄存器写入。但并非每条指令都有2个有效的源操作数——立即数指令(如addi)只有1个源寄存器,无操作数指令(如nop、fence)没有源寄存器。通过检测有效的读请求数量,可以对未使用的RAT读端口施加门控时钟(clock gating),避免不必要的SRAM活动。
// Clock gating for RAT read ports
// Only activate read port if source register is valid
always_comb begin
for (int i = 0; i < DISPATCH_WIDTH; i++) begin
rat_rd_en[i][0] = rs1_valid[i]; // Gate if no rs1
rat_rd_en[i][1] = rs2_valid[i]; // Gate if no rs2
end
end在典型的RISC-V程序中,约30%的指令只有1个源寄存器(立即数类型),约5%没有源寄存器。因此门控时钟可以减少RAT读取的动态功耗约15%20%。
比较器矩阵的功耗门控
RAW检测的30个比较器在每个周期都翻转,即使很多比较的结果是"不匹配"。一种优化是按需激活比较器:如果第条指令没有目的寄存器(rd_valid[k] = 0),则所有以I的rd为一端的比较器都不需要工作。通过将rd_valid作为比较器的时钟门控信号,可以禁用这些不必要的比较操作。
在典型的指令混合中(约70%的指令有目的寄存器),这种优化可以减少比较器矩阵的动态功耗约20%30%。
SMT对重命名的影响
在支持同时多线程(Simultaneous Multi-Threading, SMT)的处理器中,重命名逻辑需要同时服务多个硬件线程。每个线程有独立的逻辑寄存器集(各32个),但通常共享物理寄存器文件。
RAT的线程隔离
每个硬件线程需要独立的RAT——因为不同线程的逻辑寄存器映射完全独立。对于2-SMT处理器:
方案一:独立RAT。为每个线程维护完全独立的RAT SRAM。面积翻倍(2个32项7位的RAT),但实现简单,两个线程的重命名可以完全并行。
方案二:共享RAT + 线程标签。使用一个64项的共享RAT(32项/线程),以线程ID作为地址的一部分。面积略小于独立RAT(共享了SRAM的外围逻辑),但端口争用可能成为问题。
在实际设计中,大多数SMT处理器(如Intel的Hyper-Threading)采用独立RAT方案,因为RAT的面积本身很小(几百字节),而端口争用可能导致的暂停对性能影响更大。
物理寄存器的线程分配策略
物理寄存器文件(PRF)通常由所有线程共享。但共享带来了一个公平性问题:一个线程可能消耗大量的物理寄存器(例如遇到大量cache miss导致ROB无法排空),导致另一个线程因空闲寄存器不足而暂停。
两种常见的策略:
静态分区:将物理寄存器平均分配给每个线程。2-SMT、128个物理寄存器时,每个线程分配64个。优点:保证公平性,一个线程不会饿死另一个。缺点:当只有一个线程活跃时,另一半物理寄存器被浪费。
动态共享:所有物理寄存器由线程动态竞争分配。优点:单线程时可以利用全部寄存器。缺点:需要防饿死机制——例如当一个线程的已分配寄存器数超过时,暂停该线程的重命名,优先为另一个线程分配。
Intel的Hyper-Threading实现采用动态共享策略,配合"公平信用"(fair credit)机制来防止单线程垄断所有物理寄存器。当一个线程的in-flight指令占用了超过预设阈值(通常为总量的60%70%)的物理寄存器时,该线程的重命名被暂停,直到占用率降到阈值以下。
| 资源 | 单线程 | 2-SMT |
|---|---|---|
| RAT | 1个(327位 = 224位) | 2个(共448位) |
| 空闲列表 | 1个 | 1个(共享)或2个(分区) |
| Checkpoint | 个快照 | 个快照(每线程个) |
| CRAT | 1个(224位) | 2个(共448位) |
| PRF | 128项 | 128项(共享) |
| 旁路网络 | 个比较器 | 不变(同周期内只重命名1个线程的指令) |
SMT对重命名硬件资源的影响(2-SMT、6发射、128个物理寄存器)
表表 26.19显示,2-SMT的主要额外开销在于RAT和Checkpoint的翻倍(约增加2240位存储),而PRF和旁路网络不增加。这是因为在每个周期中,6发射的重命名带宽通常只服务一个线程(或在两个线程之间轮转),不需要同时重命名两个线程的指令。
物理寄存器数量的设计空间探索
物理寄存器的数量是寄存器重命名系统最重要的设计参数之一。物理寄存器太少会导致空闲列表频繁枯竭,前端暂停;太多则浪费面积和功耗。
物理寄存器数量的下界
物理寄存器数量的理论下界为:
其中是逻辑寄存器数量(RISC-V为32),是同时处于流水线中(已重命名但未提交)的指令中使用目的寄存器的最大数量。的上界是ROB容量(因为每条in-flight指令最多占用1个物理寄存器)。
对于一个256项ROB的处理器,理论上需要个物理寄存器。但实际中并非每条指令都有目的寄存器(约70%有),因此实际需要约个物理寄存器。
物理寄存器数量与IPC的关系
物理寄存器数量对IPC的影响遵循一个收益递减的曲线。当物理寄存器严重不足时,IPC急剧下降;当物理寄存器略超过需求时,IPC接近峰值;进一步增加寄存器数量的收益趋于零。
| 物理寄存器数 | 相对IPC | 空闲列表枯竭率 | PRF面积(KB) |
|---|---|---|---|
| 64 | 0.62 | 38% | 0.5 |
| 96 | 0.81 | 12% | 0.75 |
| 128 | 0.94 | 3% | 1.0 |
| 160 | 0.98 | 0.5% | 1.25 |
| 192 | 1.00 | 0.1% | 1.5 |
| 256 | 1.00 | 0.01% | 2.0 |
不同物理寄存器数量对IPC的影响(6发射、256项ROB、SPECint模拟)
表表 26.20展示了一个典型的模拟结果。128个物理寄存器可以达到峰值IPC的94%,160个可以达到98%。大多数商业处理器选择128192个整数物理寄存器——这个范围在IPC收益和面积开销之间取得了最佳平衡。
值得注意的是,move消除和零值优化可以有效减少物理寄存器的消耗。如果这些优化能够减少15%的物理寄存器分配,则128个物理寄存器的有效利用率等价于不使用优化时的个——接近表中160个寄存器的性能水平。这就是move消除的间接收益:在不增加PRF面积的前提下,提升了有效的物理寄存器数量。
重命名阶段的完整硬件预算
最后,我们汇总一个6发射RISC-V处理器(32个整数逻辑寄存器、128个物理寄存器、8个Checkpoint槽)的重命名阶段完整硬件预算。
| 组件 | 存储量(位) | 面积(m, 7nm) | 说明 |
|---|---|---|---|
| SRAT(推测RAT) | 800 | 12R+6W端口SRAM | |
| CRAT(提交RAT) | 200 | 6R+6W端口SRAM | |
| Checkpoint缓冲区 | 2880 | 2W+1R端口SRAM | |
| 空闲列表FIFO | 300 | 1R+1W+预取缓冲 | |
| RAW比较器矩阵 | — | 210 | 30个5位比较器 |
| WAW比较器矩阵 | — | 105 | 15个5位比较器 |
| 旁路MUX网络 | — | 420 | 级联2选1 MUX链 |
| WAW仲裁逻辑 | — | 50 | OR门+AND门 |
| 就绪位表 | 200 | 12R+6W端口 | |
| 引用计数(如有) | 800 | 可选,18端口 | |
| 控制逻辑 | — | 500 | 状态机+暂停逻辑 |
| 总计(不含引用计数) | 3264 | 5665 | mm |
| 总计(含引用计数) | 3776 | 6465 | mm |
6发射RISC-V处理器重命名阶段的硬件预算汇总
表表 26.21显示,整个重命名阶段的面积约为0.006 mm,在7nm处理器核心(典型面积5 mm)中占比仅约0.1%。Checkpoint缓冲区是面积最大的单一组件(约50%),其次是SRAT(约14%)。与PRF本身(128个64位寄存器、12R6W端口,面积约0.150.3 mm)相比,重命名逻辑的面积约为PRF的2%4%——这说明重命名的面积开销在整个乱序执行引擎中是很小的一部分,其设计决策主要受时序约束而非面积约束驱动。
本章小结
本章深入讨论了超标量处理器寄存器重命名中的核心工程问题。
在RAW相关性处理方面(26.1 节节),我们分析了同一周期内6条指令的RAW检测需要30个5位比较器(共420 GE),以及旁路优先级MUX链的两种实现方式——级联MUX链(7.5 FO4延迟)和优先级编码器+MUX(6 FO4延迟)。整个RAW检测与旁路网络的面积约1260 GE,关键路径约10.5 FO4。Move消除和零值优化进一步在重命名阶段消除5%15%的指令。
在WAW相关性处理方面(26.2 节节),我们展示了WAW仲裁需要额外15个比较器和5个OR门,用于屏蔽被后续指令覆盖的RAT写入。物理寄存器的安全释放条件通过两个反例(过早释放导致推测恢复错误、过晚释放导致资源浪费)进行了严格推导。
在重命名恢复方面(26.3 节节),我们对三种方式进行了全面的量化对比。Checkpoint方式需要约2880 m面积但仅1周期恢复;WALK方式面积最小(约250 m)但恢复延迟不确定(264周期);Architecture State方式面积也很小(约243 m)但恢复延迟最大(10200+周期)。IPC影响分析表明,Checkpoint比WALK高27%、比Architecture State高81%。现代处理器普遍采用Checkpoint + CRAT的混合策略。
在分发方面(26.4 节节),我们详细分析了ROB满、IQ满、LSQ满、空闲列表空、Checkpoint槽不足等五种暂停条件的检测电路和时序(关键路径约17 FO4),讨论了部分分发与全部暂停的权衡,以及操作数就绪状态的确定逻辑。
在特殊寄存器重命名方面(26.5 节节),我们分析了向量PRF面积随宽度线性增长的问题、RISC-V V扩展LMUL分组的三种方案(微操作拆分、分组RAT、间接映射表)、标志寄存器的拆分重命名消除伪依赖、以及谓词寄存器的端口需求。
最后在高级优化(26.6 节节)中,我们讨论了分簇重命名架构如何将8-wide拆分为两个4-wide的簇以改善时序,以及功耗门控和物理寄存器数量的设计空间探索。
设计权衡 4 — 前向桥接——从重命名到发射
经过重命名阶段的处理,每条指令已经获得了唯一的物理寄存器标识,假依赖被彻底消除。但重命名只解决了"谁依赖谁"的命名问题,并未解决"何时可以执行"的调度问题。指令的源操作数可能尚未就绪——生产者指令可能仍在执行甚至仍在等待自己的操作数。指令需要一个等待的地方,直到所有源操作数就绪后才能被发射到功能单元。这个等待的地方——发射队列(Issue Queue)——是乱序执行引擎的"心脏",其容量大小直接决定了处理器能"看到"多远的未来以发掘ILP。第 27.0 章将系统讨论发射队列的结构设计。