Skip to content

重排序缓冲区与提交

ROB是超标量处理器中最大的单一结构之一——Intel Golden Cove的ROB有512项,每项约80位,总计约5 KB的SRAM。它的存在只为一个目的:让处理器能够"假装"所有指令都是按程序顺序执行的,即使内部是乱序的。考虑一个具体场景:一条DIV指令正在执行(需要20+周期),而它后面的50条独立指令已经全部执行完毕。这些指令的结果暂存在ROB中,不能写入体系结构寄存器——因为如果DIV触发除零异常,所有后续指令都必须被撤销。这种"有序假象"是精确异常、投机恢复和正确程序语义的基础。

从本书的统一视角看——处理器设计的本质是在有限的晶体管预算和功耗约束下,通过投机和并行的层层叠加来逼近指令吞吐率的理论上限——ROB是"投机"的安全网。它记录了所有投机执行的结果,使得投机失败时可以完整地回滚到正确状态。没有ROB,就没有安全的投机——处理器无法进行分支预测(第 17.0 章),无法乱序执行Load(第 36.0 章),无法在异常点之后继续取指。ROB的容量直接决定了投机窗口的大小:512-entry ROB意味着处理器可以同时追踪512条投机指令的状态,为分支预测器和内存消歧器提供了"胆大妄为"的资本。

在乱序处理器中,指令以程序顺序被取指和解码,但可以以任意顺序执行。这种执行灵活性带来了巨大的性能收益,却也引入了一个根本性的难题:如何在指令乱序执行完成后,仍然保持处理器状态的正确性?具体来说,当一条指令在执行过程中触发了异常(如页错误、除零),处理器必须能够呈现出一个精确的处理器状态(precise processor state)——所有在程序顺序中位于该异常指令之前的指令都已完成,而该异常指令及其后续指令都没有修改任何体系结构状态。这就是精确异常(precise exception)的要求。

重排序缓冲区(Reorder Buffer, ROB)正是解决这一问题的核心硬件结构。ROB由James E. Smith和Andrew R. Pleszkun于1985年首次提出,它按照程序顺序记录所有分发(dispatch)到乱序引擎中的指令,并在指令执行完成后以程序顺序逐条提交(commit/retire)到体系结构状态中。ROB就像一个"检查站"——指令可以乱序执行,但必须按序通过这个检查站才能将结果固化到体系结构中。

第 24.0 章中,我们已经从寄存器重命名的角度介绍了ROB的基本概念和它作为重命名载体的角色。在第 25.0 章的映射表设计中,也讨论了ROB索引作为推测标签的用法。本章将以ROB本身为中心,系统而深入地讨论其硬件结构设计、端口需求分析、两种状态管理方式(ROB存储结果值方式 vs PRF方式),以及提交阶段的具体实现机制。本章的内容与前面章节中关于ROB的讨论互为补充,但侧重点不同——前面章节关注的是ROB在重命名和发射中的辅助角色,而本章聚焦于ROB作为顺序提交引擎的核心功能。

ROB的基本结构

回顾第 24.0 章中的寄存器重命名流程:每条指令在重命名阶段被分配一个ROB表项索引,该索引成为指令在乱序引擎中的"身份标识"。重命名阶段产生的旧物理寄存器映射(POld)被记录在ROB中,用于提交时释放旧寄存器。在第 39.0 章中,我们将看到ROB在恢复机制中扮演的另一个关键角色——当分支预测失败或异常发生时,ROB记录的信息驱动整个恢复流程:从RAT回退到空闲列表恢复,再到发射队列和LSQ的清除。

ROB在逻辑上是一个先进先出(FIFO)的队列:指令在分发时从队列尾部进入,在提交时从队列头部离开。由于指令按程序顺序分发,ROB中指令的排列顺序天然保持了程序顺序。这种顺序性是ROB实现精确异常的基础——提交逻辑只需从队列头部开始,检查每条指令是否已经执行完毕并且没有异常,就可以按程序顺序将结果固化到体系结构状态。

ROB表项的字段

每个ROB表项(entry)对应一条分发到乱序引擎中的指令(或微操作,μ\muop)。表项中需要记录足够的信息,使得提交逻辑能够完成其工作:判断指令是否可以提交、如何更新体系结构状态、以及在发生异常时如何处理。图图 38.1展示了一个典型的ROB表项字段布局。

ROB表项的典型字段布局(PRF方式)。上排为核心控制字段,下排为辅助字段。在ROB方式下还需额外包含64位的Value字段和完整的PC字段。
ROB表项的典型字段布局(PRF方式)。上排为核心控制字段,下排为辅助字段。在ROB方式下还需额外包含64位的Value字段和完整的PC字段。

下面逐一说明各字段的含义、作用和位宽选择依据。

控制与状态标志字段

(1)有效位V(Valid,1位):标识该表项是否已被分配给一条有效指令。当指令从分发阶段进入ROB时,V位被置1;当指令提交或因流水线冲刷(flush)被作废时,V位被清0。在循环队列实现中,V位可以通过头尾指针的关系隐式表达,从而省去显式的V位——如果表项索引ii满足headi<tail\mathrm{head} \leq i < \mathrm{tail}(考虑回绕),则ii有效。然而,显式的V位在调试和某些特殊操作(如选择性冲刷)中仍有价值,许多实现会保留它。

(2)完成位Done(Completed,1位):标识该指令是否已经执行完毕。当指令被分发到ROB时,Done位初始化为0;当指令在执行单元中完成计算后,执行单元通过完成信号(completion signal)将对应ROB表项的Done位置1。提交逻辑检查ROB头部的Done位来决定是否可以提交该指令。Done位是ROB中被写入频率最高的字段——每条完成执行的指令都需要写一次,而且写入的时机是随机的(取决于指令何时完成执行)。这一特性对Done位的硬件实现方式有重要影响,将在38.2.2 节中详细分析。

(3)异常标志Exception(1位):标识该指令在执行过程中是否触发了任何异常。将异常标志作为独立的1位字段,可以让提交逻辑快速判断是否需要进入异常处理路径——提交检查的关键路径只需检查Done和Exception两个位,而不需要解码完整的异常码。只有当Exception=1时,提交逻辑才需要进一步读取ExceptionCode字段以确定异常类型。

(4)异常码ExceptionCode(4位):编码异常的具体类型。4位可以区分16种异常类型,足以覆盖RISC-V的主要异常:

  • 0000:无异常

  • 0001:指令地址未对齐

  • 0010:非法指令

  • 0011:断点

  • 0100:Load地址未对齐

  • 0101:Load访问错误(页错误)

  • 0110:Store地址未对齐

  • 0111:Store访问错误(页错误)

  • 1000:环境调用(ecall)

  • 1001:整数除零(非标准扩展)

  • 1010–1111:保留/自定义

在x86处理器中,由于异常类型更多(包括各种浮点异常、段错误、通用保护错误等),异常码可能需要5–6位。

(5)分支预测错误标记BranchMispred(1位):标识该指令是一条分支指令且其预测结果已被确认为错误。这个标志在分支执行单元完成分支比较后设置。当提交逻辑在提交到这条分支时发现BranchMispred=1,会触发流水线冲刷和恢复操作。将预测错误信息记录在ROB中(而非立即触发恢复)的好处是:恢复操作可以在提交时有序进行,避免了多条分支同时检测到错误时的冲突处理问题。

指令类型与分类字段

(6)指令类型Type(6位):编码指令的类型和子类型。6位可以区分64种类型,典型的编码方案如下:

  • 位[5:4]:大类——00=整数ALU,01=浮点/SIMD,10=访存,11=控制流/系统

  • 位[3:0]:子类——例如在整数ALU大类中,0000=加法,0001=移位,0010=乘法,0011=除法等

提交逻辑需要根据指令类型执行不同的操作——例如Store指令在提交时需要将Store Queue中的数据标记为可以排出到Cache;分支指令在提交时可以释放分支预测资源;除法指令可能需要检查特殊的异常条件。更细粒度的类型编码使提交逻辑能够精确地选择操作路径。

(7)IsBranch标记Br(1位):标识该指令是否是分支指令。虽然这个信息也可以从Type字段解码得到,但将其作为独立的1位字段可以减少提交逻辑中Type解码器的关键路径延迟。提交逻辑在提交分支指令时可以执行分支预测结果的最终确认,释放对应的分支检查点资源。

(8)IsStore标记St(1位):标识该指令是否是Store指令。Store的提交涉及与Store Queue/Store Buffer的交互,逻辑与普通ALU指令不同。独立的St标记允许提交逻辑快速识别Store指令,无需解码Type字段。

(9)IsLoad标记Ld(1位):标识该指令是否是Load指令。Load的提交可能涉及内存序违规(memory order violation)的最终确认。当Load Queue中检测到内存序违规时,相关的Load在ROB中被标记为异常,在提交时触发流水线冲刷。

寄存器映射字段

(10)目的逻辑寄存器Dst(5位,RV64):记录该指令写入的体系结构寄存器编号。对于RV64的32个通用整数寄存器,需要5位编码。对于不产生寄存器结果的指令(如Store、无条件分支J),此字段可以标记为x0(寄存器0,硬件零寄存器),表示没有实际的目标寄存器。在PRF方式下,提交逻辑使用Dst字段来更新提交RAT(Committed RAT)。

注意:如果处理器同时支持整数和浮点寄存器的重命名,还需要1位标志区分目标是整数寄存器还是浮点寄存器,或者将Dst扩展到6位(高位表示寄存器类型)。

(11)物理目标寄存器PDstlog2NPRF\lceil\log_2 N_\mathrm{PRF}\rceil位):在PRF方式下,记录分配给该指令的物理寄存器编号。对于NPRF=192N_\mathrm{PRF} = 192个物理寄存器,需要log2192=8\lceil\log_2 192\rceil = 8位。提交时,该物理寄存器成为Dst逻辑寄存器的新的体系结构映射。对于没有目标寄存器的指令,PDst可以标记为无效(例如使用一个特殊的"空寄存器"编号)。

(12)旧物理寄存器POldlog2NPRF\lceil\log_2 N_\mathrm{PRF}\rceil位):记录在该指令被重命名之前,Dst逻辑寄存器映射到的旧物理寄存器编号。当指令提交时,POld指向的物理寄存器将被释放回空闲列表——因为此时已经没有任何在程序顺序中更早的指令需要该值了。这是PRF方式中物理寄存器生命周期管理的关键字段。POld字段的位宽与PDst相同(8位在上述配置中)。

分支推测管理字段

(13)分支掩码BranchMaskNckptN_\mathrm{ckpt}位,典型值8位):标识该指令依赖于哪些尚未解析的分支。BranchMask是一个位向量,其中第kk位为1表示该指令是在第kk号分支预测之后分发的,如果该分支被发现预测错误,则该指令应被丢弃。

BranchMask的位宽等于处理器支持的最大未决分支数NckptN_\mathrm{ckpt}。现代处理器通常支持8–16个并发的未决分支,因此BranchMask为8–16位。当一条分支指令在ROB中被提交后,所有还在飞的指令的BranchMask中对应该分支的位可以被清除——这条分支不再是"未决"的。

为什么BranchMask在ROB中有用?虽然BranchMask主要在发射队列中用于选择性冲刷(参见第 29.0 章),但ROB中也需要保留它,用于在分支预测错误恢复时确定需要丢弃的指令范围。如果使用检查点恢复策略,BranchMask可以帮助ROB快速定位需要冲刷的表项,而不需要从ROB尾部逐条回退。

辅助字段

(14)Store Queue索引SQ_Idxlog2NSQ\lceil\log_2 N_\mathrm{SQ}\rceil位):对于Store指令,记录其在Store Queue中的位置。提交时用于定位对应的Store Queue表项并将其标记为已提交。对于NSQ=64N_\mathrm{SQ} = 64的Store Queue,需要6位。非Store指令此字段无意义,可以与其他字段复用。

(15)Load Queue索引LQ_Idxlog2NLQ\lceil\log_2 N_\mathrm{LQ}\rceil位):类似地,记录Load指令在Load Queue中的位置。提交时用于释放Load Queue表项,并确认无内存序违规。

(16)PC偏移量PC_Offset(12位):记录指令PC相对于某个基准地址的偏移量,而不是完整的64位PC。基准地址可以是取指块的起始地址,通过ROB索引和分发位置隐式推导。12位偏移量可以覆盖4KB范围内的指令地址,对于大多数取指窗口足够使用。在发生异常时,完整的PC通过基准地址加偏移量恢复。这种压缩方式将64位的PC存储开销降低到12位,节省了大量的ROB存储空间。

(17)微操作组标记UopGroup(2–3位):如果一条ISA指令被分解为多条μ\muop(如x86中的复杂指令),需要标记哪些μ\muop属于同一条ISA指令,以确保整组μ\muop要么全部提交,要么全部取消。常用的编码方式:

  • 位[1:0]:00=独立指令(单μ\muop),01=组首,10=组中,11=组尾

  • 位[2]:可选,标识是否为指令边界(用于精确中断定位)

设计提示

在ROB方式(指令结果存储在ROB中)的设计中,ROB表项还需要额外包含一个Value字段(64位,RV64)用于存储指令的执行结果,以及一个完整PC字段(64位,用于异常处理时确定异常指令的地址)。这使得每个表项的宽度可达200位以上。而在PRF方式下,结果值存储在独立的物理寄存器文件中,ROB表项不需要Value字段,且PC可以用偏移量压缩,宽度可缩减至70–80位,显著降低了ROB的面积和功耗。这也是现代处理器普遍采用PRF方式的重要原因之一。

ROB表项位宽的定量分析

将上述所有字段的位宽汇总,可以精确计算ROB表项在两种方式下的总位宽。表表 38.1给出了详细的分析。

#字段PRF方式(位)ROB方式(位)
1Valid(V)11
2Done/Completed11
3Exception11
4ExceptionCode44
5BranchMispred11
6Type66
7IsBranch11
8IsStore11
9IsLoad11
10Dst55
11PDst8
12POld8
13BranchMask88
14SQ_Idx66
15LQ_Idx66
16PC_Offset1264 (完整PC)
17UopGroup33
18Value64
19其他(预留/对齐)34
合计76177

ROB表项各字段位宽的详细对比

PRF方式下的76位可以向上对齐到80位(10字节),便于硬件实现中的字节边界对齐。ROB方式下的177位约为PRF方式的2.3倍。对于一个512表项的ROB,两种方式的总存储量对比如下:

  • PRF方式512×80=40,960512 \times 80 = 40{,}960=5.0  KB= 5.0\;\text{KB}

  • ROB方式512×180=92,160512 \times 180 = 92{,}160=11.3  KB= 11.3\;\text{KB}

存储量的差异本身还不是最关键的因素——更关键的是端口需求的差异,这将在38.2 节中详细分析。

ROB表项的工艺实现考量

ROB表项的位宽选择还受到工艺实现的约束。在SRAM实现中,每一列(column)对应一个位,列数过多会增加字线(word line)的RC延迟。对于80位宽的PRF方式表项,一个直接的实现方式是使用一个80列的SRAM阵列。但更高效的做法是将80位拆分为多个子阵列(sub-array),例如4个20位的子阵列,每个子阵列独立驱动,然后在输出端合并。这种拆分可以缩短位线长度,减小读取延迟。

字段对齐与填充。在实际的SRAM编译器中,列数通常需要对齐到一定的粒度(如8列、16列或32列的倍数)。如果ROB表项的有效位宽不是对齐粒度的整数倍,会产生一些填充位(padding bits)的浪费。例如,76位的表项在32列对齐的SRAM中需要占用96列(3×323 \times 32),其中20位为填充,利用率约79%。在64列对齐的SRAM中占用128列,利用率仅59%。设计者需要在对齐粒度和面积利用率之间做出选择。

关键字段的独立存储。如前所述,Done位和Exception位等频繁被写入或读取的字段通常不存储在ROB主SRAM阵列中,而是使用独立的触发器阵列实现。这样做的好处包括:(a)避免频繁的随机写入(完成标记)对主SRAM的干扰;(b)Done位和Exception位需要在提交检查的关键路径上被快速读取,独立的小阵列可以提供更低的读取延迟;(c)触发器阵列的写端口灵活性远高于SRAM,可以支持大量的同时写入而不需要复杂的bank化逻辑。

循环队列实现

ROB在物理上实现为一个循环队列(circular buffer/circular queue),由两个指针管理:

  • 头指针(head pointer):指向ROB中最老的(最早分发的)尚未提交的指令。提交逻辑从头指针开始,顺序检查并提交指令。每成功提交一条指令,头指针向前推进一位。

  • 尾指针(tail pointer):指向ROB中下一个可用的空闲表项。当新指令从分发阶段进入ROB时,被写入尾指针指向的位置,然后尾指针前进。

图 38.2展示了ROB循环队列的结构和头尾指针的运动方向。

ROB循环队列的结构。Head指针指向最老的未提交指令,Tail指针指向下一个空闲位置。绿色表项为已分配的在飞指令。
ROB循环队列的结构。Head指针指向最老的未提交指令,Tail指针指向下一个空闲位置。绿色表项为已分配的在飞指令。

分发入队操作

NN条指令在同一周期被分发时,它们被依次写入tail指向的位置及其后续N1N-1个位置,然后tail向前推进NN位。在硬件中,tail的更新使用模运算:

tail(tail+N)modR\mathrm{tail} \leftarrow (\mathrm{tail} + N) \bmod R

其中RR是ROB的容量。如果RR选择为2的幂次(如256、512),模运算简化为截断高位比特,不需要除法器。

分发宽度与实际入队数量。WdW_d-wide的分发中,每周期最多分发WdW_d条指令,但实际分发数量可能少于WdW_d——例如解码阶段未能提供WdW_d条指令(因取指带宽不足或分支导致的取指块中断),或者部分指令槽位被NOP填充。某些NOP指令(如RISC-V的NOP伪指令,即ADDI x0, x0, 0)可以在分发时被识别并丢弃,不分配ROB表项,从而节省ROB容量。

然而,处理器是否选择为NOP分配ROB表项取决于设计的复杂度-收益权衡。不为NOP分配表项意味着分发逻辑需要在WdW_d条指令中选择哪些需要ROB表项、哪些不需要,并且tail的递增量不再固定为WdW_d,而是一个可变的值。这增加了分发逻辑的复杂度。在实际设计中,许多处理器选择为NOP也分配ROB表项(写入Done=1,提交时不执行任何操作),以简化分发控制逻辑。

提交出队操作

提交逻辑从head开始,连续检查最多WcW_c个表项(WcW_c为提交宽度)。对于每个Done=1且无异常的表项,执行提交操作并推进head:

head(head+Wc)modR\mathrm{head} \leftarrow (\mathrm{head} + W_c') \bmod R

其中WcW_c'是本周期实际成功提交的指令数(0WcWc0 \leq W_c' \leq W_c)。注意,如果连续检查中遇到Done=0的表项,提交必须停止——因为在它之后的指令即使已经完成,也不能跳过它而先提交,否则会违反程序顺序。

空/满判断逻辑

循环队列的满和空状态判断是经典的硬件设计问题。常用的方法有三种:

方法一:计数器法。维护一个计数器nn记录当前ROB中的有效表项数。每周期更新为:

nn+NdispatchWcn \leftarrow n + N_\mathrm{dispatch} - W_c'

其中NdispatchN_\mathrm{dispatch}是本周期分发的指令数,WcW_c'是本周期提交的指令数。n=0n = 0表示空,n=Rn = R表示满。当n+Ndispatch,next>Rn + N_\mathrm{dispatch,next} > R时,分发暂停。

计数器法的优势是判断逻辑简单——只需将计数器与常数RR比较。缺点是计数器的更新涉及加减法(需要一个加法器),在高频设计中可能成为关键路径。特别是,计数器的更新依赖于分发数和提交数两个变量,而这两个值在同一周期的不同阶段才确定,可能导致时序紧张。

方法二:扩展指针法。将head和tail指针各扩展一位(即使用log2R+1\lceil\log_2 R\rceil + 1位),额外的最高位用于区分满和空。当head和tail的低位相同但最高位不同时,队列满;当head和tail完全相同(包括最高位)时,队列空。这种方法在硬件中更常用,因为只需要比较逻辑而不需要算术运算。

具体地,对于R=256R = 256的ROB,head和tail各为9位(8位索引 + 1位回绕位)。空满判断的逻辑如下:

empty=(head[8:0]==tail[8:0])full=(head[7:0]==tail[7:0])(head[8]tail[8])\begin{aligned} \mathrm{empty} &= (\mathrm{head}[8:0] == \mathrm{tail}[8:0]) \\ \mathrm{full} &= (\mathrm{head}[7:0] == \mathrm{tail}[7:0]) \,\land\, (\mathrm{head}[8] \neq \mathrm{tail}[8]) \end{aligned}

但在超标量分发中,仅判断"满"是不够的——分发逻辑需要知道ROB中是否有至少WdW_d个空闲表项(而不仅仅是至少1个)。这可以通过比较空闲表项数与WdW_d来实现:

available=(tailhead)mod(2R)Wd暂停分发\mathrm{available} = (\mathrm{tail} - \mathrm{head}) \bmod (2R) \geq W_d \quad \Rightarrow \quad \text{暂停分发}

其中使用2R2R模运算处理回绕。在硬件中,这个比较用一个减法器和一个比较器实现。

方法三:空闲表项向量法。维护一个RR位的向量,每位标识对应表项是否空闲。分发时将被分配的位清零,提交时将被释放的位置1。通过popcount\mathrm{popcount}操作(计算向量中1的个数)可以得到空闲表项数。但popcount\mathrm{popcount}在大宽度下(如512位)的硬件开销较大,实际中不常用。

硬件描述 1 — ROB空满判断的时序优化

在高频处理器中,ROB空满判断位于分发阶段的关键路径上——分发逻辑必须在确认ROB有足够空间后才能发出分发信号。为了减小这条路径的延迟,一种常见的优化是使用提前计算(early computation):

在本周期结束时,预先计算下一周期ROB中将要剩余的空闲表项数,假设下一周期分发WdW_d条指令:

nnext=n+WcWdn_\mathrm{next} = n + W_c' - W_d

如果nnextRn_\mathrm{next} \geq R,则下一周期发出暂停信号。这样,暂停信号在当前周期结束时就已经确定,下一周期的分发逻辑可以直接使用,不需要等待空满判断的计算结果。

另一种方法是维护一个几近满阈值(almost-full threshold):当空闲表项数降至WdW_d以下时,提前一个周期发出暂停信号。这只需要将空闲计数与常数WdW_d比较,逻辑很简单。代价是可能在ROB还有少量空间时就暂停分发,浪费了最多Wd1W_d - 1个表项的容量,但对于256+表项的ROB,这个浪费微乎其微。

循环队列的状态变化图解

为了更直观地理解ROB循环队列的动态行为,图图 38.3展示了一个16表项ROB在连续几个周期中的状态变化过程。

ROB循环队列在4个连续周期中的状态变化。绿色为已分配表项,灰色为空闲表项,红色为头阻塞的表项。Head指针随提交前进,Tail指针随分发前进。
ROB循环队列在4个连续周期中的状态变化。绿色为已分配表项,灰色为空闲表项,红色为头阻塞的表项。Head指针随提交前进,Tail指针随分发前进。

从图图 38.3中的周期3可以看到头阻塞效应:即使ROB中有许多已完成的指令(绿色),但由于head指向的表项#2尚未完成(红色),提交无法推进,ROB的占用率不断上升。如果这种阻塞持续到ROB满(16项全部被占用),前端分发将被暂停。

多宽度分发时的tail更新电路

WdW_d-wide的分发中,tail指针的更新看似简单(加WdW_d),但实际中需要考虑变宽度分发——由于取指块边界、分支指令、解码限制等原因,实际分发的指令数NactualN_\mathrm{actual}可能少于WdW_d。tail的更新电路需要接受一个log2(Wd+1)\lceil\log_2(W_d+1)\rceil位的增量值,实现通用的模RR加法:

tailnext={tail+Nactualif tail+Nactual<Rtail+NactualRotherwise\mathrm{tail}_\mathrm{next} = \begin{cases} \mathrm{tail} + N_\mathrm{actual} & \text{if } \mathrm{tail} + N_\mathrm{actual} < R \\ \mathrm{tail} + N_\mathrm{actual} - R & \text{otherwise} \end{cases}

在硬件中,当RR是2的幂次时,上述运算退化为简单的截断加法,不需要比较器和减法器。这是选择RR为2的幂次的重要原因之一。

RR不是2的幂次时(某些设计为了节省面积可能选择非2的幂次容量,如Intel Sandy Bridge的168表项),tail更新需要一个log2R\log_2 R位的加法器和一个与RR的比较器/减法器,增加了约1–2级门延迟。

ROB的SRAM实现

ROB在物理上通常使用寄存器阵列(register file)或SRAM实现。选择哪种取决于ROB的容量和端口需求:

  • 小容量ROB\leq128个表项):可以使用触发器(flip-flop)阵列实现,每个表项的每个字段都用独立的触发器存储。这种方式面积较大,但读写速度快、端口灵活性高——可以几乎任意增加读写端口而不影响时序。

  • 大容量ROB\geq256个表项):使用编译型SRAM(compiled SRAM)或定制SRAM实现,以减小面积。SRAM的缺点是端口数量受限——每增加一个读写端口,SRAM的面积大约增加20%–30%,且位线加长导致访问延迟增加。

以Intel Golden Cove的512表项ROB为例,假设PRF方式下每表项80位,总存储量为512×80=40,960512 \times 80 = 40{,}9605  KB\approx 5\;\text{KB}。在Intel 7(10nm Enhanced SuperFin)工艺下,使用高密度SRAM的面积约0.008  mm20.008\;\text{mm}^2/KB,因此ROB存储阵列本身的面积约0.04  mm20.04\;\text{mm}^2。但加上解码逻辑、端口电路和布线开销,实际面积约为0.10.15  mm20.1\sim0.15\;\text{mm}^2

表 38.2对比了不同容量ROB的存储实现选择。

ROB容量存储方式面积(5nm)读延迟端口灵活性代表处理器
\leq64触发器阵列\sim0.01 mm2^2<<50 ps极高Gracemont(E-core)
128触发器/SRAM\sim0.02 mm2^2\sim80 psZen 3(ROB部分)
256Bank化SRAM\sim0.05 mm2^2\sim120 ps香山昆明湖
512Bank化SRAM\sim0.12 mm2^2\sim150 psGolden Cove
696Bank化SRAM\sim0.18 mm2^2\sim170 ps中偏低Apple Avalanche

不同容量ROB的存储实现方式对比

从表中可以看出,随着ROB容量增大,读延迟也随之增加。对于5 GHz时钟(周期200 ps),256表项ROB的120 ps读延迟占用了60%的时钟周期,但仍可以在单周期内完成读取。696表项ROB的170 ps读延迟则非常接近时钟周期的限制,可能需要将读取操作安排在时钟周期的更早阶段,或者通过流水线化来放松时序。

触发器阵列 vs SRAM的详细权衡

触发器(Flip-Flop)阵列使用标准的D触发器存储每一位数据。一个R×BR \times B位的触发器阵列包含R×BR \times B个D触发器,加上读写解码和数据选择逻辑。

触发器阵列的优势:

  • 任意端口数:每个触发器可以被任意数量的读信号访问(通过扇出),写入通过独立的使能和数据输入。增加端口不改变存储单元本身,只增加外围的MUX和解码逻辑。

  • 低延迟:读取延迟仅为MUX的传播延迟(\sim2–3级门延迟),不涉及位线和灵敏放大器。

  • 设计灵活性:可以在RTL中直接描述,不依赖SRAM编译器。

触发器阵列的劣势:

  • 面积大:一个标准的D触发器占用约12–16个晶体管(取决于具体的库单元),而一个6T SRAM单元只占用6个晶体管。触发器阵列的存储密度约为SRAM的1/2到1/3。

  • 功耗高:触发器在每个时钟边沿都会翻转内部状态(即使数据未改变),产生更多的动态功耗。SRAM只有在被选中访问时才消耗动态功耗。

对于现代处理器的ROB设计,典型的选择是:Done位和Exc位使用触发器阵列(利用其多端口灵活性),主控制字段使用bank化SRAM(利用其面积优势),PC偏移量等低频访问字段使用单端口SRAM(利用其最小面积)。

字段分离存储策略。在实际设计中,ROB表项的各字段并不一定存储在同一个SRAM阵列中。将不同字段存储在不同的物理阵列中,可以针对每个字段的访问模式独立优化端口数量和时序:

  • Done位阵列:独立的1位×R\times R阵列,支持多端口随机写入(完成标记)和顺序读取(提交检查)。由于只有1位宽,即使端口数很多,面积也很小。

  • Exception阵列:独立的5位×R\times R阵列,写入模式与Done类似,但频率低得多。

  • 主控制字段阵列:包含Type、Dst、PDst、POld等字段的宽SRAM阵列。分发时写入(连续访问),提交时读出(连续访问)。

  • 辅助字段阵列:BranchMask、SQ_Idx等,可能在不同流水线阶段被访问。

这种分离存储策略使得ROB在物理上不是一个统一的大表,而是由多个小型特化阵列组成的集合。每个阵列可以独立优化其端口配置和时序约束。

硬件描述 2 — 512表项ROB的物理阵列划分实例

以一个512表项、6-wide处理器的PRF方式ROB为例,下面给出一种具体的物理阵列划分方案:

阵列名称宽度深度实现方式写端口读端口
Done位阵列1位512触发器12(随机)6(连续)
Exc阵列5位512触发器12(随机)6(连续)
BrMispred阵列1位512触发器1(随机)6(连续)
主控制阵列40位5126-bank SRAM6(连续)6(连续)
BranchMask阵列8位512触发器6(连续)选择性冲刷
SQ/LQ索引阵列12位5126-bank SRAM6(连续)6(连续)
PC偏移量阵列12位5121R1W SRAM6(连续)1(异常时)

主控制阵列包含Type(6b)、Dst(5b)、PDst(8b)、POld(8b)、IsBr(1b)、IsSt(1b)、IsLd(1b)、UopGroup(3b)等字段,共约40位,分为6个bank,每bank约85行×\times40位。每个bank使用1R1W SRAM,分发时通过交叉开关路由写数据到对应bank,提交时通过交叉开关路由读数据到提交逻辑。

PC偏移量阵列只在异常处理时需要读取(频率极低),因此只配置1个读端口,使用最小面积的单端口SRAM。在异常处理时,需要1个周期来读取异常指令的PC偏移量,这不影响性能(因为异常处理本身就需要数十个周期)。

BranchMask阵列需要在选择性冲刷时被全局匹配(类似CAM操作),因此使用触发器实现以支持并行匹配。每个表项的8位BranchMask需要与冲刷信号中的分支ID进行按位与操作,确定该表项是否在错误路径上。这种操作不需要传统意义上的读端口——它是一个广播匹配操作,所有有效表项同时参与。

所有阵列的总存储量为512×(1+5+1+40+8+12+12)=512×79=40,448512 \times (1 + 5 + 1 + 40 + 8 + 12 + 12) = 512 \times 79 = 40{,}448\approx4.9 KB,与之前的估算一致。但由于不同阵列使用不同的实现方式,总面积约为0.08  mm20.08\;\text{mm}^2(SRAM部分)+0.03  mm2+ 0.03\;\text{mm}^2(触发器部分)+0.02  mm2+ 0.02\;\text{mm}^2(交叉开关和控制逻辑)0.13  mm2\approx 0.13\;\text{mm}^2

超标量分发的写入组织。NN-wide的超标量处理器中,每周期最多有NN条指令同时写入ROB。这要求ROB支持NN个同时写端口。在硬件实现中,由于这NN条指令写入的是连续NN个表项(从tail到tail+N1N-1),可以利用这种局部性来简化写电路。一种常见的做法是将ROB的存储阵列按bank(组)划分——例如,对于8-wide的分发,将ROB分为8个bank,每个bank在每周期只需要1个写端口,通过在bank之间交错排列表项(interleaving)来确保每周期的NN条写入落在不同的bank中。

ROB索引作为指令标识符

在乱序引擎中,每条在飞指令需要一个唯一的标识符(tag)来在各个硬件结构之间引用。ROB的表项索引天然具备这一功能——ROB索引kk唯一标识了当前ROB中第kk个位置的指令。这个索引在以下场景中被使用:

  • 完成标记:执行单元完成指令后,通过ROB索引定位对应表项并设置Done位。

  • 发射队列:发射队列中的指令携带其ROB索引,用于在完成时向ROB回报状态。

  • Load/Store队列:Load Queue和Store Queue中的表项记录对应指令的ROB索引,用于确定指令的程序顺序(年龄比较)。两条指令的先后顺序可以通过比较它们的ROB索引来判断——但需要注意循环队列的回绕问题。

  • 分支预测错误恢复:分支指令的ROB索引用于确定需要丢弃的指令范围——ROB中该分支之后的所有表项都应该被丢弃。

ROB索引的年龄比较

在循环队列中比较两条指令的"年龄"(即确定哪条指令在程序顺序中更早)需要特殊处理。假设两条指令的ROB索引分别为aabb,简单的a<ba < b比较在队列回绕后会给出错误结果。

考虑一个R=16R = 16的ROB,head = 12。指令A在索引14,指令B在索引2(回绕后)。虽然14>214 > 2,但A在程序顺序中更早于B(A更接近head)。

正确的年龄比较方法是利用扩展指针法中的最高位:如果使用(log2R+1)(\lceil\log_2 R\rceil + 1)位的索引,则两条指令的先后顺序可以通过比较完整索引(含回绕位)来正确判断。另一种方法是使用全局递增的序列号(sequence number),不受循环队列回绕的影响,但序列号的位宽需要足够大以避免溢出。

基于head的相对年龄计算。一种在硬件中高效的年龄比较方法是:将每个索引转换为相对于head的偏移量,然后比较偏移量。对于索引aabb

agea=(ahead)modRageb=(bhead)modR\begin{aligned} \mathrm{age}_a &= (a - \mathrm{head}) \bmod R \\ \mathrm{age}_b &= (b - \mathrm{head}) \bmod R \end{aligned}

age\mathrm{age}值越小的指令越老(越接近head)。这种方法的优点是只需要两个减法器和一个比较器,且在RR为2的幂次时模运算退化为截断。

ROB的容量与性能的关系

ROB的容量直接决定了乱序执行引擎能够容纳的在飞指令(in-flight instructions)的数量上限。ROB容量越大,处理器能够"看到"的程序窗口就越宽,能够发现和利用的指令级并行性(ILP)就越多。然而,更大的ROB也意味着更多的面积、更高的功耗以及更长的访问延迟。

Little’s Law推导ROB容量下界

ROB容量需求可以用排队论中的Little’s Law进行严格推导。Little’s Law指出:在稳态下,排队系统中的平均客户数LL等于到达率λ\lambda乘以平均逗留时间WW

L=λWL = \lambda \cdot W

将其应用于ROB:

  • LL:ROB中的平均指令数(即ROB的平均占用量)

  • λ\lambda:指令进入ROB的平均速率,等于IPC(每周期提交的指令数,在稳态下等于分发速率)

  • WW:每条指令在ROB中的平均逗留时间(从分发到提交的周期数)

因此:

Rmin=IPC×WavgR_\mathrm{min} = \mathrm{IPC} \times W_\mathrm{avg}

WavgW_\mathrm{avg}取决于指令的执行延迟和头阻塞。对于一条指令,其在ROB中的逗留时间包括:

  1. 等待发射的时间(在发射队列中等待操作数就绪)

  2. 执行时间(在执行单元中的延迟)

  3. 等待前序指令完成和提交的时间(头阻塞延迟)

在典型的SPEC CPU 2017整数工作负载中,Wavg40W_\mathrm{avg} \approx 408080个周期。如果目标IPC为4,则Rmin=4×80=320R_\mathrm{min} = 4 \times 80 = 320个表项。若考虑长尾效应(少数指令的逗留时间远超平均值,如DRAM访问的200+周期),ROB需要更大的容量以避免在这些长延迟事件中溢出。

多级Cache延迟下的ROB容量需求。在实际处理器中,Load未命中的延迟不是固定的,而是取决于数据在哪一级Cache中命中。表表 38.3分析了不同Cache层级未命中时对ROB容量的需求。

未命中级别延迟(周期)所需ROB容量每千条指令频率对IPC影响
L1命中4–5基线
L1\toL212–1572–90\sim50轻微
L2\toL330–50180–300\sim10中等
L3\toDRAM150–250900–1500\sim2严重

各级Cache未命中延迟与ROB容量需求(6-wide分发,IPC=4假设)

从表表 38.3可以看出,覆盖L2延迟只需要约90个ROB表项——这在Sandy Bridge的168表项ROB中已经绰绰有余。L3延迟需要约300个表项,Golden Cove的512表项ROB可以完整覆盖。但DRAM延迟需要900+表项,即使是Apple Avalanche的696表项ROB也无法完全覆盖。这就是为什么DRAM访问仍然是现代处理器性能的最大瓶颈之一——即使有最大的ROB,也无法完全隐藏DRAM延迟。

ROB容量对IPC的影响

性能分析 1 — ROB容量与IPC的量化关系

ROB容量RR对IPC的影响可以通过以下简化模型来分析。假设程序中Load指令占比fL=25%f_L = 25\%,平均L1 Cache命中率h=95%h = 95\%,L1未命中的惩罚为Pmem=50P_\mathrm{mem} = 50个周期(访问L2+L3),则在ROB容量为RR的处理器上,因ROB满导致前端暂停的概率近似为:

PstallfL(1h)max(0,1RWdPmem)P_\mathrm{stall} \approx f_L \cdot (1-h) \cdot \max\left(0, 1 - \frac{R}{W_d \cdot P_\mathrm{mem}}\right)

其中WdW_d是分发宽度。当RWdPmemR \geq W_d \cdot P_\mathrm{mem}时,ROB有足够的容量容纳一次L2/L3访问期间所有被分发的指令,Pstall0P_\mathrm{stall} \approx 0。对于Wd=6W_d = 6Pmem=50P_\mathrm{mem} = 50,这要求R300R \geq 300

ROB容量平均IPC(int)相对IPC提升主要瓶颈
642.1基准频繁ROB满暂停
1282.9+38%L2延迟下ROB偶尔满
1923.3+57%L3延迟下ROB偶尔满
2563.6+71%接近饱和
3843.8+81%边际收益递减
5123.9+86%其他瓶颈为主

从上表可以看出,ROB容量从64增加到256时,IPC提升显著(+71%)。但从256增加到512时,IPC仅额外提升约15%——这说明在该配置下,256个表项已经能够覆盖大部分L2访问延迟,进一步增大ROB的边际收益迅速递减。然而,如果L3延迟很长(如DRAM访问需要200+周期),更大的ROB仍然有显著价值。

图 38.4以图形方式展示了ROB容量与IPC之间的典型关系曲线。

ROB容量与IPC的关系曲线(定性示意)。不同类型的工作负载对ROB容量的敏感度不同:访存密集型负载受益最大,计算密集型负载在较小ROB即可饱和。
ROB容量与IPC的关系曲线(定性示意)。不同类型的工作负载对ROB容量的敏感度不同:访存密集型负载受益最大,计算密集型负载在较小ROB即可饱和。

从图图 38.4中可以观察到三个特征:

(1)快速上升区R<128R < 128):ROB容量对IPC的影响非常敏感。小ROB导致频繁的满溢暂停,严重限制了乱序引擎发现ILP的能力。

(2)渐近饱和区128R384128 \leq R \leq 384):IPC随ROB增大而提高,但增速逐渐放缓。这个区域是大多数处理器的ROB设计点——256到384表项覆盖了L2延迟并部分覆盖L3延迟。

(3)边际收益区R>384R > 384):IPC几乎不再随ROB增大而提高(对于Cache友好的工作负载)。但对于访存密集型工作负载,由于DRAM延迟极长,即使在这个区域仍有可观的IPC收益。

不同负载类型的分析。计算密集型负载(如矩阵乘法的内核循环,Cache命中率极高)在128表项ROB即已接近饱和——因为几乎没有长延迟的Cache未命中导致头阻塞。访存密集型负载(如数据库查询、图遍历,Cache命中率较低)则在512甚至更大的ROB中仍能获益——因为频繁的L3/DRAM访问产生大量的头阻塞事件。

现代处理器ROB容量的演进

表 38.5列出了现代高性能处理器的ROB容量和相关参数,从中可以看出ROB容量的增长趋势。

处理器年份ROB容量分发宽度ROB/分发宽度比
Intel Sandy Bridge2011168442
Intel Skylake2015224456
Intel Golden Cove2021512685
Intel Lion Cove2024576872
AMD Zen 42022320653
AMD Zen 52024448856
Apple Firestorm2020630879
Apple Avalanche2022696977
ARM Cortex-X32022320564
香山昆明湖2024256643

现代高性能处理器的ROB容量

从表中可以观察到两个趋势:(1)ROB容量随工艺进步和微架构演进持续增长,从2011年Sandy Bridge的168个表项增长到2024年的500+表项;(2)ROB容量与分发宽度的比值大致维持在50–85之间,这反映了"ROB需要容纳足够多个周期的分发指令以覆盖Cache未命中延迟"的设计原则。Apple的设计尤为激进,Avalanche微架构的ROB容量接近700个表项,配合8+的分发宽度,使其能够在长延迟事件(如L2/L3未命中)期间仍保持前端的流水线填充。

为什么Intel从168到224到512?Intel ROB容量的增长轨迹直接对应了工艺进步和内存延迟的变化。Sandy Bridge(2011,32nm)的168表项在当时足以覆盖L2访问延迟(\sim12周期),但对L3未命中(\sim40周期)的覆盖能力有限。Skylake(2015,14nm)将ROB增长到224表项,部分原因是更先进的工艺降低了大ROB的面积成本。Golden Cove(2021,Intel 7)的ROB跳跃到512表项,反映了三个变化:(a)分发宽度从4增加到6,需要更大的ROB来保持相同的覆盖周期数;(b)DRAM延迟在DDR5时代并未显著下降(仍然\sim80ns),而时钟频率从3GHz提升到5GHz,使得以周期计的DRAM访问延迟增加了约67%;(c)Intel 7工艺使得512表项ROB的面积和功耗在可接受范围内。

设计权衡 1 — ROB容量的设计权衡

ROB容量的选择是面积/功耗与性能之间的经典权衡:

  • 更大的ROB>>384表项):

    • 优势:能覆盖更长的Cache未命中延迟,在DRAM密集型工作负载中保持高IPC。

    • 代价:ROB面积增大,功耗增加。更重要的是,大ROB可能增加关键路径延迟(如提交逻辑需要扫描更多表项),影响时钟频率。

    • 间接效果:更大的ROB需要更多的物理寄存器(因为更多的在飞指令同时占用物理寄存器),导致PRF也必须增大。

  • 更小的ROB<<192表项):

    • 优势:面积和功耗更低,适用于移动端和能效核(如Intel Gracemont的128项ROB)。

    • 代价:在长延迟事件中频繁暂停前端,导致IPC下降。

实际设计中,ROB容量的选择需要结合目标工作负载的Cache行为特征。对于服务器处理器(大量Cache未命中),大ROB至关重要;对于嵌入式/移动处理器,较小的ROB配合高命中率的Cache层次可以实现良好的能效。

ROB容量与其他微架构资源的协同约束

在实际处理器中,ROB满并不是唯一导致前端暂停的原因。即使ROB有空闲表项,如果以下任一资源耗尽,分发同样会暂停:

  • 物理寄存器:PRF中的空闲物理寄存器用完。这通常在ROB满之前发生,因为并非所有ROB表项对应的物理寄存器都已被释放。

  • 发射队列:发射队列(issue queue / reservation station)已满。发射队列的容量通常远小于ROB——例如Intel Golden Cove的整数发射队列仅\sim97个表项,远小于512项ROB。

  • Load Queue / Store Queue:Load/Store专用队列已满,限制了新的访存指令进入乱序引擎。

  • 分支检查点:未决分支数量超过检查点存储的上限(如16个未决分支)。

因此,增大ROB容量而不同步增大其他资源的容量,可能无法带来预期的性能提升。现代处理器的微架构设计需要平衡所有关键资源的容量,使它们在典型工作负载下大致同时成为瓶颈,避免某一资源过度配置而浪费面积。

ROB容量的功耗考量

ROB的功耗包括静态功耗和动态功耗两部分。静态功耗与表项数量成正比,在先进工艺(5nm及以下)中,由于晶体管漏电流增加,静态功耗成为越来越大的问题。动态功耗则与每周期访问的表项数量成正比——分发、完成标记和提交操作都会产生动态功耗。对于一个512表项的ROB,在5nm工艺下的功耗估计约为50–100 mW,占整个处理器核心功耗的2%–4%。通过时钟门控(clock gating)技术——在ROB的空闲表项上关闭时钟信号——可以有效降低动态功耗。

ROB的端口需求

ROB作为乱序引擎中被多个流水线阶段同时访问的核心结构,其端口需求直接决定了硬件的面积和时序。ROB需要在三个主要操作中提供端口:分发时的写入、执行完成时的完成标记写入,以及提交时的读出。本节将深入分析每类端口的需求、面积影响和优化手段。

分发写端口

在分发阶段,处理器将解码/重命名后的指令写入ROB。对于WdW_d-wide的分发宽度,每周期最多有WdW_d条指令需要同时写入ROB。每次写入需要向ROB表项的各字段写入相应的值:指令类型、目的寄存器号、物理寄存器号、旧物理寄存器号等,同时设置V=1、Done=0。

因此,ROB需要WdW_d写端口。对于一个6-wide的分发处理器,需要6个同时写端口。

多端口SRAM的面积代价

多端口SRAM的面积与端口数的关系是ROB设计中的核心挑战。一个RR×B\times B位的SRAM,如果有PRP_R个读端口和PWP_W个写端口,其面积近似为:

ARB(PR+PW)2A \propto R \cdot B \cdot (P_R + P_W)^2

面积与(PR+PW)2(P_R + P_W)^2成正比的原因是:每个端口需要独立的字线(word line)和位线(bit line)。字线沿行方向走线,位线沿列方向走线。PP个端口需要PP组字线和PP组位线,二维走线的面积与P2P^2成正比。

以一个256行×\times80位的SRAM为例:

  • 1R1W(1个读端口 + 1个写端口):面积基准A0A_0

  • 6R6W(6个读端口 + 6个写端口):面积A0×(12/2)2=36A0\approx A_0 \times (12/2)^2 = 36 A_0

  • 6R6W+10W(再加10个完成标记写端口):面积A0×(22/2)2=121A0\approx A_0 \times (22/2)^2 = 121 A_0

这说明,如果将ROB作为一个统一的多端口SRAM实现,面积将膨胀数十倍甚至上百倍,完全不可接受。因此,端口优化是ROB物理设计的核心问题。

Bank化设计降低写端口数

幸运的是,分发写入具有一个有利特性:这WdW_d条指令写入的是连续的ROB表项(从tail到tail+Wd1W_d-1)。这种顺序写入模式可以通过bank化(banking)设计来大幅降低端口需求。

具体做法如下:将ROB的RR个表项分为WdW_d个bank,每个bank包含R/WdR / W_d个表项。表项kk所在的bank由kk的低位决定:

bank(k)=kmodWd\mathrm{bank}(k) = k \bmod W_d

这种交错编址确保连续的WdW_d个表项分布在不同的bank中。因此,WdW_d条同时分发的指令一定写入不同的bank——每个bank在每周期只需要1个写端口

硬件描述 3 — 6-wide分发的ROB Bank化实例

对于一个R=384R = 384Wd=6W_d = 6的处理器,ROB被分为6个bank,每个bank有384/6=64384 / 6 = 64个表项。当tail = 12时,分发6条指令写入表项#12–#17:

指令ROB索引写入Bank
I0_0#12Bank 0 (12mod6=012 \bmod 6 = 0)
I1_1#13Bank 1 (13mod6=113 \bmod 6 = 1)
I2_2#14Bank 2 (14mod6=214 \bmod 6 = 2)
I3_3#15Bank 3 (15mod6=315 \bmod 6 = 3)
I4_4#16Bank 4 (16mod6=416 \bmod 6 = 4)
I5_5#17Bank 5 (17mod6=517 \bmod 6 = 5)

每个bank只需要1个写端口,因此6个64表项×\times80位的单端口SRAM bank替代了一个384表项×\times80位的6端口SRAM。面积节省约16×(1/6)×(2/12)2=11/697%1 - 6 \times (1/6) \times (2/12)^2 = 1 - 1/6 \approx 97\%——实际节省比例取决于bank间的互连开销,但仍然非常显著。

需要注意的一个问题是:每条指令对应的bank不是固定的——如果tail的值改变,同一指令可能写入不同的bank。因此,需要一个交叉开关(crossbar)将WdW_d条指令的写数据路由到正确的bank。对于Wd=6W_d = 6,这是一个6×66 \times 6的交叉开关,面积开销可控。

Bank化的一个微妙问题:当RR不是WdW_d的整数倍时,最后一个bank可能比其他bank少一些表项。在实际设计中,通常选择RRWdW_d的整数倍以避免这种不规则性。

Bank化交叉开关的实现

Bank化需要一个交叉开关(crossbar)将WdW_d条指令的写数据路由到正确的bank。交叉开关的路由取决于当前tail指针的值:

指令 Ik 写入Bank=(tail+k)modWd\text{指令}~I_k~\text{写入Bank} = (\mathrm{tail} + k) \bmod W_d

对于Wd=6W_d = 6,交叉开关有6个输入端(6条分发指令的数据)和6个输出端(6个bank的写端口)。路由矩阵是一个循环移位模式——由tailmodWd\mathrm{tail} \bmod W_d决定移位量。这种固定模式的交叉开关可以用WdW_dWdW_d-to-1的多路选择器实现,每个MUX的控制信号由tail的低log2Wd\lceil\log_2 W_d\rceil位决定。

交叉开关的面积估算。每个66-to-1的MUX处理80位数据,需要80×6=48080 \times 6 = 480个2输入MUX门(使用树形结构,实际约80×3=24080 \times 3 = 240个4输入LUT或等效门)。6个MUX总共约1,4401{,}440个门。在5nm工艺下,面积约0.001  mm20.001\;\text{mm}^2,远小于SRAM阵列本身的面积。

读端口的Bank化。提交时的读取同样具有连续性——从head开始连续读取WcW_c个表项。因此,读端口也可以采用与写端口相同的bank化策略和交叉开关结构,只是方向相反(bank输出到提交逻辑的输入)。交叉开关的路由由headmodWc\mathrm{head} \bmod W_c决定。

读写bank冲突分析。一个重要的设计问题是:分发写入和提交读取是否可能同时访问同一个bank?由于写入从tail端进行、读取从head端进行,它们访问的是ROB中不同区域的表项,不会发生bank冲突——前提是使用相同的bank数量和交错模式。在Wd=Wc=6W_d = W_c = 6的情况下,分发和提交各自需要访问6个不同的bank;如果ROB分为6个bank,分发访问的bank由tailmod6\mathrm{tail} \bmod 6决定,提交访问的bank由headmod6\mathrm{head} \bmod 6决定。即使tailmod6=headmod6\mathrm{tail} \bmod 6 = \mathrm{head} \bmod 6,两者访问的是同一bank内的不同行,可以通过为每个bank配置1个读端口和1个写端口(1R1W SRAM)来同时满足。

设计权衡 2 — ROB的物理组织方式

ROB的物理组织有两种主要选择:

  • 统一阵列 + 多端口:使用一个大的多端口SRAM,所有字段存储在一起。优点是逻辑简单,缺点是多端口面积大。适用于小容量ROB(\leq64表项)。

  • 分离阵列 + Bank化:将字段分组存储到不同的专用阵列中,每个阵列独立bank化和端口优化。优点是面积小、时序好,缺点是控制逻辑更复杂。适用于大容量ROB(\geq128表项),是现代处理器的主流选择。

在分离阵列方案中,一个典型的512表项ROB可能被拆分为以下物理结构:

  1. Done位阵列:512×\times1,触发器阵列,12个写端口 + 6个读端口

  2. Exc阵列:512×\times5,触发器阵列,12个写端口 + 6个读端口

  3. 主控制阵列:512×\times50,6-bank SRAM,每bank 1R1W

  4. BranchMask阵列:512×\times8,用于冲刷时的掩码匹配

  5. PC_Offset阵列:512×\times12,仅在异常处理时读取(低端口需求)

完成标记端口

当一条指令在执行单元中完成计算后,需要将对应ROB表项的Done位从0置为1。这个操作称为完成标记(completion marking)或写回标记(writeback marking)。

完成标记端口的数量需求

完成标记端口的数量取决于执行单元的数量——准确地说,是每周期可能完成执行的最大指令数。在一个典型的6-wide处理器中,可能配置如下执行端口:

  • 4个整数ALU端口

  • 2个浮点/SIMD端口

  • 2个Load端口

  • 2个Store地址计算端口

  • 1个分支执行端口

  • 1个整数乘法/除法端口

这意味着每周期最多有12条指令可能同时完成执行,因此ROB需要12个完成标记端口。

完成端口数量的优化。12个完成端口是理论上的最大值。在实际中,某些执行端口不可能在同一周期完成——例如,整数除法器通常需要多个周期,不会与其他除法同时完成。此外,多周期指令(如浮点乘加、向量运算)的完成时机是确定性的,可以通过调度避免与其他端口冲突。因此,实际需要的同时完成标记端口数量可能小于12。

但是,由于Done位的设置不使用传统SRAM端口而是使用解码器-或门结构,端口数量对面积的影响很小(线性增长而非平方增长),因此大多数设计不会刻意减少完成标记端口数——直接按最大执行端口数配置即可。

完成标记的Bank化可能性

虽然完成标记的访问地址是随机的,无法像分发写入那样利用连续性进行bank化,但可以使用一种概率性bank化方案:将Done位阵列分为BB个bank,每个bank包含R/BR/B个表项。每个bank只需Nexec/B+1\lceil N_\mathrm{exec} / B \rceil + 1个写端口(额外的1个是处理bank冲突的缓冲端口)。

Bank冲突分析。在随机访问模型下,NexecN_\mathrm{exec}个完成信号落入BB个bank中的分布服从多项式分布。两个信号落入同一bank的概率(bank冲突率)约为:

Pconflict=1k=0Nexec1BkB1eNexec(Nexec1)/(2B)P_\mathrm{conflict} = 1 - \prod_{k=0}^{N_\mathrm{exec}-1} \frac{B-k}{B} \approx 1 - e^{-N_\mathrm{exec}(N_\mathrm{exec}-1)/(2B)}

对于Nexec=12N_\mathrm{exec} = 12B=16B = 16Pconflict1e66/3287%P_\mathrm{conflict} \approx 1 - e^{-66/32} \approx 87\%——冲突率很高,说明bank化对完成标记的效果不如分发写入那样好。

实际上,由于Done位只有1位宽,解码器-或门方案的面积本身就很小,不需要bank化来降低面积。因此,大多数设计直接使用全局的解码器-或门结构,不对Done位进行bank化。

完成标记与传统SRAM端口的区别

与写端口不同,完成标记具有两个特殊性质:

(1)数据宽度极窄。完成标记只需要修改1位(Done位),加上可能同时写入的异常码(4–5位),总共不超过6位。这与分发写入的80位数据宽度形成鲜明对比。

(2)访问地址随机。完成标记的目标地址(ROB索引)是随机分布的——已完成的指令可能对应ROB中任意位置的表项。这与分发写入的连续访问模式完全不同,使得bank化优化不像分发写入那样简单。

这两个特性的组合意味着:完成标记不适合用传统的多端口SRAM实现,但由于数据宽度极窄,可以用更灵活的逻辑电路来处理。

Done位阵列的解码器-或门实现

在硬件中,完成标记最常见的实现方式是将Done位从ROB主存储阵列中分离出来,形成一个独立的Done位阵列,使用解码器和或门网络实现多端口随机写入。

硬件描述 4 — Done位阵列的集中设置逻辑

对于一个RR项ROB,Done位的设置可以这样实现:每个执行端口广播一个ROB索引idxk\mathrm{idx}_k和一个有效信号validk\mathrm{valid}_k。对于ROB表项ii,其Done位的更新逻辑为:

Done[i]Done[i]k=0Nexec1(validk(idxk=i))\mathrm{Done}[i] \leftarrow \mathrm{Done}[i] \,\lor\, \bigvee_{k=0}^{N_\mathrm{exec}-1} (\mathrm{valid}_k \,\land\, (\mathrm{idx}_k = i))

这等价于:每个执行端口的完成信号先经过一个log2R\log_2 R位的解码器(decoder),产生一个RR位的独热码(one-hot),然后所有执行端口的独热码与当前Done位进行按位或运算。

R=256R = 256Nexec=12N_\mathrm{exec} = 12为例:需要12个8-to-256的解码器,以及256个12输入或门。

门数估算:

  • 每个8-to-256解码器:约256×3=768256 \times 3 = 768个门(每个输出是8个输入的与门,加上反相器)

  • 12个解码器:12×768=9,21612 \times 768 = 9{,}216个门

  • 256个12输入或门:256×12=3,072256 \times 12 = 3{,}072个门(每个或门用12个二输入门级联)

  • 总计:约12,28812{,}288个门,面积很小(不到0.005  mm20.005\;\text{mm}^2 in 5nm)

解码器的延迟约为3–4级门延迟(\sim200 ps in 5nm),或门阵列约2–3级门延迟。总延迟约\sim400 ps,可以在半个时钟周期内完成(5 GHz时钟的半周期为100 ps——因此在实际中可能需要流水线化,将解码和或运算分到不同的半周期)。

异常信息的写入

除了Done位,执行单元还可能需要向ROB写入异常信息——例如,除法器检测到除零异常,或Load单元检测到页错误。异常信息的写入可以与Done位的设置复用同一组解码逻辑(使用相同的ROB索引),只需将异常标志和异常码作为额外的数据一同写入。

在硬件实现中,Exc字段和ExceptionCode字段通常与Done位存储在同一个小型寄存器阵列中,共享相同的写入解码逻辑。由于异常在正常执行中非常罕见(<<0.01%的指令),异常信息的写端口不需要优化到极致——甚至可以使用与Done位相同的解码器但增加少量的数据锁存逻辑。

完成标记的时序考虑

完成标记的时序与指令唤醒(wakeup)和旁路转发(bypass)密切相关。当一条指令完成执行时,以下操作通常需要在同一周期内并行完成:

  1. 将结果值写入PRF(或ROB的Value字段)。

  2. 设置ROB中对应表项的Done位。

  3. 通过CDB或唤醒总线广播结果标签,唤醒发射队列中等待该结果的指令。

  4. 通过旁路网络将结果值转发给同周期发射的依赖指令。

在高频设计中,这些操作之间的时序关系是关键路径的重要组成部分。特别是,完成标记(操作2)和唤醒广播(操作3)必须协调一致——如果Done位还未设置就允许提交,可能导致正确性问题。不过在实际设计中,Done位的设置和提交逻辑的检查通常不在同一流水线阶段,之间有足够的时间裕量。

读端口(提交时读出)

在提交阶段,提交逻辑需要从ROB头部读出表项的内容,以执行提交操作。提交逻辑需要读取的信息包括:Done位(判断是否可提交)、Exc字段(判断是否有异常)、指令类型(确定提交操作)、Dst和PDst/POld(更新体系结构映射和释放物理寄存器)。

对于WcW_c-wide的提交(每周期最多提交WcW_c条指令),ROB需要WcW_c个读端口。这WcW_c条指令的读取同样具有连续性——它们从head开始连续读取WcW_c个表项。因此,与分发写端口类似,可以利用bank化设计来简化读端口。

提交读取的两阶段优化

提交逻辑的读取具有条件性——只有当前表项的Done=1且Exc=0时,才会推进到下一个表项。如果第ii个表项的Done=0,则第i+1i+1到第Wc1W_c-1个表项的读取结果将被丢弃。硬件可以利用这一点进行优化:

第一阶段:快速状态读取。先快速读取WcW_c个表项的Done位和Exc位。这些位存储在独立的小型寄存器文件中(Done位阵列),读取延迟很低。根据结果计算本周期的实际提交数WcW_c'

第二阶段:控制信息读取。根据WcW_c'的值,只从ROB主阵列中读取实际需要的WcW_c'个表项的完整控制信息。这避免了在提交被阻塞时(Wc=0W_c' = 0)无谓地读取ROB主阵列,节省动态功耗。

ROB方式下的读端口压力

在ROB方式下(结果存储在ROB中),提交时还需要读取Value字段(64位),以将结果写入ARF。这使得读端口的数据宽度很大(每端口64+位),增加了读端口的面积和功耗。在PRF方式下,提交时不需要读取大型数据字段,读端口的数据宽度仅为控制信息(\sim30位),显著简化了设计。

端口需求总结

表 38.6总结了ROB在不同操作下的端口需求。

操作端口类型数量数据宽度访问模式实现方式
分发写入写端口6\sim80b连续Bank化SRAM
完成标记Done位写端口121–6b随机解码器+或门
提交读出读端口6\sim30b连续Bank化SRAM
异常信息读取Exc读端口65b连续独立小阵列

ROB的端口需求总结(以6-wide、12个执行端口的处理器为例)

端口带宽的量化分析。从表表 38.6可以计算ROB每周期的总数据传输带宽:

  • 写入带宽6×80=4806 \times 80 = 480位/周期(分发写入)+12×6=72+ 12 \times 6 = 72位/周期(完成标记写入)=552= 552位/周期

  • 读出带宽6×30=1806 \times 30 = 180位/周期(提交读出)+6×5=30+ 6 \times 5 = 30位/周期(Exc读取)=210= 210位/周期

  • 总带宽552+210=762552 + 210 = 762位/周期95\approx 95字节/周期

在5 GHz时钟下,这相当于95×5×109=475  GB/s95 \times 5 \times 10^9 = 475\;\text{GB/s}的数据带宽。虽然数字看起来很大,但由于ROB是片上结构且数据宽度较窄(主要是控制信息而非64位数据),这个带宽在物理上完全可以用合理的布线实现。

作为对比,如果使用ROB方式存储64位Value字段,总带宽将增加到:10×64=64010 \times 64 = 640位/周期(结果值写入)+12×64=768+ 12 \times 64 = 768位/周期(操作数读取)+6×64=384+ 6 \times 64 = 384位/周期(提交读出)=1,792= 1{,}792位/周期,是PRF方式的3.4倍——而且这些都是64位宽的随机访问端口,面积代价远大于控制信息级别的端口。

设计提示

ROB端口设计中最困难的部分不是分发写入或提交读出(它们都是连续访问,可以高效bank化),而是完成标记——因为它是随机访问的。幸运的是,完成标记只修改1位数据,可以通过独立的Done位阵列和解码器网络高效实现。这种将高扇入、窄数据宽度的操作从主存储阵列中分离出来的设计思想,在处理器微架构中非常普遍——类似的例子包括发射队列中的唤醒逻辑(第 29.0 章中的tag广播匹配只需要修改就绪位,而不需要访问完整的表项数据)。

使用ROB管理处理器状态

第 24.0 章所述,早期的乱序处理器(如Intel P6微架构、AMD K6)采用了一种直接的状态管理方式:将指令的执行结果存储在ROB表项中,提交时再从ROB复制到体系结构寄存器文件(ARF)。本节进一步讨论这种方式的详细实现及其与ARF的交互。

ROB中存储结果值

在ROB方式下,每个ROB表项包含一个与体系结构寄存器等宽的Value字段(如RV64下为64位)。当指令在执行单元中完成计算后,结果值被写入对应ROB表项的Value字段,同时Done位被置1。

这种方式下的数据流如下:

  1. 分发阶段:为指令分配ROB表项,记录控制信息。Value字段此时未定义。分配的ROB索引作为该指令的重命名标签——后续依赖于该指令结果的指令将记录此ROB索引作为源操作数的标识符。

  2. 执行完成:执行单元将结果值写入ROB表项的Value字段。此操作需要ROB提供与执行端口数量相同的结果值写端口——这比仅写1位Done位要昂贵得多,因为每个写端口需要传输64位数据。

  3. 操作数读取:后续指令的源操作数可能来自ROB中某个未提交表项的Value字段。这要求ROB提供操作数读端口——在NN-wide处理器中,每周期最多2N2N个源操作数可能需要从ROB读取。

  4. 提交:将Value字段的值写入ARF中Dst指定的逻辑寄存器。

ROB方式下的操作数来源判定

在ROB方式下,一条新指令的每个源操作数需要确定从哪里读取值。这涉及一个关键问题:如何找到某个逻辑寄存器的最新写入者

重命名逻辑维护一个映射表,记录每个逻辑寄存器当前映射到的ROB索引(如果有在飞指令写该寄存器)或标记为"来自ARF"(如果没有在飞指令写该寄存器)。查询过程如下:

  • 如果最新的写入者已经提交(该逻辑寄存器在映射表中没有指向任何in-flight的ROB表项),则从ARF读取。

  • 如果最新的写入者在ROB中且Done=1,则从对应ROB表项的Value字段读取。

  • 如果最新的写入者在ROB中但Done=0,则记录对应的ROB索引作为标签,等待结果产生后通过旁路网络或ROB读取获得。

这种"两源读取"机制需要在操作数读取路径上增加一个多路选择器(MUX),选择来自ARF或ROB的值。在宽发射处理器中,这增加了数据通路的延迟和面积。

使用CAM搜索确定最新写入者的替代方案。有些ROB方式的实现不使用单独的映射表,而是在需要读取操作数时直接搜索ROB,找到写入目标逻辑寄存器的最新表项。这需要将ROB实现为内容可寻址存储器(CAM),以逻辑寄存器号为搜索键,在所有有效且Done=1的表项中查找匹配。CAM搜索的优点是不需要维护额外的映射表,但缺点是:(a)CAM面积远大于等价容量的SRAM(通常3–5倍);(b)搜索延迟较长(需要全局匹配线和优先级编码);(c)每条分发指令的每个源操作数都需要一次搜索,总搜索带宽为2×Wd2 \times W_d。这些开销使得CAM方案在宽发射设计中不可行。

ROB方式的数据流:执行结果写入ROB的Value字段,提交时复制到ARF。后续指令可能需要从ROB和ARF两个位置读取操作数。
ROB方式的数据流:执行结果写入ROB的Value字段,提交时复制到ARF。后续指令可能需要从ROB和ARF两个位置读取操作数。

ROB方式的端口压力分析

在ROB方式下,ROB的Value字段需要以下端口:

  • 结果值写端口NexecN_\mathrm{exec}个(等于执行端口数),每个64位宽,随机访问。

  • 操作数读端口:最多2×Wd2 \times W_d个(每条分发指令最多2个源操作数),每个64位宽,随机访问。

  • 提交读端口WcW_c个(提交宽度),每个64位宽,连续访问。

以6-wide、12个执行端口的处理器为例,ROB Value字段的端口总数为12W+12R+6R=12W+18R12W + 12R + 6R = 12W + 18R,共30个64位端口。对于一个256+表项的结构,这样的多端口需求在面积和时序上几乎不可行。这正是ROB方式在宽发射处理器中被放弃的根本原因。

ROB与ARF的交互

在ROB方式下,ARF(Architecture Register File,体系结构寄存器文件)始终保存已提交的体系结构状态。ARF的内容只在指令提交时才被更新——这保证了ARF中的状态始终是精确的、可恢复的。

提交写回。当ROB头部的指令满足提交条件(Done=1、无异常)时,提交逻辑将该表项的Value字段值写入ARF:

ARF[Dst]ROB[head].Value\mathrm{ARF}[\mathrm{Dst}] \leftarrow \mathrm{ROB}[\mathrm{head}].\mathrm{Value}

对于多指令提交(Wc>1W_c > 1),每周期可能有WcW_c条指令同时提交,需要WcW_c个ARF写端口。如果同一周期内多条提交指令写入同一个逻辑寄存器(WAW情况),只需保留程序顺序中最后一条指令的写入结果。

异常恢复的简洁性。当提交逻辑检测到异常时,ARF中的状态天然是精确的——因为只有已提交的(正确路径上的、无异常的)指令的结果才被写入过ARF。恢复过程只需清空ROB中所有未提交的表项,并将映射关系恢复到仅使用ARF即可。这种恢复的简洁性是ROB方式的重要优势。

案例研究 1 — Intel P6微架构的ROB与ARF交互

Intel P6微架构(Pentium Pro, 1995)是ROB方式的经典实现。P6的参数如下:ROB容量40个表项,ARF包含8个32位整数寄存器和8个80位x87浮点寄存器,分发和提交宽度均为3-wide。

在P6中,μ\muop被分发到保留站(reservation station)时,操作数可以来自三个来源:(1)ARF;(2)ROB中已完成的表项(Done=1);(3)通过CDB(公共数据总线)旁路转发的刚完成的结果。P6使用一个查找逻辑来确定每个源操作数应该从哪个来源获取。

由于P6只有3个CDB(对应3个执行端口),ROB的Value字段需要3个写端口(结果写入)和最多6个读端口(3条指令×\times2个源操作数)。对于40个表项的ROB,这些端口需求在当时的0.6μ\mum工艺下是可行的。然而,随着后续微架构将发射宽度扩展到4-wide和6-wide,P6的ROB方式就变得不可持续了。Intel从NetBurst(Pentium 4, 2000)开始转向PRF方式。

ROB方式的面积扩展性分析

为了更直观地理解ROB方式在宽发射处理器中面临的困境,下面量化分析ROB Value字段的面积随发射宽度的增长。

对于一个NN-wide的处理器,ROB Value字段需要的端口包括:NexecN_\mathrm{exec}个64位写端口(执行完成写入)、2N2N个64位读端口(操作数读取,假设每条指令2个源操作数)、NN个64位读端口(提交读出)。表表 38.7展示了不同发射宽度下的端口需求和面积估算。

发射宽度NN执行端口读端口总端口ROB大小估算面积
3 (P6)3W9R1240项\sim0.02 mm2^2
46W12R18128项\sim0.15 mm2^2
610W18R28256项\sim0.8 mm2^2
812W24R36384项\sim2.5 mm2^2

ROB方式下Value字段的端口和面积随发射宽度增长

面积的增长大致与(Nread+Nwrite)2×R(N_\mathrm{read} + N_\mathrm{write})^2 \times R成正比(多端口SRAM的面积随端口数的平方增长)。从3-wide到8-wide,面积增长了超过100倍——这使得ROB方式在6-wide及以上的设计中变得完全不切实际。

使用物理寄存器管理处理器状态

现代高性能处理器几乎无一例外地采用统一物理寄存器文件(Unified Physical Register File, PRF)方式来管理处理器状态。在这种方式下,ROB不再存储指令的执行结果——结果值被直接写入独立的物理寄存器文件(PRF)中。ROB仅保留控制信息,用于维护程序顺序、支持精确异常和管理物理寄存器的生命周期。

这种分离带来了三个根本性的优势:(1)ROB表项宽度大幅缩小(不含64位Value字段),使得大容量ROB(512+表项)成为可能;(2)ROB不再需要高带宽的数据读写端口,端口设计回归简单;(3)数据值集中存储在PRF中,操作数读取只需访问一个位置(PRF),消除了ROB方式下"ARF还是ROB"的两源选择问题。代价是需要额外的映射管理硬件(推测RAT、提交RAT、空闲列表)和更复杂的物理寄存器生命周期管理。但在现代工艺下,这些控制逻辑的面积和功耗远小于在ROB中存储数据值的开销。

从Intel NetBurst(2000年)开始,所有主流高性能处理器——Intel Core系列、AMD Zen系列、ARM Cortex-A/X系列、Apple A/M系列、RISC-V香山——都采用了PRF方式。可以说,PRF方式是当代乱序处理器设计的事实标准

从ROB方式到PRF方式的历史转变。ROB方式到PRF方式的转变不是一蹴而就的,而是经历了约10年的过渡期。表表 38.8展示了这一历史过程。

处理器年份发射宽度状态管理方式说明
Intel P6 (Pentium Pro)19953ROB方式首个商用ROB处理器
MIPS R1000019964PRF方式首个商用PRF处理器
AMD K619973ROB方式P6的竞争者
Alpha 2126419984PRF方式高性能PRF实现
Intel Pentium 420003PRF方式Intel首次转向PRF
AMD K7 (Athlon)19993PRF方式AMD转向PRF
ARM Cortex-A920072ROB方式移动端仍用ROB方式
ARM Cortex-A1520123PRF方式ARM转向PRF

从ROB方式到PRF方式的历史转变

值得注意的是,MIPS R10000(1996)几乎与Intel P6(1995)同时问世,但它从一开始就采用了PRF方式。这说明PRF方式并不是ROB方式的"后继者"——两种方式几乎同时被提出和实现,但PRF方式最终因其更好的扩展性而胜出。在移动和嵌入式领域,由于发射宽度较窄(2–3 wide),ROB方式的端口压力可控,因此ARM直到Cortex-A15(2012年)才完成向PRF的转变。

PRF方式下的数据流

图 38.6对比了ROB方式和PRF方式的数据流差异。

ROB方式(左)与PRF方式(右)的数据流对比。PRF方式下,ROB不存储数据值,结果直接写入PRF;提交时只需更新映射关系和释放旧寄存器,无需复制数据。
ROB方式(左)与PRF方式(右)的数据流对比。PRF方式下,ROB不存储数据值,结果直接写入PRF;提交时只需更新映射关系和释放旧寄存器,无需复制数据。

在PRF方式下,处理器状态的管理涉及以下核心组件的协同工作:

(1)推测RAT(Speculative RAT,也称前端RAT或重命名RAT):维护每个逻辑寄存器到物理寄存器的当前映射。在分发/重命名阶段被查询(读取源操作数映射)和更新(分配新的物理寄存器给目标操作数)。推测RAT反映的是包含推测指令在内的最新映射状态。

(2)提交RAT(Committed RAT,也称后端RAT或架构RAT):维护已提交指令对应的逻辑寄存器到物理寄存器的映射。只在指令提交时才被更新。提交RAT反映的是精确的体系结构状态——当需要从错误路径恢复时,可以将推测RAT回退到提交RAT的状态。

(3)ROB:仅记录控制信息(不含Value字段),特别是PDst和POld字段,用于指导提交RAT的更新和物理寄存器的释放。

(4)空闲列表(Free List):管理当前未被任何逻辑寄存器映射使用的物理寄存器池。新指令在重命名时从空闲列表获取一个物理寄存器,旧的物理寄存器在对应指令提交时被归还到空闲列表。

(5)物理寄存器文件(PRF):存储所有的数据值。PRF的容量通常为逻辑寄存器数量的3倍到6倍(如32个逻辑寄存器对应128–192个物理寄存器),以提供足够的重命名空间。

PRF方式下的指令生命周期

PRF方式下的指令生命周期如下:

  1. 重命名/分发:指令的目标逻辑寄存器rdr_d从推测RAT中查找到当前映射PoldP_\mathrm{old}。从空闲列表分配新物理寄存器PnewP_\mathrm{new}。更新推测RAT:SpecRAT[rd]Pnew\mathrm{SpecRAT}[r_d] \leftarrow P_\mathrm{new}。在ROB表项中记录(rd,Pnew,Pold)(r_d, P_\mathrm{new}, P_\mathrm{old})

  2. 执行完成:结果值写入PRF[Pnew]\mathrm{PRF}[P_\mathrm{new}],对应ROB表项Done=1。

  3. 提交:更新提交RAT:CommitRAT[rd]Pnew\mathrm{CommitRAT}[r_d] \leftarrow P_\mathrm{new}。将PoldP_\mathrm{old}释放回空闲列表。

  4. 错误恢复:将推测RAT回退到提交RAT的状态(或恢复到最近的检查点)。将错误路径上分配的物理寄存器全部归还空闲列表。

下面通过一个具体的示例,展示PRF方式下ROB和相关结构的协同工作过程。

示例:三条指令的重命名、执行和提交。考虑以下RISC-V指令序列,假设处理器有64个整数物理寄存器(P0–P63),初始映射为x1P1\mathrm{x1} \to \mathrm{P1}x2P2\mathrm{x2} \to \mathrm{P2}x3P3\mathrm{x3} \to \mathrm{P3},空闲列表头部为P32、P33、P34。

asm
add  x1, x2, x3    # I1: x1 = x2 + x3
    sub  x4, x1, x5    # I2: x4 = x1 - x5  (RAW依赖I1)
    mul  x1, x6, x7    # I3: x1 = x6 * x7  (WAW与I1)

周期1(重命名/分发):I1被重命名。查询推测RAT得到x2\toP2、x3\toP3作为源操作数。从空闲列表分配P32作为新的x1映射。推测RAT更新:x1\toP32。ROB表项#0记录:Dst=x1, PDst=P32, POld=P1。

周期2(重命名/分发):I2被重命名。查询推测RAT得到x1\toP32(I1的结果)、x5\toP5。分配P33给x4。推测RAT更新:x4\toP33。ROB表项#1记录:Dst=x4, PDst=P33, POld=P4。I3被重命名。查询推测RAT得到x6\toP6、x7\toP7。分配P34给x1。推测RAT更新:x1\toP34。ROB表项#2记录:Dst=x1, PDst=P34, POld=P32。

周期3–5(执行):I1在ALU中执行,结果写入PRF[P32],ROB#0的Done=1。I2等待P32就绪后执行,结果写入PRF[P33],ROB#1的Done=1。I3在乘法器中执行3个周期。

周期6(提交I1):ROB head=0。检查ROB#0:Done=1、Exc=0。提交I1:更新提交RAT:x1\toP32。释放POld=P1到空闲列表。Head前进到1。

周期7(提交I2):ROB head=1。检查ROB#1:Done=1、Exc=0。提交I2:更新提交RAT:x4\toP33。释放POld=P4。Head前进到2。

周期8(提交I3):ROB head=2。假设I3已完成。提交I3:更新提交RAT:x1\toP34。释放POld=P32。注意:P32是I1的结果寄存器,在I3提交后才被释放——因为直到I3提交时,I2(使用P32的值)一定已经提交完毕,P32中的值不再被任何指令需要。

这个示例清楚地展示了POld字段的作用:I3的POld=P32,正是I1分配的物理寄存器。I3覆盖了I1对x1的映射,因此当I3提交时,I1的结果寄存器P32可以被安全释放。

两种方式的面积与时序量化对比

硬件描述 5 — PRF方式的ROB表项缩减

将PRF方式和ROB方式的表项位宽进行具体比较。假设RV64处理器,192个物理寄存器(8位编号),512表项ROB:

指标ROB方式PRF方式比值
每表项位宽\sim177b\sim76b2.3×\times
512表项总存储量11.1 KB4.8 KB2.3×\times
Value字段端口数30个64位端口0
SRAM面积(存储阵列)\sim0.09 mm2^2\sim0.04 mm2^22.3×\times
端口相关面积开销\sim2.5 mm2^2\sim0.10 mm2^225×\times
ROB总面积\sim2.6 mm2^2\sim0.14 mm2^219×\times

面积差异的主导因素不是存储密度(两者的存储阵列面积只差2.3倍),而是端口开销——ROB方式需要30个64位的数据端口,其面积与(30)2=900(30)^2 = 900成正比;PRF方式的ROB只需要控制信息级别的端口(每端口8–30位宽),面积开销降低了一到两个数量级。

时序对比。ROB方式还存在额外的时序劣势,下面逐一分析。

(1)操作数读取路径。在ROB方式下,操作数可能来自ARF或ROB,需要在读取路径上增加一个2-to-1的MUX。这个MUX增加了约1级门延迟(\sim50 ps in 5nm)。更严重的是,MUX的选择信号来自映射表查询结果——需要先确定源操作数是否在ROB中、是否已完成,然后才能选择MUX方向。这个选择逻辑的延迟可能达到2–3级门延迟,使得操作数读取的总延迟增加约150 ps。

(2)ROB Value字段的读取延迟。在6-wide设计中,ROB的Value字段需要18个64位读端口。对于一个256行的SRAM,18个读端口使得每条位线的负载非常重——18个读取晶体管连接在同一条位线上,总电容约为单端口SRAM的18倍。位线电容的增加直接导致读取延迟增加:

TreadRCcell(1+Nread_ports)RwireT_\mathrm{read} \propto R \cdot C_\mathrm{cell} \cdot (1 + N_\mathrm{read\_ports}) \cdot R_\mathrm{wire}

对于18端口、256行的64位SRAM,读取延迟可能超过500 ps——在5 GHz时钟下(200 ps周期)这需要至少2.5个流水线阶段。

(3)提交写回的额外延迟。ROB方式在提交时需要将Value从ROB复制到ARF,这涉及从ROB读取64位数据并写入ARF。这个数据搬运操作在PRF方式下完全不存在——PRF方式的提交只需要更新几个位宽很窄的映射信息,不涉及任何64位数据搬运。

总结:PRF方式在以下每个时序路径上都优于ROB方式:操作数读取(少一个MUX)、结果写回(写PRF与写ROB的端口压力相当,但PRF可以独立优化)、提交(无64位数据搬运)。这些时序优势使得PRF方式在5 GHz+的高频设计中几乎是唯一可行的选择。

提交时释放旧的物理寄存器

在PRF方式下,物理寄存器的释放时机是设计中最微妙的部分之一。一个物理寄存器PoldP_\mathrm{old}可以被安全释放的充要条件是:没有任何当前在飞或将来会执行的指令需要从PoldP_\mathrm{old}中读取值

这个条件在以下时刻得到满足:当写入同一逻辑寄存器rdr_d的后续指令IjI_j提交时。此时:

  • IjI_j的结果已经存入PnewP_\mathrm{new}并成为rdr_d的新的体系结构值。

  • IjI_j之前的所有指令(可能使用PoldP_\mathrm{old}的值)都已经提交——因为提交是严格按程序顺序进行的。

  • IjI_j之后的指令使用的是PnewP_\mathrm{new}(或更晚版本的映射),不再引用PoldP_\mathrm{old}

因此,释放规则可以简洁地表述为:

当指令Ij提交时,释放  Pold=Ij被重命名之前rd映射到的物理寄存器 \text{当指令$I_j$提交时,释放}\; P_\mathrm{old} = \text{$I_j$被重命名之前$r_d$映射到的物理寄存器}

这正是ROB表项中POld字段的用途——在重命名阶段记录旧映射,在提交阶段用于释放。

释放流程的具体实现

  1. 提交逻辑从ROB头部读取待提交指令的Dst和POld字段。

  2. 更新提交RAT:CommitRAT[Dst]PDst\mathrm{CommitRAT}[\mathrm{Dst}] \leftarrow \mathrm{PDst}

  3. 将POld放入空闲列表的尾部。

  4. 对于多指令提交(Wc>1W_c > 1),需要检查同一周期内多条提交指令是否写入同一逻辑寄存器——如果是,只有最后一条(程序顺序中最晚的)指令的PDst成为最终的体系结构映射,而中间指令的PDst也应该被释放。

多指令提交的WAW检测

WcW_c条指令在同一周期提交,且其中第ii条和第jj条(i<ji < j)写入同一个逻辑寄存器rdr_d时,提交逻辑需要:

  • 最终只将第jj条的PDst写入提交RAT(它是程序顺序中最新的映射)

  • 释放第ii条的PDst(因为它立即被第jj条覆盖)

  • 同时释放第ii条和第jj条的POld

检测WcW_c条指令中的WAW冲突需要(Wc2)\binom{W_c}{2}个Dst比较器。对于Wc=6W_c = 6,需要(62)=15\binom{6}{2} = 15个5位比较器。这些比较器的面积很小,但产生的优先级编码逻辑(确定每个逻辑寄存器的最后写入者)会增加提交阶段的关键路径延迟。

性能分析 2 — 物理寄存器的平均占用时间

一个物理寄存器从被分配到被释放之间的占用时间(lifetime)取决于两个因素:(1)该指令从分发到提交的延迟(包括执行延迟和等待程序顺序中更早指令完成的延迟);(2)之后下一条写入同一逻辑寄存器的指令从分发到提交的延迟。

在典型的SPEC CPU工作负载中,物理寄存器的平均占用时间约为30–60个时钟周期。对于一个6-wide的处理器,每周期大约消耗4个物理寄存器(约2/3的指令产生寄存器结果),则PRF需要至少4×60+32=2724 \times 60 + 32 = 272个物理寄存器才能避免频繁的空闲列表耗尽。这解释了为什么现代处理器的PRF容量通常在200+个物理寄存器的范围内。

无目标寄存器指令的处理

并非所有指令都有目标寄存器——Store指令将数据写入内存而非寄存器,无条件分支指令不产生寄存器结果。对于这些指令,ROB表项中的PDst和POld字段为无效,提交时不需要更新提交RAT,也不需要释放物理寄存器。硬件通过指令类型字段或专门的标记位来识别这种情况。

异常和流水线冲刷时的物理寄存器恢复

当发生异常或分支预测错误导致流水线冲刷时,需要回收错误路径上分配的所有物理寄存器。恢复方法取决于错误恢复的策略:

  • 检查点恢复:使用最近的检查点恢复推测RAT和空闲列表,被丢弃指令分配的物理寄存器随空闲列表的恢复而自动回收。这种方法恢复速度快(1周期),但需要在每个分支处保存检查点,空间开销大。

  • Walk回退:从ROB尾部向head方向逐条回退,利用每个表项的POld信息将推测RAT恢复到分支点的状态,并将PDst回收到空闲列表。这种方法不需要检查点的额外存储开销,但恢复速度慢——如果ROB中有KK条需要回退的指令且每周期可以回退WwalkW_\mathrm{walk}条,则恢复时间为K/Wwalk\lceil K / W_\mathrm{walk} \rceil个周期。以Wwalk=6W_\mathrm{walk} = 6K=200K = 200为例,恢复需要34个周期,这对分支密集的代码是不可接受的延迟。

  • 提交RAT恢复:直接将推测RAT整体拷贝为提交RAT的值。此法恢复速度快(1–2个周期,取决于RAT的大小和拷贝带宽)且不需要ROB walk,但只能恢复到最后提交的状态(而非分支点的状态),在分支点和当前提交点之间的正确路径指令也会被丢弃,需要重新执行。现代处理器通常将此法作为最坏情况下的后备方案。

  • 混合方式:许多现代处理器结合使用检查点和Walk回退。对于分支预测错误,使用检查点快速恢复(因为分支频繁且需要低延迟恢复);对于异常和中断,使用提交RAT恢复(因为异常罕见且恢复到提交点是正确的)。

Walk回退的具体操作。Walk回退过程中,ROB从tail向head逐条扫描每个表项。对于每个有效表项iiV[i]=1V[i] = 1):

  1. 读取Dst[i]\mathrm{Dst}[i]POld[i]\mathrm{POld}[i]

  2. 将推测RAT恢复:SpecRAT[Dst[i]]POld[i]\mathrm{SpecRAT}[\mathrm{Dst}[i]] \leftarrow \mathrm{POld}[i]

  3. PDst[i]\mathrm{PDst}[i]释放回空闲列表。

  4. 将tail指针回退一位。

这个过程本质上是重命名操作的逆操作——重命名时将逻辑寄存器的映射从POld更新为PDst,Walk回退时将其从PDst恢复为POld。由于Walk回退需要反向扫描ROB(从tail到head),ROB需要支持从tail端读取表项的能力,这对bank化的ROB设计增加了额外的读端口需求。

引用计数方式的物理寄存器管理

在支持Move消除(Move Elimination)的处理器中,一个物理寄存器可能同时被多个逻辑寄存器映射——因为Move消除通过让目标逻辑寄存器共享源操作数的物理寄存器来实现零延迟拷贝。在这种情况下,简单的POld释放规则不再适用,需要为每个物理寄存器维护一个引用计数器(reference counter)。物理寄存器只有在引用计数归零时才能被释放。引用计数的管理增加了硬件复杂度,但Move消除带来的性能收益(在x86代码中约2–3% IPC提升)使其物有所值。

引用计数器的位宽取决于最大可能的引用数。在最坏情况下,所有在飞指令都可能对同一个物理寄存器进行Move消除映射,但实际中3–4位的计数器(支持最多15个引用)已足够。每个物理寄存器一个计数器,NPRF=192N_\mathrm{PRF} = 192个物理寄存器需要192×4=768192 \times 4 = 768位的额外存储,面积开销微乎其微。

PRF方式下物理寄存器的完整生命周期。正常路径(实线)从空闲列表分配,经过使用,最终在覆盖指令提交后释放回空闲列表。错误路径(虚线)在流水线冲刷时直接回收。
PRF方式下物理寄存器的完整生命周期。正常路径(实线)从空闲列表分配,经过使用,最终在覆盖指令提交后释放回空闲列表。错误路径(虚线)在流水线冲刷时直接回收。

从图图 38.7可以看出,一个物理寄存器的生命周期可以分为五个阶段。在正常执行中,物理寄存器从空闲列表被分配出来,经历"已分配\to已写入\to已提交\to过时"四个状态后回到空闲列表。在流水线冲刷时,处于"已分配"和"已写入"状态(但尚未提交)的物理寄存器直接回收到空闲列表。"已提交"状态的物理寄存器不受冲刷影响——它们保存着正确的体系结构状态。

提交的实现

提交(commit),也称为退休(retire),是乱序处理器流水线的最后一个阶段。提交逻辑的核心任务是:按照程序顺序,将已经执行完毕且没有异常的指令的结果固化到体系结构状态中。提交之后,指令的效果对软件可见,不可撤销。

提交阶段在处理器的整体流水线中的位置可以通过以下时序图来理解。图图 38.8展示了一条指令从分发到提交的完整时序。

一条指令从重命名到提交的流水线时序。指令在ROB中的生命周期从分发开始到提交结束。"等待发射"和"等待提交"的时间是可变的。
一条指令从重命名到提交的流水线时序。指令在ROB中的生命周期从分发开始到提交结束。"等待发射"和"等待提交"的时间是可变的。

从图图 38.8可以看出,指令在ROB中的逗留时间包括三个可变的部分:(a)在发射队列中等待操作数就绪的时间(0到数十个周期);(b)执行单元中的计算时间(1到数十个周期,取决于指令类型和Cache行为);(c)等待程序顺序中更早的指令完成以便自己被提交的时间(0到数百个周期)。其中(c)是头阻塞的体现,也是ROB容量需求的主要驱动因素。

提交宽度

提交宽度(commit width/retire width)WcW_c定义了每周期最多可以提交的指令数。提交宽度的选择涉及以下考虑:

(1)提交宽度必须至少等于分发宽度。如果提交宽度小于分发宽度,在持续高IPC的工作负载中,ROB的入队速率将持续超过出队速率,最终导致ROB满溢、前端暂停。因此,WcWdW_c \geq W_d是一条基本设计原则。在实际设计中,Wc=WdW_c = W_d是最常见的选择。

(2)提交宽度可以大于分发宽度。有些处理器将WcW_c设计得大于WdW_d,以在间歇性的提交暂停(如等待长延迟指令完成)之后快速"追赶"。例如,如果一条L2 Cache未命中的Load指令导致提交暂停了50个周期,在该Load完成后,ROB中可能积累了50×Wd=30050 \times W_d = 300条已完成的指令等待提交。如果Wc=Wd=6W_c = W_d = 6,追赶这些积压的指令需要50个周期;如果Wc=8>Wd=6W_c = 8 > W_d = 6,只需38个周期。

(3)提交宽度对硬件复杂度的影响。更大的WcW_c意味着提交逻辑每周期需要检查更多表项、更新更多提交RAT映射、释放更多物理寄存器。这增加了提交级的关键路径延迟和面积。在实际设计中,提交宽度通常等于或略小于分发宽度。

处理器分发宽度提交宽度比值
Intel Golden Cove661.0
AMD Zen 4661.0
Apple Firestorm881.0
ARM Cortex-X3561.2
香山昆明湖661.0

现代处理器的分发宽度与提交宽度

提交宽度对IPC的瓶颈分析

提交宽度是否会成为性能瓶颈?这取决于ROB头部指令的完成模式。在实际运行中,提交的有效吞吐量受限于以下因素:

(a)头部指令的完成延迟。如果ROB头部指令因执行延迟或Cache未命中而尚未完成(Done=0),提交带宽为0,无论WcW_c多大。这是最主要的提交瓶颈。

(b)连续可提交的指令数量。即使Wc=6W_c=6,如果连续6个表项中第3个是Done=0,则本周期只能提交2条指令。这种"中间阻塞"降低了提交带宽的利用率。

(c)特殊指令的序列化约束。Fence、CSR写入等特殊指令在提交时可能需要独占整个提交周期,不能与其他指令并行提交。

根据对SPEC CPU 2017工作负载的模拟分析,6-wide提交的平均利用率约为60%–75%,即平均每周期实际提交3.6–4.5条指令。提交带宽本身很少成为瓶颈——真正的瓶颈是头部指令的完成延迟,即头阻塞效应。

性能分析 3 — 提交带宽利用率的分布分析

在一个6-wide提交的处理器上运行SPEC CPU 2017整数基准(mcf为例),提交带宽的利用分布如下:

每周期提交数WcW_c'占总周期比例累计
035%35%
18%43%
27%50%
36%56%
47%63%
58%71%
629%100%

可以观察到一个双峰分布:大约35%的周期完全没有提交(Wc=0W_c' = 0,因为头阻塞),约29%的周期满带宽提交(Wc=6W_c' = 6),中间的提交数占比相对均匀。这种双峰特征反映了程序行为的间歇性——要么头部指令完成后大量积压的已完成指令被快速提交(Wc=6W_c' = 6),要么等待头部长延迟指令而完全无法提交(Wc=0W_c' = 0)。

mcf是SPEC CPU中最典型的访存密集型基准之一(L3未命中率约5%),其35%的零提交周期说明头阻塞是其主要性能瓶颈。对于Cache友好的基准(如lbm),零提交周期比例可降至10%以下。

平均提交数Wˉc=0.35×0+0.08×1++0.29×63.0\bar{W}_c = 0.35 \times 0 + 0.08 \times 1 + \cdots + 0.29 \times 6 \approx 3.0条/周期,即IPC\approx3.0。这与mcf在现代6-wide处理器上的典型IPC一致。

提交逻辑的关键路径

提交宽度WcW_c对关键路径的影响主要体现在条件串行依赖上。考虑连续的WcW_c个表项的提交检查:

  • 第1个表项可以无条件开始检查(Done[head]? Exc[head]?)。

  • 第2个表项只有在第1个表项可提交时才需要检查——否则提交在第1个位置就停止了。

  • kk个表项的检查依赖于前k1k-1个表项全部可提交。

这形成了一条从head到head+Wc1W_c-1的串行依赖链。在最简单的实现中,这需要WcW_c级门延迟(每级进行Done和Exc的与运算)。对于Wc=8W_c = 8,这约为8级门延迟——在高频处理器中可能占用半个时钟周期以上。

并行前缀优化

优化方法之一是使用并行前缀树(parallel prefix tree),类似于进位前缀加法器中的技术。定义"可提交"位:

Ck=Done[k]Exc[k]C_k = \mathrm{Done}[k] \,\land\, \overline{\mathrm{Exc}[k]}

则第kk个表项实际可提交的条件是:

Commit[k]=CheadChead+1Chead+k\mathrm{Commit}[k] = C_\mathrm{head} \,\land\, C_{\mathrm{head}+1} \,\land\, \cdots \,\land\, C_{\mathrm{head}+k}

这是一个前缀与(prefix AND)操作,可以用Kogge-Stone或Brent-Kung结构的并行前缀树在O(logWc)O(\log W_c)级门延迟内完成。对于Wc=8W_c = 8,仅需log28=3\lceil\log_2 8\rceil = 3级门延迟。

8-wide提交的前缀与树具体结构。Wc=8W_c = 8为例,8个CkC_k位的前缀与计算使用3级门延迟完成:

  • 第0级(输入):C0,C1,C2,C3,C4,C5,C6,C7C_0, C_1, C_2, C_3, C_4, C_5, C_6, C_7

  • 第1级(相邻对与):C0:1=C0C1C_{0:1} = C_0 \land C_1C2:3=C2C3C_{2:3} = C_2 \land C_3C4:5=C4C5C_{4:5} = C_4 \land C_5C6:7=C6C7C_{6:7} = C_6 \land C_7

  • 第2级(4-wide前缀):C0:3=C0:1C2:3C_{0:3} = C_{0:1} \land C_{2:3}C4:7=C4:5C6:7C_{4:7} = C_{4:5} \land C_{6:7}

  • 第3级(8-wide前缀):C0:7=C0:3C4:7C_{0:7} = C_{0:3} \land C_{4:7}

  • 输出合成Commit[0]=C0\mathrm{Commit}[0] = C_0Commit[1]=C0:1\mathrm{Commit}[1] = C_{0:1}Commit[3]=C0:3\mathrm{Commit}[3] = C_{0:3}Commit[7]=C0:7\mathrm{Commit}[7] = C_{0:7}。中间位置需要额外的组合逻辑,如Commit[2]=C0:1C2\mathrm{Commit}[2] = C_{0:1} \land C_2

总门数:约8+4+2+1+4=198 + 4 + 2 + 1 + 4 = 19个与门(包括中间位置的补充计算)。延迟:3级与门\approx3个门延迟\approx150 ps(5nm),完全可以在200 ps的时钟周期内完成。

WcW_c'导出head的增量。前缀与树的输出Commit[0..Wc1]\mathrm{Commit}[0..W_c-1]是一个形如111...10...0111...10...0的位模式(从1到某个位置变为0)。WcW_c'等于连续1的个数,可以通过一个WcW_c位的前导一计数器(leading-ones counter)得到。对于Wc=8W_c = 8,这个计数器输出3位(编码0–8),逻辑深度约2级门延迟。

流水线化提交

另一种优化是将提交检查和实际的状态更新分为两个流水线阶段:

  1. 提交检查阶段(Commit Check):从ROB头部读取WcW_c个表项的Done位和Exc位,用并行前缀树计算本周期的实际提交数WcW_c'。同时预读取这WcW_c个表项的完整控制信息(Dst、PDst、POld等)。

  2. 提交执行阶段(Commit Execute):根据WcW_c'执行实际的提交操作——更新提交RAT、释放物理寄存器到空闲列表、推进head指针、更新性能计数器等。

将提交分为两个阶段的好处是:提交检查阶段的逻辑相对简单(主要是Done位的前缀与运算和Exc位的检查),可以在半个时钟周期内完成;而提交执行阶段涉及RAT和空闲列表的更新,逻辑更复杂但不在关键路径上——因为提交执行的结果只影响后续的提交操作和(通过空闲列表)后续的重命名操作,不在前端到后端的关键延迟路径上。

提交的顺序约束与精确异常

提交必须严格按程序顺序进行——这是ROB存在的根本意义。下面分析为什么顺序提交对处理器正确性至关重要,以及它在不同场景下的具体表现。

精确异常的实现

考虑程序顺序中的三条指令:I1、I2、I3。假设I2是一条Load指令,执行时触发了页错误异常。在乱序引擎中,I3可能在I2之前就完成了执行。如果允许I3先于I2提交,那么I3的结果就已经写入了体系结构状态。当I2的异常被处理时,处理器无法呈现"I2及其之后的指令未执行"的精确状态——因为I3已经修改了体系结构寄存器。

顺序提交确保了:在I2被检查之前,I3不会提交。当提交逻辑检测到I2的异常时,I2及其后所有指令(包括已执行但未提交的I3)都被丢弃,体系结构状态保持精确。

具体而言,异常处理的流程如下:

  1. 提交逻辑检查ROB头部指令,发现Done=1但Exception=1。

  2. 记录异常指令的PC(从ROB的PC_Offset字段恢复)和异常码。

  3. 冲刷ROB中该指令及其后的所有表项(将tail回退到head+1或直接清空ROB)。

  4. 在PRF方式下,恢复推测RAT到提交RAT的状态,回收错误路径上的物理寄存器。

  5. 将异常PC和异常码写入CSR(如RISC-V的mepcmcause),跳转到异常处理程序入口。

分支预测错误恢复

如果I1是一条分支指令且被错误预测,在提交I1时发现预测错误(BranchMispred=1),I2和I3(错误路径上的指令)即使已经执行完毕也不会被提交,而是被丢弃。

需要注意的是,在现代处理器中,分支预测错误的恢复通常不等到提交时才执行——分支执行单元在检测到预测错误时就立即启动恢复(使用检查点方式),以减少错误预测的惩罚。但ROB中的BranchMispred标记仍然用于提交时的最终确认,以及在检查点不可用时的后备恢复路径。

早期恢复 vs 提交时恢复。分支预测错误恢复有两种时机选择:

  • 早期恢复(Early Recovery):分支在执行单元中完成计算后,立即比较预测方向与实际方向。如果不匹配,立即从检查点恢复推测RAT和空闲列表,冲刷错误路径上的所有指令,不等到该分支到达ROB头部。这种方式的优势是恢复延迟短(错误预测惩罚约12–15个周期),但需要每条分支都有对应的检查点。

  • 提交时恢复(Commit-time Recovery):分支执行完成后只设置BranchMispred标志,等到该分支到达ROB头部(被提交时)才执行恢复。这种方式不需要检查点(直接使用cRAT恢复),但恢复延迟很长——需要等待该分支之前的所有指令都完成执行和提交,惩罚可能超过50个周期。

现代高性能处理器普遍采用早期恢复。提交时恢复仅作为后备方案——当检查点资源耗尽时(所有检查点都被未解析的分支占用),新的分支无法创建检查点,只能退化到提交时恢复。

中断处理

当外部中断到达时,处理器需要在一个精确的指令边界停下来。实现方式是:让ROB中在某个指令之前的所有指令正常提交,该指令及之后的指令不提交。中断被视为附加在某条指令上的"虚拟异常"。

具体而言,中断处理通常按以下步骤实现:

  1. 中断控制器向处理器发出中断请求信号。

  2. 提交逻辑在下一个指令边界处"注入"一个虚拟异常——选择ROB中某个已提交或即将提交的指令作为中断点。

  3. 该中断点之前的所有指令正常提交。

  4. 该中断点及其之后的所有ROB表项被冲刷(flush),处理器进入中断服务程序。

  5. 中断服务程序执行完毕后,处理器从中断点恢复执行。

内存序违规的处理

在推测执行中,Load指令可能在某个Store之前执行(如果推测它们访问不同的地址),但如果后来发现它们实际上访问了相同的地址(即发生了内存序违规),则该Load的结果是错误的。内存序违规通常在Store地址计算完成时被检测到(通过与Load Queue中已执行Load的地址比较)。检测到违规后,违规的Load指令在其ROB表项中被标记为异常,当它到达ROB头部时,触发流水线冲刷和重新执行。

提交逻辑的状态机。提交逻辑每周期从head开始检查表项,根据Done位和Exc位决定正常提交、等待或触发异常处理。
提交逻辑的状态机。提交逻辑每周期从head开始检查表项,根据Done位和Exc位决定正常提交、等待或触发异常处理。

图 38.9展示了提交逻辑的简化状态机。在正常操作中,提交逻辑在"检查"和"正常提交"之间快速循环。一旦检测到异常,进入异常处理状态,冲刷整个ROB并恢复体系结构状态。

提交逻辑的硬件实现细节

提交逻辑是ROB中最复杂的控制电路之一。下面详细描述其每周期的操作流程和硬件结构。

第一步:读取Done和Exc位。在每个周期的开始,提交逻辑从Done位阵列和Exception阵列中同时读取从head开始的WcW_c个表项的状态。由于Done位阵列和Exc阵列都是独立的小型触发器阵列,可以在时钟的上升沿后很快(\sim100 ps)提供数据。

第二步:计算可提交前缀长度。利用并行前缀树计算WcW_c'——从head开始连续满足(Done[i]=1)(Exc[i]=0)(Done[i] = 1) \land (Exc[i] = 0)的最长前缀。定义:

CanCommit[k]={Done[head]Exc[head]k=0CanCommit[k1]Done[head+k]Exc[head+k]k>0\mathrm{CanCommit}[k] = \begin{cases} \mathrm{Done}[\mathrm{head}] \land \overline{\mathrm{Exc}[\mathrm{head}]} & k = 0 \\ \mathrm{CanCommit}[k-1] \land \mathrm{Done}[\mathrm{head}+k] \land \overline{\mathrm{Exc}[\mathrm{head}+k]} & k > 0 \end{cases}

Wc=max{k+1:CanCommit[k]=1,0k<Wc}W_c' = \max\{k+1 : \mathrm{CanCommit}[k] = 1, 0 \leq k < W_c\},如果CanCommit[0]=0\mathrm{CanCommit}[0] = 0Wc=0W_c' = 0

第三步:读取主控制字段。根据WcW_c'的值,从ROB主控制阵列中读取实际需要的WcW_c'个表项的完整控制信息。在两阶段流水线的设计中,这一步可以与第一步并行执行(乐观地读取所有WcW_c个表项),然后根据WcW_c'的值选择性地使用。

第四步:执行提交操作。对每条实际提交的指令执行以下操作(并行进行):

  • 向提交RAT的对应端口写入新映射(Dst[i],PDst[i])(\mathrm{Dst}[i], \mathrm{PDst}[i])

  • 向空闲列表的入队端口写入POld[i]\mathrm{POld}[i]

  • 如果是Store指令,向Store Queue发送提交信号

  • 如果是分支指令,向分支预测器发送训练信号

  • 如果是Load指令,向Load Queue发送释放信号

  • 更新性能计数器

第五步:更新head指针。head(head+Wc)modR\mathrm{head} \leftarrow (\mathrm{head} + W_c') \bmod R

第六步:异常检查。如果Wc<WcW_c' < W_cDone[head+Wc]=1\mathrm{Done}[\mathrm{head} + W_c'] = 1Exc[head+Wc]=1\mathrm{Exc}[\mathrm{head} + W_c'] = 1,说明下一条未提交的指令有异常。触发异常处理流程。注意,异常处理不在当前提交周期内开始——它在下一个周期生效,因为当前周期已经提交了WcW_c'条指令。

提交RAT的写端口组织。提交RAT需要WcW_c个写端口以支持每周期WcW_c条指令的映射更新。但由于WAW冲突的存在,WcW_c条指令中可能有多条写入同一逻辑寄存器,此时只有最后一条的写入有效。硬件使用(Wc2)\binom{W_c}{2}个5位Dst比较器检测WAW冲突,并用优先级编码器生成每个写端口的有效信号:

valid_write[i]=CanCommit[i]¬(j=i+1Wc1(Dst[i]=Dst[j]))\mathrm{valid\_write}[i] = \mathrm{CanCommit}[i] \,\land\, \neg\left(\bigvee_{j=i+1}^{W_c'-1} (\mathrm{Dst}[i] = \mathrm{Dst}[j])\right)

只有valid_write[i]=1\mathrm{valid\_write}[i] = 1的指令才实际更新提交RAT。其他被WAW覆盖的指令的PDst也需要被释放到空闲列表(因为它们的映射立即被后续指令覆盖)。

空闲列表的入队逻辑。提交逻辑每周期最多向空闲列表入队WcW_c个物理寄存器号(POld)。空闲列表通常实现为一个FIFO队列(也是循环队列),入队操作与ROB的tail更新类似。WcW_c个同时入队需要WcW_c个写端口,可以通过与ROB相同的bank化策略优化。

需要注意的是,不是所有提交的指令都会释放物理寄存器——没有目标寄存器的指令(Store、无链接分支等)不释放。空闲列表的入队逻辑需要根据指令类型(通过IsStore/IsBranch等标记位)过滤掉这些指令。实际入队数量为:

Nrelease=i=0Wc1HasDst[i]N_\mathrm{release} = \sum_{i=0}^{W_c'-1} \mathrm{HasDst}[i]

其中HasDst[i]\mathrm{HasDst}[i]表示第ii条提交指令是否有目标寄存器。

ROB冲刷的实现

当提交逻辑检测到异常或中断时,需要执行ROB冲刷(ROB flush/squash),丢弃ROB中所有未提交的指令。冲刷操作涉及以下步骤:

(1)停止前端。立即向取指和解码阶段发送冲刷信号,停止新指令的进入。这个信号必须在冲刷决定的同一周期或下一周期就到达前端。

(2)重置tail指针。将tail设置为等于head(或head+1,取决于异常指令本身是否需要保留)。这在逻辑上将ROB中所有未提交的表项标记为无效。由于循环队列中表项的有效性由head/tail指针的关系隐式定义,重置tail指针就等价于"丢弃"所有未提交的指令,不需要逐个清除每个表项的Valid位。

(3)恢复推测RAT。将推测RAT的内容恢复为提交RAT的内容。在硬件中,这通常通过将提交RAT的所有32个条目同时拷贝到推测RAT来实现——需要推测RAT支持整体写入(bulk write)。对于32个8位条目,整体写入需要32×8=25632 \times 8 = 256位的数据通路,在现代工艺中可以在1个周期内完成。

(4)恢复空闲列表。将空闲列表的head指针恢复到冲刷前的检查点值。如果使用提交RAT恢复方案,空闲列表的恢复可以通过以下方式实现:扫描恢复后的推测RAT(即提交RAT的内容),获取当前体系结构映射使用的所有物理寄存器编号,然后将所有不在这个集合中的物理寄存器放入空闲列表。这个操作需要比较NarchN_\mathrm{arch}个RAT条目与NPRFN_\mathrm{PRF}个物理寄存器号,在硬件中可以通过位向量操作高效实现。

(5)冲刷发射队列、Load Queue和Store Queue。这些结构中的推测指令也需要被丢弃。发射队列和Load Queue直接清空(使用类似的tail指针重置或Valid位整体清零)。Store Queue中只有推测状态的表项被清除,已提交的表项保留。

冲刷延迟。完整的冲刷操作可能需要2–3个周期:第1周期停止前端并重置tail指针;第2周期恢复推测RAT和空闲列表;第3周期发出冲刷信号到发射队列和Load/Store Queue。在冲刷完成之前,前端不能开始取指新的指令(异常处理程序或正确路径的指令),因此冲刷延迟直接增加到异常/分支恢复的总惩罚中。

选择性冲刷。对于分支预测错误的恢复,不需要冲刷整个ROB——只需要冲刷错误路径上的指令。如果使用检查点恢复,可以通过BranchMask字段精确识别哪些指令在错误路径上(BranchMask中对应该分支的位为1),只冲刷这些指令。这种选择性冲刷比全局冲刷更精确,保留了正确路径上已经在ROB中的指令,减少了重新取指和执行的代价。但选择性冲刷的硬件实现更复杂——需要在ROB的所有有效表项中进行BranchMask匹配,类似于CAM搜索操作。

头阻塞与顺序提交的性能代价

顺序提交的主要性能代价在于头阻塞(head-of-line blocking):如果ROB头部的指令是一条长延迟操作(如L3 Cache未命中的Load,延迟100+周期),则即使ROB中后续的许多指令都已经完成执行,它们也无法提交。这导致ROB的有效占用量持续增加,最终可能导致ROB满溢、前端暂停。

这就是为什么ROB容量必须足够大以覆盖最坏情况下的头阻塞延迟——如果ROB只有64个表项,而头部指令等待200个周期的DRAM访问,那么仅仅32个周期(对于2-wide分发)后ROB就会满,前端将被暂停超过160个周期,造成严重的性能损失。

头阻塞的严重程度可以通过以下公式估算。设ROB容量为RR,分发宽度为WdW_d,头部指令的阻塞延迟为TblockT_\mathrm{block}个周期,则因头阻塞导致的前端暂停周期数为:

Tstall=max(0,TblockRWd)T_\mathrm{stall} = \max\left(0, T_\mathrm{block} - \frac{R}{W_d}\right)

TblockR/WdT_\mathrm{block} \leq R / W_d时,ROB有足够的缓冲空间容纳阻塞期间的新指令,Tstall=0T_\mathrm{stall} = 0。当Tblock>R/WdT_\mathrm{block} > R / W_d时,前端被暂停TstallT_\mathrm{stall}个周期。以R=256R = 256Wd=6W_d = 6为例,ROB可以缓冲约43个周期的分发——对于L2 Cache命中(\sim15个周期)完全够用,但对于DRAM访问(\sim200个周期)则远远不够,将导致约157个周期的前端暂停。

性能分析 4 — 头阻塞导致的IPC损失量化

可以定量分析头阻塞对IPC的影响。假设在一段NtotalN_\mathrm{total}条指令的执行中,发生了KK次头阻塞事件,每次阻塞的平均持续时间为Tˉblock\bar{T}_\mathrm{block}个周期。则因头阻塞导致的总暂停周期数为:

Tstall,total=Kmax(0,TˉblockRWd)T_\mathrm{stall,total} = K \cdot \max\left(0, \bar{T}_\mathrm{block} - \frac{R}{W_d}\right)

在没有头阻塞时的理想IPC为IPCideal\mathrm{IPC}_\mathrm{ideal},则理想执行时间为Tideal=Ntotal/IPCidealT_\mathrm{ideal} = N_\mathrm{total} / \mathrm{IPC}_\mathrm{ideal}。加上暂停时间后,实际IPC为:

IPCactual=NtotalTideal+Tstall,total=IPCideal1+IPCidealTstall,totalNtotal\mathrm{IPC}_\mathrm{actual} = \frac{N_\mathrm{total}}{T_\mathrm{ideal} + T_\mathrm{stall,total}} = \frac{\mathrm{IPC}_\mathrm{ideal}}{1 + \mathrm{IPC}_\mathrm{ideal} \cdot \frac{T_\mathrm{stall,total}}{N_\mathrm{total}}}

以一个具体例子说明:Ntotal=106N_\mathrm{total} = 10^6条指令,IPCideal=4\mathrm{IPC}_\mathrm{ideal} = 4K=200K = 200次DRAM访问(L3未命中率约0.02%),Tˉblock=200\bar{T}_\mathrm{block} = 200周期,R=256R = 256Wd=6W_d = 6。则:

Tstall,total=200×max(0,20043)=200×157=31,400个周期Tideal=106/4=250,000个周期IPCactual=106250,000+31,4003.55\begin{aligned} T_\mathrm{stall,total} &= 200 \times \max(0, 200 - 43) = 200 \times 157 = 31{,}400\text{个周期} \\ T_\mathrm{ideal} &= 10^6 / 4 = 250{,}000\text{个周期} \\ \mathrm{IPC}_\mathrm{actual} &= \frac{10^6}{250{,}000 + 31{,}400} \approx 3.55 \end{aligned}

IPC从理想的4.0下降到3.55,损失约11%。如果ROB增大到512表项(R/Wd=85R/W_d = 85),则Tstall,total=200×115=23,000T_\mathrm{stall,total} = 200 \times 115 = 23{,}000周期,IPCactual3.66\mathrm{IPC}_\mathrm{actual} \approx 3.66,损失减小到约8.5%。进一步增大ROB到1024表项(R/Wd=170R/W_d = 170),则Tstall,total=200×30=6,000T_\mathrm{stall,total} = 200 \times 30 = 6{,}000周期,IPCactual3.91\mathrm{IPC}_\mathrm{actual} \approx 3.91,损失仅2.3%。

这个分析说明了ROB容量与DRAM访问延迟之间的深层关系:在DRAM延迟固定的情况下,ROB需要足够大以覆盖Wd×TDRAMW_d \times T_\mathrm{DRAM}个表项才能有效消除头阻塞的影响。对于Wd=6W_d = 6TDRAM=200T_\mathrm{DRAM} = 200周期,这需要R1200R \geq 1200——远超当前任何处理器的ROB容量。这也解释了为什么Cache层次结构(减小有效的Tˉblock\bar{T}_\mathrm{block})和预取技术(减小KK)对性能如此重要——单纯增大ROB无法完全解决内存延迟问题。

多个长延迟事件的叠加效应。在实际程序中,多个长延迟事件可能在时间上重叠——例如,多个独立的Load同时经历Cache未命中。在这种情况下,ROB的缓冲效果比单一事件分析更好:多个并行的Cache未命中共享相同的ROB缓冲空间,因为它们的延迟可以重叠(MLP,Memory Level Parallelism)。ROB越大,能够"看到"的并行Cache未命中就越多,从而更有效地利用内存带宽。这是大ROB的另一个重要价值——不仅仅是隐藏单个Cache未命中的延迟,还能发现和利用多个并行的Cache未命中。

设计提示

ROB的顺序提交虽然是性能的约束因素,但它是实现精确异常的必要条件,而精确异常又是支持虚拟内存、调试、操作系统上下文切换等关键系统功能的基础。学术界曾提出过一些放松顺序提交约束的方案——如检查点处理器(checkpoint processing)允许在检查点之间的指令以任意顺序提交,以及非阻塞提交(non-blocking retire)允许Store指令在提交后异步完成Cache写入。但在商用处理器中,严格的顺序提交仍然是主流,因为它在正确性验证和硬件复杂度之间提供了最佳的平衡点。

各类指令的提交操作

提交逻辑需要根据指令类型执行不同的操作。表表 38.12总结了各类指令在提交时的具体行为。

指令类型提交时的操作
ALU指令更新提交RAT(Dst\toPDst),释放POld到空闲列表
Load指令同ALU指令。另外从Load Queue中释放对应表项,确认无内存序违规
Store指令将Store Queue中对应表项标记为已提交(可排出到Cache)。如果Store有目标寄存器(如Load-and-Store),还需更新提交RAT
分支指令释放对应的分支检查点资源。如果分支有链接寄存器(如JAL),更新提交RAT
系统调用/Fence可能触发流水线排空(drain),确保所有先前指令都已完成
无目标寄存器指令不更新提交RAT,不释放物理寄存器

各类指令的提交操作

提交时的状态更新总结

提交一条指令涉及以下状态更新操作的组合:

(1)更新提交RAT(cRAT)。对于有目标寄存器的指令,将cRAT[Dst]PDst\mathrm{cRAT}[\mathrm{Dst}] \leftarrow \mathrm{PDst}。cRAT的更新需要WcW_c个写端口(每周期最多WcW_c条指令提交,每条最多更新一个映射)。但由于WAW冲突的存在,实际写入cRAT的有效更新可能少于WcW_c个——同一逻辑寄存器只保留最后一条的映射。

(2)释放POld到空闲列表。对于有目标寄存器的指令,将POld归还到空闲列表。空闲列表的入队端口需要WcW_c个。

(3)更新分支预测器。分支指令提交时,可以向分支预测器回报该分支的实际结果(taken或not-taken)以及实际目标地址。这些信息用于更新预测器的内部状态(如PHT表项、BTB表项),提高后续预测的准确率。在提交时而非执行完成时更新预测器的好处是:只有正确路径上的分支信息才被用于训练预测器,避免了错误路径分支对预测器的"污染"。

(4)释放分支检查点。如果使用检查点恢复策略,分支指令在提交后可以释放对应的检查点存储空间,使其可以被后续分支复用。

(5)释放Load Queue和Store Queue表项。Load指令提交时释放其Load Queue表项;Store指令提交时将其Store Queue表项标记为"已提交"(不立即释放,因为数据还需要排出到Cache)。

(6)更新性能计数器。提交阶段也是更新性能计数器(performance counter)的理想位置。由于提交后的指令代表了真正执行的体系结构指令(而非推测路径上被丢弃的指令),性能计数器应该在提交时才递增。常见的提交时更新的计数器包括:已退休指令计数(minstret,RISC-V的机器指令退休计数CSR)、已退休分支计数、已退休Load/Store计数、异常计数等。

多指令提交中的WAW冲突处理

在一个WcW_c-wide的提交周期中,可能有多条指令写入同一个逻辑寄存器。例如,在Wc=6W_c = 6的提交中,第2条和第5条指令都写入x1。在这种情况下:

  • 提交RAT最终应该反映程序顺序中最后一条写入该寄存器的指令的映射——即第5条指令的PDst。

  • 第2条指令的PDst在同一周期内就变成了"旧的"映射,应该被释放到空闲列表。

  • 硬件需要一个优先级编码器(priority encoder)来检测同一周期提交中的WAW冲突,并正确处理提交RAT的更新和物理寄存器的释放。

这种WAW提交冲突的检测逻辑对于宽提交处理器(Wc6W_c \geq 6)是一个不可忽视的设计复杂度。在PRF方式下,它影响提交RAT的写端口组织和空闲列表的入队逻辑。

硬件描述 6 — 6-wide提交的WAW冲突检测电路

对于Wc=6W_c = 6的提交,WAW冲突检测需要(62)=15\binom{6}{2} = 15个Dst字段比较器。每个比较器比较两个5位Dst值,产生1位匹配信号。15个比较器的总门数约为15×10=15015 \times 10 = 150个门(每个5位比较器约10个门),面积微乎其微。

但比较结果的优先级编码更复杂。对于每条提交指令ii0i<60 \leq i < 6),需要检查是否存在j>ij > i使得Dst[j]=Dst[i]\mathrm{Dst}[j] = \mathrm{Dst}[i]。如果存在,则ii的PDst应该被释放而不是写入cRAT。优先级编码的逻辑深度为3级门延迟(比较\to或归约\to使能生成)。

提交RAT的写端口可以利用WAW检测结果进行优化:由于被WAW覆盖的指令不需要写入cRAT,实际写入cRAT的有效端口数可能少于WcW_c。但在硬件设计中,通常仍然为cRAT配置WcW_c个物理写端口,通过使能信号控制哪些端口实际写入——这样做的好处是避免了动态仲裁的复杂度。

提交与空闲列表的交互细节

空闲列表(Free List)是提交逻辑的重要下游消费者。每周期,提交逻辑需要向空闲列表归还最多WcW_c个物理寄存器号。空闲列表通常也实现为循环队列,其tail指针(入队端)每周期推进NreleaseN_\mathrm{release}位。

空闲列表的容量与ROB的关系。空闲列表的容量等于PRF容量减去逻辑寄存器数量:NFL=NPRFNarchN_\mathrm{FL} = N_\mathrm{PRF} - N_\mathrm{arch}。例如,192个物理寄存器、32个逻辑寄存器的配置中,空闲列表最多包含160个条目。空闲列表的占用量(当前空闲寄存器数)与ROB中在飞指令数呈互补关系——ROB中有KK条有目标寄存器的在飞指令时,空闲列表中有NFLKN_\mathrm{FL} - K个空闲寄存器。当空闲列表为空(K=NFLK = N_\mathrm{FL})时,即使ROB有空间,分发也必须暂停——因为无法为新指令分配物理寄存器。

空闲列表耗尽 vs ROB满:哪个先发生?这取决于指令mix中有目标寄存器的指令比例fdstf_\mathrm{dst}。如果fdstf_\mathrm{dst}较高(如纯计算代码,fdst80%f_\mathrm{dst} \approx 80\%),则空闲列表可能在ROB满之前先耗尽。如果fdstf_\mathrm{dst}较低(如Store密集代码,fdst50%f_\mathrm{dst} \approx 50\%),则ROB可能先满。设计者需要确保NFLN_\mathrm{FL}RR的比值与典型工作负载的fdstf_\mathrm{dst}匹配。对于fdst=0.67f_\mathrm{dst} = 0.67(约2/3的指令有目标寄存器),平衡条件为:

NFL=fdstRNPRF=fdstR+NarchN_\mathrm{FL} = f_\mathrm{dst} \cdot R \quad \Rightarrow \quad N_\mathrm{PRF} = f_\mathrm{dst} \cdot R + N_\mathrm{arch}

对于R=512R = 512fdst=0.67f_\mathrm{dst} = 0.67Narch=32N_\mathrm{arch} = 32NPRF375N_\mathrm{PRF} \approx 375。这接近Apple设计的整数PRF容量(\sim380),说明Apple的PRF配置是针对其大ROB精心匹配的。

提交与分发的交互。提交操作释放ROB表项和物理寄存器,使得分发阶段可以分配新的资源。这形成了一个反馈环路:提交速度影响可用资源量,可用资源量影响分发是否暂停,分发暂停又影响ROB中指令的填充和最终的提交速率。在稳态下,提交速率必须等于分发速率(否则ROB会持续填充或持续排空)。在瞬态情况下(如长延迟指令导致提交暂停后恢复),提交可以暂时快于分发,消化ROB中积压的已完成指令。

Store指令的提交与Store Buffer排出

Store指令的提交是所有指令类型中最复杂的,因为它涉及两个独立的操作:(1)将Store标记为已提交(可以安全写入Cache/内存);(2)将数据实际写入Cache。这两个操作不必在同一周期完成——事实上,在几乎所有现代处理器中,它们是解耦(decoupled)的。

Store的完整生命周期

图 38.10展示了Store指令从执行到最终写入Cache的完整流程。

Store指令从分发到写入Cache的完整流程。提交时Store从推测状态变为已提交状态,但实际写入Cache由Store Buffer异步完成。
Store指令从分发到写入Cache的完整流程。提交时Store从推测状态变为已提交状态,但实际写入Cache由Store Buffer异步完成。

Store的提交与Store-to-Load转发的交互

Store的提交对Store-to-Load转发(STL forwarding)有重要影响。在Store提交之前,后续的Load可以通过Store Queue中的地址匹配获取Store的数据(转发)。Store提交之后,Store Queue中的表项可能被释放(在分离结构中)或被标记为已提交(在统一结构中)。

关键问题:如果一个Store刚刚提交但尚未排出到Cache,此时一个同线程的后续Load访问相同地址,该Load是否仍然能获取Store的数据?答案是必须能够——否则会产生数据正确性问题。

在分离结构中,这意味着Store Buffer也需要支持Store-to-Load转发(不仅仅是Store Queue)。具体而言,Load单元在访问L1 Data Cache的同时,需要并行搜索Store Queue和Store Buffer中是否有更新的Store写入了相同地址。如果在任一结构中找到匹配的Store且其数据覆盖了Load的全部字节,则直接转发数据,不需要等待Cache访问完成。

在统一结构中,转发逻辑不需要区分推测和已提交的Store——所有Store都在同一个结构中,统一搜索即可。这是统一结构的一个设计简化优势。

转发优先级。当Store Queue和Store Buffer中都有匹配的Store时,需要确定转发优先级。规则是:程序顺序中最新的(最接近该Load的)Store的数据被转发。在分离结构中,如果Store Queue和Store Buffer都命中,Store Queue中的匹配Store一定比Store Buffer中的更新(因为Store Buffer中的Store已经提交,而Store Queue中的Store尚未提交,在程序顺序中更晚)。因此,Store Queue的转发优先级高于Store Buffer。

部分转发的处理。如果匹配的Store的写入范围不完全覆盖Load的读取范围(如Store写入了4字节,Load读取8字节),则不能完整转发。处理方式有两种:(a)拒绝转发,让Load等待Store排出到Cache后再从Cache读取;(b)部分转发——从Store获取已写入的字节,从Cache获取其余字节,然后合并。方式(b)更高效但硬件更复杂,需要字节级的合并逻辑。现代处理器通常采用方式(b),特别是对于Store和Load大小不同的情况(如一个32位Store后跟一个64位Load)。

Store Queue与Store Buffer的分离

在很多微架构教材中,Store Queue和Store Buffer常被混淆使用,但在实际硬件设计中,它们通常是不同的结构,承担不同的角色:

  • Store Queue(SQ):属于乱序引擎的一部分,保存推测性的(尚未提交的)Store指令的地址和数据。Store Queue支持Store-to-Load转发(第 36.0 章),即后续的Load指令可以从尚未提交的Store中直接获取数据,无需等待数据写入Cache。Store Queue中的表项在指令被取消(如分支预测错误)时可以被丢弃。

  • Store Buffer(SB):保存已提交的Store指令的地址和数据,等待被排出(drain/dequeue)到L1 Data Cache或写缓冲。Store Buffer中的数据是不可撤销的——它们代表了已经成为体系结构状态一部分的存储操作。

在一些实现中,Store Queue和Store Buffer合并为一个统一结构,通过一个"已提交"标记位来区分推测状态和已提交状态的表项。Apple和ARM的设计通常采用统一结构;Intel和AMD则倾向于使用分离的结构。

设计权衡 3 — Store Queue和Store Buffer:统一 vs 分离

两种实现方式的权衡如下:

统一结构的优势:

  • 不需要在提交时将数据从一个结构搬移到另一个结构——只需翻转一个标记位。

  • 总表项数可以在推测Store和已提交Store之间灵活分配——在Store密集代码中,已提交Store可以占用更多表项(只要Store-to-Load转发仍然有足够的推测Store表项)。

  • 硬件更简单——只需一个CAM/SRAM结构而非两个。

分离结构的优势:

  • Store Queue可以针对Store-to-Load转发优化(需要CAM搜索以进行地址匹配),而Store Buffer可以针对排出优化(需要顺序FIFO访问),两者的访问模式不同。

  • 分支预测错误恢复时,Store Queue中的推测Store被丢弃——在分离结构中这只影响Store Queue,不影响Store Buffer中已提交的Store。在统一结构中,冲刷操作需要更仔细地只丢弃推测表项而保留已提交表项。

  • Store Buffer的排出可以独立于Store Queue的分发,减少端口冲突。

在现代设计中,分离结构更为主流(Intel、AMD均采用),因为它允许两个结构独立优化,且冲刷逻辑更简单。

Store提交的详细步骤

  1. 提交逻辑检查ROB头部的Store指令:确认Done=1(地址和数据已就绪)、无异常。

  2. 将对应的Store Queue表项标记为已提交。在分离结构中,这可能涉及将数据从Store Queue移动到Store Buffer;在统一结构中,只需修改一个状态位。

  3. 推进ROB的head指针,释放ROB表项。

  4. 异步排出:Store Buffer中的已提交表项由独立的排出逻辑(drain logic)按顺序写入L1 Data Cache。排出操作独立于提交操作——提交逻辑不需要等待Store Buffer排出完成就可以继续提交后续指令。

Store Buffer排出的约束

  • 排出必须按程序顺序(在TSO内存模型下)。如第 37.0 章所讨论,x86(TSO)要求Store之间保持程序顺序,因此Store Buffer的排出必须按照Store在程序中出现的顺序进行。ARM和RISC-V等弱内存模型的处理器可以放松这一约束,允许Store乱序排出(在fence指令的约束范围内)。

  • 排出可能因Cache未命中而暂停。如果Store的目标地址在L1 Data Cache中未命中,需要发起Cache行填充请求,排出操作被暂停直到Cache行到达。在此期间,后续的Store(在TSO下)也不能排出,但这不会阻塞ROB的提交——提交逻辑只管将Store标记为已提交,不关心它是否已经实际写入Cache。

  • Store Buffer接近满时反压提交。如果Store Buffer的容量有限(如Intel Golden Cove的72个表项),当Store Buffer接近满时,新的Store指令即使在ROB中已完成也无法提交,因为没有空间将它们移入Store Buffer。这会导致提交暂停,进而导致ROB填满、前端暂停。这就是为什么Store Buffer的容量也是性能的关键参数之一。

  • 排出与缓存一致性协议的交互。Store的排出需要获得对应Cache行的独占所有权(Exclusive ownership)——在MESI协议中,这意味着Cache行必须处于Modified或Exclusive状态。如果Cache行当前被其他核心共享(Shared状态),需要先发送Invalidate请求使其他核心失效该行,等待确认后才能完成Store排出。这个过程可能需要数十个周期,进一步增加了排出延迟。

Store的两阶段完成

Store指令实际上涉及两个子操作:(1)地址计算——由AGU(地址生成单元)完成,计算Store的目标内存地址;(2)数据就绪——Store的源数据寄存器值已可用。这两个子操作可能在不同的时间完成。ROB中Store指令的Done位通常在两者都完成后才被设置为1。某些设计对Store使用两个Done位(地址Done和数据Done),只有当两者都为1时,提交逻辑才认为该Store可以提交。

这种两阶段完成机制的意义在于:即使Store的数据源尚未就绪,Store的地址计算可以提前完成,使得Store-to-Load转发逻辑可以提前进行地址比较,判断后续的Load是否与该Store冲突。这对内存消歧(memory disambiguation,参见第 36.0 章)至关重要。

性能分析 5 — Store Buffer排出速率对性能的影响

Store Buffer的排出速率受限于L1 Data Cache的写端口数量写延迟。在典型的高性能处理器中,L1 Data Cache提供2个Store端口(每周期最多排出2个Store)。

如果程序中Store指令占比fS=15%f_S = 15\%,在IPC=4的情况下,每周期平均产生4×0.15=0.64 \times 0.15 = 0.6个提交的Store。2个Store排出端口可以轻松处理这个负载。但在Store密集的代码段(如memcpy、数组初始化),Store可能占总指令的50%以上,每周期产生2+个提交的Store,此时Store Buffer的排出可能成为瓶颈。

场景Store比例每周期Store是否瓶颈
通用整数代码15%0.6
矩阵转置50%2.0临界
memcpy(非向量化)50%2.0临界
向量化Store12%0.5否(每Store更多数据)

现代处理器通过Store合并(store coalescing/combining)来缓解排出瓶颈:如果Store Buffer中多个连续的Store写入同一Cache行的不同位置,可以将它们合并为一次Cache写入,有效提高排出效率。

Store合并的实现

Store合并(Store Coalescing)是Store Buffer的一个重要优化。其基本思想是:如果Store Buffer中的连续几个Store指令写入同一个Cache行的不同字节位置,可以将这些Store的数据合并到一个临时缓冲中,然后一次性写入Cache,从而将多次Cache写入操作合并为一次。

Store合并的实现需要以下硬件支持:

  1. 地址比较逻辑:比较Store Buffer中相邻表项的地址高位(Cache行地址),判断是否指向同一Cache行。对于64字节的Cache行,比较的是地址的高PA_width6\mathrm{PA\_width} - 6位。

  2. 合并缓冲:一个临时的64字节缓冲区(等于一个Cache行大小),加上64位的字节有效掩码(标识哪些字节被写入)。多个Store的数据按其地址偏移量写入合并缓冲的对应位置。

  3. 合并写入控制:当合并缓冲被"关闭"(即不再有新的Store指向同一Cache行,或合并缓冲满了)时,将合并缓冲的内容连同字节有效掩码一次性写入L1 Data Cache。

Store合并在memset等内存填充操作中效果显著。例如,连续8个8字节的Store(写入64字节)可以合并为1次Cache行写入,排出带宽提高8倍。

Store合并的约束。在TSO内存模型下,Store合并必须只在相邻的已提交Store之间进行——不能跨越Fence指令或I/O空间的Store。此外,对MMIO(Memory-Mapped I/O)区域的Store不能合并,因为每个Store可能触发不同的硬件副作用。处理器通常通过页表中的Cache属性位(如UC、WC、WB)来判断某个地址是否允许Store合并。

Store提交的原子性考虑

在支持多线程的处理器中,Store的提交和排出之间的窗口期是一个微妙的正确性问题。考虑以下场景:Store指令S已经提交(成为体系结构状态的一部分),但尚未排出到Cache。此时,另一个线程执行了一条Load指令,读取与S相同的地址——该Load应该看到S的值吗?

答案取决于内存一致性模型。在TSO模型下,其他线程在S实际写入Cache之前看不到S的值,这是合法的——TSO允许Store被"缓冲"。但在更强的一致性模型(如SC)下,这可能需要额外的机制来确保全局可见性顺序。Store Buffer的设计必须与目标一致性模型紧密配合,这在第 37.0 章中有详细讨论。

Fence指令和序列化指令的提交

某些特殊指令在提交时需要确保特定的序列化行为:

  • 内存Fence指令(如RISC-V的FENCE、x86的MFENCE):在提交时可能需要等待Store Buffer完全排空,确保所有先前的Store对其他核心可见后,才允许后续指令提交。这会导致提交暂停,直到Store Buffer清空。

  • CSR写入指令(如RISC-V的CSRRW):可能修改处理器的控制状态(如中断使能、浮点舍入模式),需要在提交时确保后续指令看到新的控制状态。某些实现会在CSR写入提交时触发流水线排空。

  • TLB刷新指令(如RISC-V的SFENCE.VMA):提交时需要使TLB失效,可能需要排空流水线以确保后续指令使用新的地址翻译。

这些序列化指令的提交处理增加了提交逻辑的复杂度,但在正常执行中它们出现的频率很低(通常<<0.1%),对整体性能的影响可以忽略。

ROB的功耗优化

随着ROB容量从128增长到512甚至700个表项,其功耗在处理器核心总功耗中的占比日益显著。本节讨论ROB功耗的组成和优化技术。

ROB功耗的组成

ROB的功耗可以分为以下几个部分:

(1)动态功耗。每次读写ROB(分发写入、完成标记、提交读出)都消耗动态功耗。动态功耗与每周期的读写次数和数据宽度成正比:

PdynamicfclkCcellVdd2(NreadBread+NwriteBwrite)P_\mathrm{dynamic} \propto f_\mathrm{clk} \cdot C_\mathrm{cell} \cdot V_\mathrm{dd}^2 \cdot (N_\mathrm{read} \cdot B_\mathrm{read} + N_\mathrm{write} \cdot B_\mathrm{write})

其中fclkf_\mathrm{clk}是时钟频率,CcellC_\mathrm{cell}是每个存储单元的电容,VddV_\mathrm{dd}是供电电压,NN是每周期的访问次数,BB是每次访问的位宽。

(2)静态功耗(漏电功耗)。SRAM单元在不被访问时仍然因晶体管漏电流而消耗功耗。静态功耗与表项数量和工艺节点直接相关:

PstaticRBtotalIleakVddP_\mathrm{static} \propto R \cdot B_\mathrm{total} \cdot I_\mathrm{leak} \cdot V_\mathrm{dd}

在5nm及以下工艺中,IleakI_\mathrm{leak}显著增加,静态功耗可能占ROB总功耗的30%–50%。

(3)时钟网络功耗。分配到ROB各阵列的时钟信号本身消耗的功耗,包括时钟树的缓冲器和布线电容。对于大容量ROB,时钟网络功耗可能占总功耗的20%–30%。

对于一个512表项、80位宽的PRF方式ROB,在5nm、5 GHz工艺下,功耗的典型分布如下:

功耗组成估算值占比
动态功耗(读/写操作)\sim25 mW35%
静态功耗(漏电)\sim25 mW35%
时钟网络功耗\sim15 mW21%
辅助逻辑功耗\sim7 mW9%
总计\sim72 mW100%

512表项ROB的功耗分布估算(5nm, 5 GHz)

时钟门控与动态功耗优化

时钟门控(clock gating)是降低ROB动态功耗最有效的技术。基本思想是:在ROB的空闲表项上关闭时钟信号,使这些表项的触发器不在每个时钟边沿翻转。

粗粒度时钟门控

将ROB阵列按区域(如bank或行组)划分,为每个区域设置一个时钟门控信号。当某个区域中没有有效表项时(通过head/tail指针判断),关闭该区域的时钟。这种方式的粒度较粗——只有整个区域都空闲时才能关闭时钟——但实现简单,控制逻辑只需要几个比较器。

对于一个分为8个bank的512表项ROB,每个bank有64个表项。在ROB占用率为50%(256表项)时,平均约4个bank完全空闲,可以关闭这4个bank的时钟,动态功耗节省约50%。

细粒度时钟门控

对每个表项或每KK个表项设置独立的时钟门控。空闲表项的时钟被关闭,只有被分配的表项(V=1)的时钟保持开启。这种方式的功耗节省更精确,但时钟门控信号的生成逻辑更复杂——需要为每个表项维护一个有效/无效状态,并驱动对应的时钟门控门。

写使能门控。更精细的优化是:即使表项有效,如果本周期没有被写入,也可以关闭该表项的写时钟。这通过将ROB索引的写解码器输出直接用作时钟门控信号来实现——只有被解码选中的表项才打开写时钟。这种方式在SRAM实现中是默认的(只有被选中的字线被激活),但在触发器阵列实现中需要显式的门控逻辑。

Done位阵列的功耗优化

Done位阵列是ROB中动态功耗最高的部分之一,因为它每周期都被完成标记逻辑广播访问。即使大多数表项不需要修改Done位,解码器和或门网络仍然会在每个周期切换,产生动态功耗。

优化方法包括:(a)使能限定的解码——只有当validk=1\mathrm{valid}_k = 1时才激活第kk个解码器,避免无效端口的解码器无谓切换;(b)分区域解码——将512表项的Done位阵列分为多个子区域(如8个64项子区域),先用ROB索引的高位选择子区域,再用低位在子区域内解码,未被选中的子区域不切换。

ROB压缩与虚拟ROB

学术界提出了一些更激进的ROB功耗优化方案:

(1)ROB压缩(ROB Compression)。观察到在许多工作负载中,ROB中大量连续的表项具有相同或相似的控制字段值(如连续的ALU指令具有相同的Type字段、相同的BranchMask等)。通过对连续相似表项进行游程编码(run-length encoding),可以用更少的物理表项存储更多的逻辑表项,在不增加物理ROB面积的情况下增大有效容量。代价是编码/解码逻辑的复杂度和延迟增加。

(2)虚拟ROB(Distributed/Virtual ROB)。将ROB的功能分散到多个独立的小结构中,而不使用一个集中的大结构。例如,将Done位和Exc位移到各个执行单元的本地缓冲中,只在需要提交时才将结果汇总到一个小型的提交缓冲中。这种分布式设计减小了单一大结构的面积和功耗,但增加了控制和协调的复杂度。

(3)分层ROB。使用一个小的"活跃层"(active layer)和一个大的"存储层"(storage layer)。最近分发的指令进入活跃层(小容量、低延迟、高端口数),完成执行后迁移到存储层(大容量、高延迟、低端口数)等待提交。这种分层设计可以在保持大ROB容量的同时,将高频访问操作集中在小型的活跃层上,降低功耗。

这些技术目前主要停留在学术研究阶段,商用处理器更多地依赖时钟门控和字段分离等成熟技术来优化ROB功耗。

提交阶段的案例分析

本节通过两个具体的商用处理器案例,分析ROB与提交机制的实际实现选择。

AMD Zen 4的提交与Store Buffer设计

案例研究 2 — AMD Zen 4的提交设计

AMD Zen 4微架构的提交和Store相关参数如下:

参数
ROB容量320个表项
分发宽度6 μ\muop/周期
提交宽度6 μ\muop/周期
整数PRF224个物理寄存器
浮点/向量PRF192个物理寄存器
Store Queue64个表项
L1 Data Cache写端口2

Zen 4的ROB采用PRF方式,每表项不包含Value字段。提交逻辑每周期最多提交6条μ\muop,与分发宽度匹配。对于Store指令,Zen 4使用统一的Store Queue结构——推测状态和已提交状态的Store共享同一个64表项的队列。Store提交时仅修改队列中对应表项的状态标记。已提交的Store从队列头部按顺序排出到L1 Data Cache(遵循x86 TSO模型的Store-Store顺序约束)。

Zen 4的设计中一个值得关注的细节是:当ROB中有大量已完成但等待提交的指令时(如因头部的长延迟Load而阻塞),Zen 4的提交逻辑可以在头部指令完成的同一周期就开始提交,不需要额外的一周期延迟——这种"零延迟提交"(zero-latency retirement)通过在完成标记和提交检查之间建立直接的旁路路径实现。

Apple Firestorm/Avalanche的超大ROB设计

案例研究 3 — Apple Firestorm/Avalanche的超大ROB设计

Apple的高性能核心以其极其激进的ROB容量著称。Firestorm(A14/M1, 2020)拥有约630个ROB表项,Avalanche(A15/M2, 2022)进一步扩展到约696个表项——几乎是同期Intel和AMD设计的两倍。

参数FirestormAvalanche
ROB容量\sim630\sim696
分发宽度89
提交宽度88+
整数PRF\sim380\sim420
浮点/向量PRF\sim384\sim420
Store Queue\sim108\sim114

Apple能够实现如此大的ROB,部分原因在于其采用ARM ISA(AArch64),拥有比x86更规整的指令格式和更少的微码扩展,使得ROB表项的控制字段相对紧凑。此外,Apple的芯片在面积上的约束相对宽松(移动SoC的核心面积预算较大),且TSMC先进工艺的密度优势使得大结构的面积成本可控。

Apple设计的一个独特之处在于其极高的分发和提交宽度(8–9 wide),配合大ROB和大PRF,使其在长延迟事件期间能够积累大量在飞指令而不暂停前端。这在DRAM访问密集的工作负载中表现尤为突出——Apple M系列处理器在内存延迟较高的统一内存架构下,仍然能够保持较高的IPC,大ROB功不可没。

Apple设计的功耗管理策略。如此大的ROB(696表项×\times约80位\approx6.8 KB)如何在移动平台上控制功耗?Apple采用了多种策略:(a)极细粒度的时钟门控——空闲的ROB区域完全关闭时钟;(b)TSMC N5/N4工艺的低漏电SRAM库——在面积略大的代价下将静态功耗降低30%–50%;(c)ARM ISA的规整性使ROB表项比x86更紧凑(不需要微码相关的额外字段);(d)在低负载时通过DVFS降低整个核心的电压和频率。

ROB容量与PRF容量的匹配。Apple设计中另一个值得注意的特征是PRF容量与ROB容量的匹配关系。Avalanche的整数PRF有\sim420个物理寄存器,是32个逻辑寄存器的13倍。按照前文的分析,\sim420个物理寄存器可以支持约(42032)/497(420 - 32) / 4 \approx 97个周期的平均占用时间。在696表项ROB中,假设平均IPC为5,每条指令在ROB中停留约696/5=139696/5 = 139个周期。这意味着PRF可能在ROB填满之前先耗尽——PRF容量是Apple设计中与ROB容量竞争的另一个关键瓶颈。这也解释了为什么Apple选择了如此大的PRF(\sim420),远超过同期其他设计的200左右。

香山昆明湖的ROB设计

案例研究 4 — 香山昆明湖的ROB与提交设计

香山处理器是中国科学院计算技术研究所开发的开源RISC-V高性能处理器。其第三代微架构"昆明湖"(KunMingHu,2024年流片)的ROB设计体现了RISC-V乱序处理器的典型实现选择。

参数
ROB容量256个表项
分发宽度6 μ\muop/周期
提交宽度6 μ\muop/周期
整数PRF192个物理寄存器
浮点PRF192个物理寄存器
向量PRF128个物理寄存器
Store Queue64个表项
Load Queue80个表项

昆明湖的ROB采用标准的PRF方式。一个值得关注的设计选择是其256表项ROB——相对于Apple和Intel的500+表项ROB,这是一个更保守的选择。原因有几个方面:(a)作为开源项目,设计复杂度需要在团队规模范围内可控;(b)256表项已经可以覆盖L2延迟下的大部分头阻塞场景;(c)RISC-V ISA的规整性使得ROB表项较窄,但256表项的ROB/分发宽度比仅为43,低于行业平均的50–85,在DRAM密集型工作负载中可能成为瓶颈。

昆明湖的提交逻辑采用两阶段流水线设计:第一阶段读取Done位和异常信息,第二阶段执行实际的提交操作。分支预测错误恢复使用检查点和Walk回退的混合策略——对于已提交的检查点可以快速恢复,否则需要ROB walk。

作为开源处理器,香山的ROB实现代码可以在GitHub上查阅(XiangShan/src/main/scala/xiangshan/backend/rob/),提供了一个学习乱序处理器ROB设计的宝贵参考。其Chisel(Scala嵌入式硬件描述语言)实现清晰地展示了ROB表项定义、循环队列管理、提交逻辑、异常处理和冲刷机制的具体代码结构。对于初学者而言,阅读香山的ROB代码是理解本章理论内容的最佳实践途径之一。

香山ROB的独特设计。香山昆明湖的ROB实现有几个值得注意的设计选择:(a)使用分组提交——将WcW_c条提交指令按是否有目标寄存器分组,分别处理cRAT更新和POld释放,减少关键路径上的逻辑深度;(b)异常处理采用两周期冲刷——第1周期确定异常类型并停止前端,第2周期恢复RAT和清空乱序引擎;(c)使用独立的walk状态机管理ROB Walk恢复过程,与正常提交逻辑解耦。

先进ROB设计方向

随着处理器性能需求的不断增长和工艺约束的日趋严峻,ROB设计面临着新的挑战和机遇。本节简要讨论几个先进的设计方向。

非阻塞提交

传统的顺序提交在头部指令阻塞时会导致整个ROB被填满。非阻塞提交(non-blocking commit/retire)尝试缓解这一限制,允许在某些条件下跳过头部的长延迟指令继续提交后续指令。

最具代表性的方案是Runahead执行(Runahead Execution)。当ROB头部因长延迟Load而阻塞时,处理器进入Runahead模式:标记该Load的结果为"无效"(poison),继续执行后续指令。依赖于该Load的指令产生无效结果,但独立的指令可以正常执行。特别重要的是,后续的Load指令可以提前发出预取请求,将数据预加载到Cache中。当原始的长延迟Load完成后,处理器回退到该Load的位置重新执行。

Runahead执行的效果是将ROB头阻塞期间的"空闲"时间转化为预取窗口——即使重新执行的代价使得那些指令不能提前"提交",预取效果带来的Cache命中率提升仍然可以显著提高性能。Intel Alder Lake的Golden Cove核心就实现了一种简化的Runahead机制。

Runahead的ROB实现细节。进入Runahead模式时,处理器需要保存当前的体系结构状态(head指针位置、提交RAT等),作为恢复点。然后继续分发和执行后续指令,但以下操作被修改:

  • 提交操作被伪提交替代——head指针向前推进,但不实际更新cRAT或释放物理寄存器。伪提交的目的是为新指令腾出ROB空间。

  • 依赖于被标记为"无效"的Load的指令,其结果也被标记为无效。无效标记沿数据流传播(类似NaN传播)。

  • 无效的Store不写入Store Buffer。

  • 有效的Load仍然发出Cache访问请求——这是Runahead的核心价值所在。

当阻塞的Load完成后,处理器从保存的恢复点重新开始正常执行。Runahead期间发出的Load请求可能已经将数据预加载到Cache中,使得重新执行时不再经历Cache未命中。

Runahead的IPC提升分析。Runahead的性能收益取决于两个因素:(a)Runahead期间能够触发多少有效的Cache预取——即多少独立的Load可以在Runahead模式下正常执行并发出Cache请求;(b)这些预取的数据在重新执行时是否仍然在Cache中(未被逐出)。在DRAM密集的工作负载(如数据库、图处理)中,Runahead可以带来10%–30%的IPC提升。在Cache友好的工作负载中,收益有限,因为头阻塞本身不是主要瓶颈。

连续Runahead(Continuous Runahead)。传统Runahead在阻塞Load完成后立即退出Runahead模式。一种改进方案是连续Runahead——如果重新执行后又遇到新的长延迟Load,立即再次进入Runahead模式。这种方案可以在DRAM访问序列较长时持续提供预取效果,但增加了状态管理的复杂度。

ROB与SMT的交互

在支持同步多线程(Simultaneous Multithreading, SMT)的处理器中,ROB需要在多个硬件线程之间共享或分区。常见的策略有两种:

(1)静态分区。将ROB平均分配给每个线程。例如,512表项ROB在2-way SMT中每个线程分配256表项。优点是每个线程有确定的ROB容量,不受其他线程的干扰;缺点是当某个线程空闲时,其ROB资源无法被另一个线程利用。

(2)动态共享。所有线程共享一个统一的ROB,每个线程可以占用任意数量的表项(受总容量约束)。优点是资源利用率高——单线程运行时可以使用全部ROB容量;缺点是需要防止某个线程"饿死"另一个线程——通常通过为每个线程设置最小保证容量来实现。

Intel的SMT实现(Hyper-Threading)在不同微架构中采用了不同的策略。Sandy Bridge到Skylake使用静态分区;Golden Cove转向了更灵活的动态共享方案。

在SMT中,ROB的head和tail指针需要按线程独立管理。每个线程有自己的head和tail指针,提交逻辑需要在多个线程的head之间仲裁,决定每周期提交哪个线程的指令。常见的仲裁策略包括轮转(round-robin)和基于age的优先级。

SMT下的提交带宽分配。在2-way SMT中,WcW_c个提交槽位如何在两个线程之间分配是一个设计选择:

  • 固定分配:每个线程固定获得Wc/2W_c / 2个提交槽位。优点是实现简单,公平性好;缺点是当一个线程因头阻塞而无法提交时,其分配到的提交槽位被浪费。

  • 动态分配:每周期根据两个线程的提交需求动态分配槽位。如果线程0因头阻塞无法提交,则全部WcW_c个槽位分配给线程1。优点是带宽利用率高;缺点是需要更复杂的仲裁逻辑。

  • 交替分配:偶数周期提交线程0,奇数周期提交线程1。每个线程实际获得的提交带宽为WcW_c但每2个周期才有一次提交机会。优点是实现极简,不需要多线程的提交仲裁;缺点是每个线程的最大提交吞吐量减半。

SMT下的ROB空/满判断。在动态共享的ROB中,每个线程的空闲表项数量不再由简单的head/tail比较决定——因为两个线程的表项交错在一起。一种解决方案是为每个线程维护独立的表项计数器n0n_0n1n_1,并维护总计数ntotal=n0+n1n_\mathrm{total} = n_0 + n_1。当ntotal+Wd>Rn_\mathrm{total} + W_d > R时暂停分发。为了公平性,还可以设置每个线程的最大占用量:niRmaxn_i \leq R_\mathrm{max}(如Rmax=R×3/4R_\mathrm{max} = R \times 3/4),防止一个线程独占ROB。

ROB与检查点的结合

如前文所述,现代处理器通常结合使用ROB和检查点来实现快速恢复。这种结合的设计空间包括:

(1)稀疏检查点 + ROB Walk。只在少数关键点(如分支指令、可能触发异常的指令)创建检查点,其他情况使用ROB Walk恢复。这种方案存储开销最小,但Walk恢复的延迟可能较高。

(2)密集检查点 + 快速恢复。在每条分支指令处都创建检查点(包括完整的推测RAT和空闲列表快照)。恢复时直接加载检查点,1–2个周期完成。这种方案恢复速度最快,但检查点的存储开销大——每个检查点需要存储32个物理寄存器编号(推测RAT的内容),共32×8=25632 \times 8 = 256位。如果支持16个并发未决分支,总存储为16×256=4,09616 \times 256 = 4{,}096=512= 512字节,面积开销可接受。

(3)增量检查点。不保存完整的RAT快照,而是只保存自上一个检查点以来被修改的映射项。这减少了检查点的存储开销,但恢复时需要从最近的完整检查点开始逐步应用增量更新。

现代高性能处理器大多采用方案(2),因为分支预测错误是最频繁的恢复场景,快速恢复对IPC的影响非常显著。每次分支预测错误的惩罚(front-end refill penalty)通常为12–20个周期,如果再加上多个周期的ROB Walk恢复,总惩罚可能超过30个周期——在分支密集的代码(如数据库查询、虚拟机解释器)中,这会严重降低IPC。

硬件描述 7 — 检查点存储的面积分析

对于一个支持16个并发未决分支的处理器,使用密集检查点方案需要的存储空间如下:

检查点组件每个检查点的位数16个检查点总位数
整数推测RAT快照32×8=25632 \times 8 = 2564,096
浮点推测RAT快照32×8=25632 \times 8 = 2564,096
空闲列表head指针快照8128
ROB tail指针快照10160
SQ/LQ tail指针快照2×7=142 \times 7 = 14224
分支PC和预测信息\sim801,280
合计\sim614\sim9,984

总计约10 Kbit\approx1.2 KB,在5nm工艺下面积约0.01  mm20.01\;\text{mm}^2——远小于ROB本身的面积。但检查点的关键设计挑战不在面积,而在写入带宽:每个周期可能需要创建一个新的检查点(如果该周期有分支指令被分发),这要求检查点存储支持整个RAT内容(512位)的单周期写入,需要非常高的写带宽。

一种优化是只对被修改的RAT项做增量保存。由于每条指令最多修改一个RAT项,WdW_d-wide的分发每周期最多修改WdW_d个RAT项。增量保存只需WdW_d个写端口(每端口5+8=135 + 8 = 13位,包含逻辑寄存器号和旧的物理寄存器号),远小于完整快照。恢复时需要从最近的完整检查点开始,逐步回放增量更新,恢复时间取决于累积的增量数量。

ROB的正确性验证

ROB和提交逻辑是处理器中最关键的正确性组件之一——任何ROB或提交逻辑的设计错误都可能导致体系结构状态被错误修改,产生不可恢复的静默数据损坏。因此,ROB的验证是处理器开发中最重要也最具挑战性的任务之一。

关键验证场景包括:

  1. 物理寄存器泄漏:由于提交逻辑的bug导致某个物理寄存器永远不被释放回空闲列表,最终耗尽所有物理寄存器。这种bug可能在运行数十亿条指令后才显现,极难通过随机测试发现。

  2. WAW提交冲突的错误处理:在多指令提交中,如果WAW检测逻辑有bug,可能导致错误的物理寄存器被释放或错误的映射被写入cRAT。

  3. 异常和冲刷的corner case:例如,异常发生在提交窗口的中间位置(第kk条指令有异常,但前k1k-1条已提交),或者冲刷与提交在同一周期发生。

  4. Store Buffer排出的内存一致性:在多核环境下,Store Buffer排出的顺序和时机必须符合内存一致性模型的要求,否则可能导致多核程序产生不符合规范的执行结果。

形式验证(formal verification)是验证ROB正确性的重要工具。ROB的核心不变量(invariant)可以表达为形式属性,并通过模型检查工具自动验证。关键不变量包括:

  • 物理寄存器守恒:在任何时刻,空闲列表中的物理寄存器数加上被映射引用的物理寄存器数等于NPRFN_\mathrm{PRF}

  • 映射一致性:对于任何逻辑寄存器rrcRAT[r]\mathrm{cRAT}[r]指向的物理寄存器包含rr的体系结构值。

  • 提交顺序:head指针严格按程序顺序递增,不会跳过任何表项。

  • 异常精确性:当异常被处理时,所有在异常指令之前的指令已提交,所有在异常指令及其之后的指令未修改体系结构状态。

面向超宽分发的ROB扩展

随着分发宽度向8-wide、10-wide甚至更宽发展,ROB的设计面临新的扩展性挑战:

(1)Bank数量增加。Wd=10W_d = 10的处理器需要10个bank,bank间的交叉开关面积与Wd2W_d^2成正比,10个bank的交叉开关面积约为6个bank的2.8×2.8\times

(2)提交逻辑的扇出。Wc=10W_c = 10的提交需要同时检查10个Done位的前缀与,即使使用并行前缀树,仍需log210=4\lceil\log_2 10\rceil = 4级门延迟。更大的WcW_c还增加了cRAT的写端口数和WAW检测的比较器数量((102)=45\binom{10}{2} = 45个比较器,而(62)=15\binom{6}{2} = 15个)。

(3)ROB容量需求。更宽的分发意味着ROB被填充的速度更快,需要更大的ROB才能维持相同的覆盖周期数。WdW_d从6增加到10时,维持85个周期覆盖的ROB容量需要从6×85=5106 \times 85 = 510增加到10×85=85010 \times 85 = 850个表项。

应对这些挑战的技术方向包括:分层ROB(如前文讨论的活跃层+存储层设计)、异步提交(将提交逻辑分散到多个独立单元中并行处理不同指令类型)、以及放松提交约束(如允许独立指令的乱序提交,同时维护精确异常的语义)。

本章要点回顾。

本章围绕重排序缓冲区(ROB)和提交机制,系统地讨论了乱序处理器中维护程序顺序和精确异常的核心硬件结构。现将要点总结如下:

  1. ROB表项字段的详细分析:核心字段包括Valid、Done、Exception、ExceptionCode、BranchMispred、Type、IsBranch/IsStore/IsLoad、Dst、PDst、POld、BranchMask、SQ/LQ索引、PC偏移量和微操作组标记。PRF方式下每表项约76位,ROB方式下约177位。位宽差异的根本原因是Value字段(64位)和完整PC字段(64位 vs 12位偏移量)。

  2. 循环队列实现:ROB使用head/tail指针管理的循环队列实现。空满判断有计数器法和扩展指针法两种方案,后者在硬件中更常用。分发时tail递增WdW_d,提交时head递增WcW_c'。ROB容量选择为2的幂次以简化模运算。

  3. ROB容量与性能:通过Little’s Law推导出Rmin=IPC×WavgR_\mathrm{min} = \mathrm{IPC} \times W_\mathrm{avg}。ROB容量从64增长到256可带来70%+的IPC提升;进一步增长的边际收益递减。现代处理器的ROB容量在256–696个表项之间,与分发宽度的比值约为50–85。Intel从Sandy Bridge的168增长到Golden Cove的512,反映了分发宽度增加和DRAM延迟(以周期计)增长的双重需求。

  4. 端口需求与Bank化设计:ROB的端口可分为分发写端口(WdW_d个,连续访问,可bank化)、完成标记端口(NexecN_\mathrm{exec}个,随机访问但仅1位,用解码器+或门实现)和提交读端口(WcW_c个,连续访问,可bank化)。多端口SRAM面积与(PR+PW)2(P_R + P_W)^2成正比,bank化可将6个写端口的等效面积降低97%以上。

  5. ROB方式 vs PRF方式:ROB方式将结果值存储在ROB中,需要30+个64位端口,面积约2.6 mm2^2(6-wide、256表项)。PRF方式ROB仅保留控制信息,面积约0.14 mm2^2,降低约19倍。PRF方式是现代处理器的事实标准。

  6. 物理寄存器释放:在PRF方式下,当指令提交时,其POld字段指向的旧物理寄存器被释放回空闲列表。多指令提交中的WAW冲突需要(Wc2)\binom{W_c}{2}个比较器检测。Move消除场景需要引用计数器管理。

  7. 提交的顺序约束:提交必须严格按程序顺序进行,以保证精确异常和正确的内存一致性。头阻塞是顺序提交的主要性能代价,其影响可通过Tstall=max(0,TblockR/Wd)T_\mathrm{stall} = \max(0, T_\mathrm{block} - R/W_d)估算。

  8. 提交逻辑的关键路径WcW_c个表项的连续可提交检查形成串行依赖链,朴素实现需要O(Wc)O(W_c)级门延迟。并行前缀树优化可将延迟降至O(logWc)O(\log W_c)。流水线化提交将检查和执行分为两个阶段。

  9. Store的提交与排出:Store指令提交时将数据从Store Queue标记为已提交(移入Store Buffer),由独立的排出逻辑异步写入Cache。Store具有两阶段完成(地址Done和数据Done)。排出不阻塞提交,但排出端口和Store Buffer容量可能成为Store密集场景的瓶颈。

  10. 提交时的状态更新:包括更新cRAT、释放POld到空闲列表、更新分支预测器、释放分支检查点、释放LQ/SQ表项、更新性能计数器等。每种操作对应不同的硬件路径和端口需求。

  11. ROB容量的协同约束:ROB容量不能孤立考虑,必须与PRF容量、发射队列容量、Load/Store Queue容量等其他微架构资源协同设计。增大ROB而不增大其他资源可能无法带来预期的性能收益。

  12. 特殊指令的提交处理:Fence指令可能需要排空Store Buffer,CSR写入和TLB刷新指令可能触发流水线排空。这些特殊情况增加了提交逻辑的复杂度,但在正常执行中出现频率很低(<<0.1%)。

  13. ROB冲刷机制:异常或分支预测错误触发ROB冲刷时,需要重置tail指针、恢复推测RAT到提交RAT状态、恢复空闲列表、冲刷发射队列和Load/Store Queue。完整冲刷操作需要2–3个周期。选择性冲刷(利用BranchMask精确丢弃错误路径指令)可以减少恢复代价。

  14. ROB功耗优化:512表项ROB在5nm工艺下的总功耗约72 mW,占核心功耗的2–4%。时钟门控(粗粒度和细粒度)是最主要的动态功耗优化手段。字段分离存储(Done位使用触发器阵列、主控制字段使用bank化SRAM)允许针对每个字段的访问模式独立优化。

  15. ROB与SMT:在支持SMT的处理器中,ROB可以在线程间静态分区或动态共享。动态共享的资源利用率更高,但需要防止线程饥饿。提交带宽的线程间分配策略(固定、动态、交替)影响SMT的吞吐量和公平性。

  16. 先进设计方向:Runahead执行利用ROB头阻塞期间的空闲时间进行预取,可带来10–30%的IPC提升。密集检查点配合ROB实现快速分支恢复(1–2周期),存储开销约1.2 KB。超宽分发(10-wide+)对ROB的bank数量、交叉开关面积和WAW检测比较器数量提出了新的挑战。

  17. PRF容量与ROB容量的匹配:空闲列表容量NFL=NPRFNarchN_\mathrm{FL} = N_\mathrm{PRF} - N_\mathrm{arch}需要与ROB容量匹配。在典型工作负载中,约2/3的指令有目标寄存器,平衡条件为NPRF0.67R+32N_\mathrm{PRF} \approx 0.67 R + 32。Apple Avalanche的NPRF420N_\mathrm{PRF} \approx 420R=696R = 696的匹配印证了这一关系。

ROB和提交机制是乱序处理器中"恢复程序顺序"的最后一道防线。它确保了尽管指令在引擎内部可以以任意顺序执行,对外部(操作系统、应用程序、调试器)呈现的始终是一个按程序顺序依次执行的干净抽象。这种将"内部复杂性"与"外部简洁性"分离的设计哲学,正是现代高性能处理器架构的精髓所在。

从本章的分析中可以看出,ROB设计远非一个简单的"FIFO队列"。它涉及存储技术选择(触发器 vs SRAM vs bank化SRAM)、端口架构优化(分离存储、解码器-或门网络、交叉开关)、容量-性能权衡(Little’s Law、头阻塞分析、边际收益曲线)、状态管理策略(ROB方式 vs PRF方式、检查点 vs Walk回退)、提交流水线设计(并行前缀树、多阶段流水线)、以及与其他微架构资源的协同约束(PRF、发射队列、Store Buffer)。这些设计维度的综合优化,使得ROB成为乱序处理器设计中最具工程挑战性的组件之一。

展望未来,随着分发宽度向10-wide甚至更宽发展、DRAM延迟(以周期计)因时钟频率提升而持续增加、以及能效约束日趋严格,ROB设计将面临更大的挑战。可以预见的趋势包括:

  • ROB容量继续增长,但增速可能放缓,因为面积和功耗约束更加紧张。1024表项ROB可能在未来5年内出现在高端服务器处理器中。

  • 分层ROB设计(活跃层+存储层)成为应对超大容量的技术路径,将频繁访问的操作集中在小型高速活跃层。

  • Runahead和预取技术与ROB的更紧密结合,在ROB头阻塞期间主动发起预取以减少后续的Cache未命中。

  • 新型低功耗存储技术(如eDRAM混合方案、近阈值SRAM)在大容量ROB中的应用,以在容量增长的同时控制静态功耗。

  • 异构ROB设计,为不同类型的指令(整数、浮点/向量、访存)使用不同大小和配置的ROB分区,提高面积效率。

图 图 38.2展示了ROB作为循环队列的三种核心操作:分配(指令从tail端进入)、提交(指令从head端离开)和刷新(tail回退到分支点)。Head和tail指针的追赶关系决定了ROB的占用率和可用容量。

ROB循环队列的三种操作。(a) 正常状态:Head指向最老的待提交指令,Tail指向下一个可分配的位置,绿色区域为已分配的指令。(b) 提交操作:Head向前推进,释放已提交的表项。(c) 刷新操作(分支预测失败):Tail回退到分支点,红色区域的错误路径指令被整体丢弃。注意Head永远不会超过Tail(ROB不会提交未分配的表项),Tail追上Head时ROB满,需要停止分配。
ROB循环队列的三种操作。(a) 正常状态:Head指向最老的待提交指令,Tail指向下一个可分配的位置,绿色区域为已分配的指令。(b) 提交操作:Head向前推进,释放已提交的表项。(c) 刷新操作(分支预测失败):Tail回退到分支点,红色区域的错误路径指令被整体丢弃。注意Head永远不会超过Tail(ROB不会提交未分配的表项),Tail追上Head时ROB满,需要停止分配。

ROB的顺序提交机制保证了正常执行路径上的正确性。但当事情"出错"时——分支预测失败、页错误、除零异常、外部中断——处理器必须将状态恢复到某个一致的点。第 39.0 章将讨论这些恢复机制的具体实现:从1周期完成的检查点恢复(用于高频的分支误预测),到逐条回退的WALK恢复(用于面积敏感设计),再到cRAT覆盖的完整冲刷(用于罕见的异常/中断)。恢复的速度直接决定了投机的"保险费"——恢复越快,处理器越敢于激进投机。

正文与图片:CC BY-NC-SA 4.0 · 本仓库少量站点配置代码:MIT