Skip to content

异常、中断与流水线恢复

在一颗6-wide、512-entry ROB的处理器中,一次分支mispredict意味着什么?最坏情况下,ROB中可能有500条已执行但尚未提交的投机指令需要被全部作废。恢复的速度——从检测到错误到正确路径的第一条指令被取回——直接决定了mispredict的"真实代价"。这个恢复延迟可以从3周期(快照恢复)到30周期(逐条回退)不等,两者之间10倍的差距将在高mispredict率的工作负载上转化为20%\sim30%的IPC差异。

从本书的统一视角看——处理器设计的本质是在有限的晶体管预算和功耗约束下,通过投机和并行的层层叠加来逼近指令吞吐率的理论上限——恢复机制是投机的"保险费"。投机越激进(更大ROB、更深流水线、更多未解析分支),一旦投机失败,需要丢弃的工作就越多,恢复的代价就越高。恢复延迟是投机收益的上限约束:如果恢复代价超过投机收益,处理器就不如保守执行。因此,高效的恢复机制不是投机的附属品,而是激进投机的前提条件——正是因为有了1周期检查点恢复的能力,现代处理器才敢于维护512-entry的投机窗口和16条未解析分支。

乱序执行引擎(Out-of-Order Execution Engine)的核心承诺是:无论指令以何种顺序执行,程序员观察到的结果必须与指令按程序顺序逐条执行时完全一致。这一承诺在正常执行路径上通过ROB的顺序提交来保证——前面几章已经详细讨论了这一机制。然而,处理器的执行并不总是沿着正常路径前行。分支预测失败(branch misprediction)、同步异常(synchronous exception)和异步中断(asynchronous interrupt)这三类事件,都会打断正常的指令流,要求处理器丢弃部分已经在流水线中的指令,并将微架构状态恢复到某个一致的点。如何在乱序执行的背景下高效、正确地完成这一恢复过程,是处理器设计中最精密也最关键的问题之一。

本章是第五篇"乱序执行引擎"的收官之章。前面几章分别讲解了寄存器重命名、发射队列、执行单元、存储器子系统和ROB提交,它们构成了乱序引擎的正常执行通路。本章则讨论"异常通路"——当事情出错时,处理器如何把自己修复到正确状态。我们将首先分析最高频的恢复场景——分支预测失败,然后讨论同步异常和异步中断的处理机制,最后考察指令离开流水线时的特殊约束。

从历史角度看,精确异常的支持是乱序执行从实验走向实用的关键里程碑。早期的乱序处理器(如CDC 6600)不支持精确异常,限制了操作系统在其上运行的能力。1985年Smith和Pleszkun提出的基于ROB的精确异常机制彻底解决了这一问题,使得乱序执行与现代操作系统(依赖异常进行虚拟内存管理、系统调用、调试等)完全兼容。此后的几十年中,恢复机制的优化——从简单的全冲刷到检查点恢复、从粗粒度序列化到非阻塞fence——一直是微架构创新的重要方向。

在性能方面,恢复机制的效率直接决定了处理器能够多么激进地进行推测。如果恢复代价很高,处理器就不得不保守推测,限制了IPC的提升;如果恢复代价很低(如1周期的检查点恢复),处理器可以大胆推测,在大多数情况下获得高性能,偶尔出错时快速修复。这种"推测-恢复"的博弈是现代处理器性能优化的核心范式。本章将深入分析这一范式的硬件实现细节,包括每种恢复机制的具体电路结构、位宽计算、时序约束和面积估算。

分支预测失败的处理

回顾前几章的关键结构:第 38.0 章中的ROB按程序顺序记录了所有投机指令,为恢复提供了"指令回溯轨迹";第 24.0 章中的重命名映射表(RAT)是恢复的核心目标——误预测后必须将RAT恢复到分支点的状态;第 17.0 章中的分支预测器决定了误预测的频率和模式。这三者共同构成了"预测–执行–恢复"循环的硬件基础。

分支预测失败是现代处理器中最常见的流水线恢复事件。在一个典型的高性能处理器中,每100条指令约有15–20条分支指令,其中约3%–8%的分支会预测失败,这意味着大约每200–600条指令就会发生一次预测失败。每次失败都需要丢弃错误路径上已经取指、解码甚至执行的指令,并将处理器的微架构状态恢复到分支点,然后从正确路径重新开始执行。这一过程的延迟——通常称为分支预测惩罚(Branch Misprediction Penalty)——直接影响处理器的IPC。

检测预测失败

分支预测失败的检测发生在分支指令被执行单元(Branch Resolution Unit,BRU)实际计算时。分支指令在解码和重命名阶段已经携带了前端分支预测器给出的预测信息:预测方向(taken/not-taken)和预测目标地址。当分支指令被发射到BRU执行时,BRU根据源操作数的实际值计算出真实的分支方向和真实的目标地址,然后与预测进行比较。

方向预测错误与目标预测错误

BRU需要检测两类预测错误:

  1. 方向预测错误:预测为taken但实际为not-taken,或反之。对于条件分支(如RISC-V的beqbneblt等),BRU根据两个源寄存器的值计算比较结果,得到真实的分支方向。具体而言,BRU内部的比较器对两个64位源操作数执行相等/大小/无符号比较,得到一个1位的条件结果cond_result\text{cond\_result},然后与预测方向位pred_taken\text{pred\_taken}进行异或:
dir_mispredicted=cond_resultpred_taken\text{dir\_mispredicted} = \text{cond\_result} \oplus \text{pred\_taken}

如果异或结果为1,则发生了方向预测错误。

  1. 目标预测错误:方向预测正确(或为无条件跳转),但预测目标地址与实际目标地址不一致。这种情况在间接跳转(jalr)和函数返回(ret)中更为常见,因为它们的目标地址来自寄存器值,无法从PC直接计算。BRU计算实际目标地址actual_target=rs1+imm\text{actual\_target} = \text{rs1} + \text{imm},然后与预测目标pred_target\text{pred\_target}逐位比较:
tgt_mispredicted=(actual_targetpred_target)branch_taken\text{tgt\_mispredicted} = (\text{actual\_target} \neq \text{pred\_target}) \wedge \text{branch\_taken}

目标预测错误只在分支实际被taken的情况下才有意义——如果分支not-taken,则不存在"跳到哪里"的问题。

综合两类错误,BRU产生的最终误预测信号为:

mispredicted=dir_mispredictedtgt_mispredicted\text{mispredicted} = \text{dir\_mispredicted} \lor \text{tgt\_mispredicted}

BRU产生的恢复信号

检测到预测失败后,BRU需要通知处理器的多个模块采取恢复动作。BRU产生的信号通常包括:

  • mispredict(1位):指示发生了预测失败。

  • correct_target(64位):正确的分支目标地址,用于前端重定向。对于方向预测错误且实际为not-taken的情况,正确目标为PCbranch+4\text{PC}_{\text{branch}} + 4(或+2+2对于压缩指令)。

  • correct_taken(1位):正确的分支方向,用于更新分支预测器。

  • rob_indexlog2NROB\lceil\log_2 N_{\text{ROB}}\rceil位,典型为8–9位):该分支指令在ROB中的索引,用于确定需要清除的指令范围。

  • checkpoint_idlog2C\lceil\log_2 C\rceil位):如果使用检查点恢复机制,该分支对应的检查点编号。

  • branch_mask_bitlog2M\lceil\log_2 M\rceil位):该分支指令在分支掩码中占用的位编号,用于一次性清除错误路径上的所有指令。

  • branch_history(BHR位宽,典型16–64位):分支执行时记录的分支历史信息,用于更新全局/局部分支历史寄存器。

以一个具体的配置为例:ROB容量512项、检查点16个、分支掩码16位,则BRU的恢复接口总线宽度为1+64+1+9+4+4+32=1151 + 64 + 1 + 9 + 4 + 4 + 32 = 115位。

设计提示

在某些微架构中,条件分支的方向预测错误可以在ALU阶段的更早时刻被检测到——例如,如果分支条件依赖的比较操作可以在数据就绪后的第一个周期内完成,则方向错误的检测不需要等待完整的BRU执行流水线。Intel从Haswell开始采用的"Branch condition early resolution"优化就利用了这一点,将部分条件分支的误预测惩罚减少了1–2个周期。

分支解析的时序分析

分支指令的解析时间取决于其源操作数的就绪时刻和BRU的执行延迟。对于大多数条件分支,BRU执行延迟为1个周期——它只需要进行一个整数比较(如相等、大小比较)并计算目标地址(PC + 偏移量)。然而,源操作数的就绪时间可能有很大的变化:如果分支条件依赖一条ALU指令的结果(1个周期延迟),则分支可以在ALU指令完成后的下一个周期解析;但如果分支条件依赖一条cache miss的load指令(可能需要几十到上百个周期),则分支的解析被大幅延迟——在此期间,处理器继续沿着预测的路径取指和执行,错误路径上的指令可能已经积累了很多。

考虑以下具体场景来理解解析延迟的影响:

  1. 最优情况:分支条件依赖于前一条ALU指令的结果。ALU指令在发射后1个周期产生结果,分支指令在下一个周期被发射并在1个周期后解析。从分支进入发射队列到解析完成,约2–3个周期。此时错误路径上可能只有6–18条指令(取决于取指宽度和前端延迟)。

  2. 一般情况:分支条件依赖于一条L1 D-Cache命中的load指令。Load延迟约4个周期,加上分支自身1个周期执行延迟,总延迟约5个周期。错误路径上可能积累20–30条指令。

  3. 最坏情况:分支条件依赖于一条L3 Cache命中的load指令(约40个周期延迟)或DRAM访问(约200个周期延迟)。此时错误路径上可能积累了上百条指令,它们占用了ROB、发射队列、物理寄存器等宝贵资源,可能导致正确路径上的先序指令因资源不足而无法被分配或执行。

多分支同时解析

在乱序执行的处理器中,ROB中可能同时存在多条未解析的分支指令,其中任何一条都可能预测失败。如果在同一周期内有多条分支同时解析并且都检测到预测失败,处理器需要选择程序顺序上最早的那一条作为恢复目标——因为更晚的分支本身处于更早分支的错误路径上,会被一起清除。

硬件描述 1 — 多分支同时解析的处理

在一个拥有多个BRU的超标量处理器中(例如ARM Neoverse V2拥有2个分支执行端口),多条分支可能在同一周期内同时解析。当多条分支同时检测到预测失败时,处理器需要通过比较它们的ROB索引来确定程序顺序上最早的那条分支。在循环ROB中,这一比较需要考虑指针回绕(wrap-around):如果ROB的head指针为HH,两条分支的ROB索引分别为iijj,则程序顺序上更早的一条满足(iH)modN<(jH)modN(i - H) \mod N < (j - H) \mod N,其中NN为ROB的表项数。

只有程序顺序上最早的预测失败分支会触发恢复动作,其他较晚的分支会在恢复过程中被一起清除。这一选择逻辑通常实现为一个优先级编码器(priority encoder),对所有BRU报告的mispredict信号按ROB年龄进行排序。

对于拥有2个BRU的处理器,年龄比较器的实现如下:

  1. 计算每条分支相对于ROB head的"年龄距离":di=(iH)modNd_i = (i - H) \mod N

  2. 比较两个年龄距离:如果di<djd_i < d_j,则分支ii更老。

  3. 选择更老的分支作为恢复目标;如果只有一条预测失败,则直接选择该分支。

在硬件上,模NN减法可以用log2N\lceil\log_2 N\rceil位无符号减法器实现(当NN是2的幂时直接截断高位即可),年龄比较用一个log2N\lceil\log_2 N\rceil位比较器。整个选择逻辑的面积和延迟很小,可以在1个周期内完成。

预测正确的分支。需要强调的是,大多数分支的预测是正确的——预测正确的分支在BRU解析后,只需执行少量簿记操作:清除分支掩码中对应的位、释放检查点(如果使用检查点恢复)、更新分支预测器的训练信息。这些操作的开销很小,不影响后续指令的执行。

前端重定向

检测到分支预测失败后,处理器需要立即对前端进行重定向(redirect),使其从正确的地址开始取指。这一过程涉及多个前端模块的协调操作。

PC重定向

BRU将计算出的正确目标地址发送给前端的取指单元(Instruction Fetch Unit,IFU)。IFU将PC寄存器更新为该地址,从下一个周期开始从正确地址取指。对于方向预测错误的情况:

  • 如果预测为taken但实际为not-taken,正确的PC是分支指令的下一条指令的地址(PC+4或PC+2对于压缩指令)。

  • 如果预测为not-taken但实际为taken,正确的PC是分支目标地址。

需要注意的是,correct_target信号从BRU(位于后端执行区域)传播到IFU(位于前端取指区域),在物理布局上可能跨越相当的距离。在高频设计中,这条跨模块的64位总线可能需要插入一级流水线寄存器(pipeline register),增加1个周期的延迟。这也是分支预测惩罚中"信号传播延迟"的重要组成部分。

清空取指缓冲与解码流水线

前端流水线中已经取回但尚未进入后端的指令都属于错误路径,必须全部丢弃。这包括:

  • 指令缓冲(Instruction Buffer / Fetch Queue)中的所有待解码指令。

  • 解码流水线各级中的指令。

  • 微码ROM(Micro-op ROM)中正在展开的微操作序列(如果有的话)。

清空操作在硬件上通常通过全局冲刷信号(global flush signal)实现:一根flush线连接到前端所有流水线寄存器的复位端口,当该信号有效时,所有流水线寄存器在下一个时钟沿被清零(或设置为无效状态)。对于指令缓冲这样的FIFO结构,冲刷操作只需将head和tail指针复位为相同值即可,不需要逐条清除每个条目——条目中的旧数据在后续写入时会被自然覆盖。

更新分支预测器

前端的分支预测器需要利用此次预测失败的信息进行自我修正。BRU将正确的方向和目标地址回传给分支预测器,用于更新方向预测器(如TAGE)的表项、BTB(Branch Target Buffer)的条目,以及在必要时更新RAS(Return Address Stack)。

方向预测器的更新。BRU回传的信息包括分支PC、正确方向和分支历史。TAGE预测器的更新涉及:(1)更新命中表项的饱和计数器(朝着正确方向递增或递减);(2)如果预测错误,可能在更长历史的表中分配新条目以捕获此模式。这一更新过程通常在1–2个周期内完成,但由于TAGE使用多个tag表(通常4–6个),更新操作需要多端口或多周期序列化访问。

BTB的更新。如果目标预测错误(或分支不在BTB中),需要将正确的目标地址写入BTB。BTB更新涉及一次tag匹配和一次数据写入,通常在1个周期内完成。

RAS的恢复。对于RAS,预测失败的处理尤为微妙:如果错误路径上的指令包含了callret指令,它们可能已经错误地推入或弹出了RAS条目。恢复时需要将RAS恢复到分支点的状态。一种常见的实现是为RAS也维护检查点——在每条分支指令处保存RAS的栈顶指针(Top-of-Stack, TOS),预测失败时恢复。如果RAS有32个条目,TOS指针为5位,则每个检查点需要额外保存5位的TOS信息。另一种更激进的方案是在每条分支指令处保存整个RAS的内容(如MIPS R10000),但对于较大的RAS(32–64条目×\times64位地址),这种方案的存储开销过大。

全局分支历史的恢复。如果分支预测器使用全局历史寄存器(Global History Register, GHR),则错误路径上的分支指令可能已经将错误的方向移入了GHR。恢复时需要将GHR回退到预测失败的分支之前的状态。一种高效实现是为每条分支指令在解码时保存当前GHR的快照,预测失败时用该快照恢复GHR,然后将正确方向移入。如果GHR为64位,则每条分支需要保存64位的历史快照——这通常记录在分支指令的ROB条目或专用的分支历史缓冲中。

硬件描述 2 — 分支预测器恢复的完整信号流

分支预测器的恢复涉及多个预测器组件的协调更新。以下列出从BRU产生mispredict信号到预测器完成更新的完整信号流:

周期T(BRU执行完成)

  • BRU产生信号:{mispredict,correct_taken,correct_target,branch_pc,branch_type}\{\text{mispredict}, \text{correct\_taken}, \text{correct\_target}, \text{branch\_pc}, \text{branch\_type}\}

  • BRU同时产生ROB年龄信息,用于多分支冲突仲裁。

周期T+1(信号传播到前端)

  • PC寄存器被更新为correct_target。

  • Fetch Buffer被清空(head=tail)。

  • GHR被恢复为分支点的快照,然后将correct_taken移入。

  • RAS的TOS指针被恢复为分支点保存的值。

  • 分支预测器训练端口接收更新请求。

周期T+1到T+3(预测器更新)

  • TAGE预测器:使用branch_pc和恢复后的GHR计算各表的index,更新命中表项的计数器。如果预测错误,在更长历史的表中分配新条目。这可能需要2–3个周期(多表串行更新或多端口并行更新)。

  • BTB:使用branch_pc和correct_target更新目标缓存。1周期完成。

  • 分支类型表:如果分支类型(条件/无条件/间接/返回)预测错误,更新相应条目。

周期T+2到T+4(正确路径取指开始)

  • I-Cache接收新PC,开始取指访问。如果I-Cache命中,约3–4周期后第一批正确路径指令到达解码器。

需要注意的是,预测器更新和取指可以并行进行——前端不需要等待预测器更新完成就可以开始取指。新取回的指令使用更新后的预测器进行预测(如果更新在取指之前完成)或使用旧的预测器(如果更新较慢)。后者可能导致短暂的预测精度下降,但影响很小。

I-Cache未命中的影响

一个值得注意的实际问题是:正确路径的目标地址对应的指令可能不在I-Cache中。在这种情况下,前端需要等待I-Cache的缺失处理(fetch miss)完成,从L2 Cache甚至更低层级获取指令。这一等待时间可能达到几十到上百个周期,将分支预测惩罚从典型的10–20个周期显著延长。在含有大量间接跳转的代码中(如虚函数调用、解释器的dispatch循环),目标地址的I-Cache未命中率可能较高,成为性能的重要瓶颈。预取机制(如Next-Line Prefetch、FDIP等)可以部分缓解这一问题。

性能分析 1 — 前端重定向延迟的量化分析

前端重定向延迟(redirect latency)是分支预测惩罚的重要组成部分。从BRU检测到预测失败到第一条正确路径指令可以被发射执行,整个延迟可以分解为以下阶段:

阶段操作内容延迟(周期)
BRU执行完成比较结果、产生mispredict信号0(与执行同周期)
信号传播重定向信号从后端到前端1
I-Cache访问从正确地址取指3–5
指令解码解码取回的指令1–2
指令重命名寄存器重命名和分配1–2
进入发射队列指令等待操作数就绪1
总计7–11

因此从分支执行完成到第一条正确路径指令可以被发射执行,整个延迟约为10–20个周期(含BRU自身的等待延迟),具体取决于流水线深度。在一个6-wide的处理器中,如果每200条指令发生一次预测失败,每次惩罚15个周期,则IPC损失约为:

ΔIPCW×Pmiss×Cpenalty1+Pmiss×Cpenalty6×0.005×151+0.005×150.42\Delta \mathrm{IPC}\approx \frac{W \times P_{\text{miss}} \times C_{\text{penalty}}}{1 + P_{\text{miss}} \times C_{\text{penalty}}} \approx \frac{6 \times 0.005 \times 15}{1 + 0.005 \times 15} \approx 0.42

其中Pmiss=1/200=0.005P_{\text{miss}} = 1/200 = 0.005为每条指令的预测失败率,Cpenalty=15C_{\text{penalty}} = 15为惩罚周期数。这意味着预测失败导致大约7%–10%的IPC损失——这也是分支预测器设计如此重要的原因。

后端的清空与恢复

前端重定向只解决了"从哪里重新取指"的问题。后端的恢复则更加复杂:需要清除错误路径上已经进入乱序引擎的所有指令,并将微架构的重命名状态恢复到分支指令刚完成重命名时的状态。

后端恢复涉及的微架构结构包括:

ROB清除

ROB中在分支指令之后分配的所有表项都属于错误路径,需要被作废。如果分支指令位于ROB索引bb处,则所有索引在bb之后(按循环队列顺序)、直到尾指针TT的表项都需要被清除。具体操作是将ROB的尾指针直接移动到b+1b+1的位置——这一操作在1个周期内即可完成,被清除的表项在后续分配时会被自然覆盖。

需要清除的指令数量为:

Nflush=(Tb1)modNROBN_{\text{flush}} = (T - b - 1) \mod N_{\text{ROB}}

其中NROBN_{\text{ROB}}是ROB的容量。在一个512项的ROB中,NflushN_{\text{flush}}的范围从0(分支刚好在尾指针之前)到511(ROB几乎全满且分支在head附近),但典型值为30–100条指令。

发射队列清除与分支掩码机制

发射队列(Issue Queue / Reservation Station)中等待发射或正在执行的错误路径指令也需要被清除。每条发射队列条目都标记了其对应的ROB索引,处理器通过分支掩码机制来实现高效的一次性清除。

分支掩码(Branch Mask)是一种精巧的硬件机制,它允许处理器在1个周期内清除所有属于某条预测失败分支之后的指令,无需逐条扫描。其工作原理如下:

  1. 掩码位分配:每条分支指令在重命名阶段被分配一个唯一的掩码位编号kk0k<M0 \le k < M,其中MM是最大同时未解析分支数,典型值为8–16)。掩码位从一个空闲掩码位池中分配,类似于物理寄存器的分配。

  2. 掩码传播:在分支BkB_k之后分发的所有指令都在其branch mask向量的第kk位标记为1。具体实现是:重命名阶段维护一个"当前活跃分支掩码"(active branch mask),每当分配一条新的分支指令并占用第kk位时,将该掩码的第kk位置1。此后所有新分发的指令都继承这个活跃分支掩码的当前值作为自己的branch mask。

  3. 预测正确时的清除:当分支BkB_k被BRU解析为预测正确时,所有条目的branch mask中第kk位被清零。这不影响指令的有效性,只是表示这些指令不再"依赖于"分支BkB_k的推测结果。同时掩码位kk被释放回空闲池。

  4. 预测失败时的清除:当分支BkB_k被检测到预测失败时,BRU广播kk值。发射队列中每条条目执行以下逻辑:

flushi=validibranch_maski[k]\text{flush}_i = \text{valid}_i \wedge \text{branch\_mask}_i[k]

所有flushi=1\text{flush}_i = 1的条目被立即无效化(将valid位清零)。这一操作在1个周期内完成,因为它只是对每条条目的一个位进行检测——硬件上是一个简单的AND门阵列,扇入为1。

分支掩码的分配与清除过程。分支$B_0$占用bit0,$B_2$占用bit2。$B_0$预测失败后,所有branch mask[0]=1的指令(I1、$B_2$、I2、I3)被一次性清除。
分支掩码的分配与清除过程。分支$B_0$占用bit0,$B_2$占用bit2。$B_0$预测失败后,所有branch mask[0]=1的指令(I1、$B_2$、I2、I3)被一次性清除。

分支掩码的位宽与性能约束。分支掩码的位宽MM等于处理器同时支持的最大未解析分支数量。当所有MM个掩码位都被占用时,新的分支指令必须暂停分配,等待某条分支被解析释放掩码位。因此MM的选择是面积与性能的权衡:

  • 每条发射队列条目需要MM位的branch mask存储。如果发射队列有96个条目且M=16M = 16,则branch mask的总存储为96×16=153696 \times 16 = 1536位。

  • 此外,ROB的每个条目也需要MM位的branch mask(用于WALK恢复时的清除),512×16=8192512 \times 16 = 8192位。

  • Load Queue和Store Queue的每个条目同样需要MM位。

  • MM值越大,因掩码位耗尽而暂停的概率越低,但面积开销线性增长。

典型的高性能处理器选择M=8M = 81616。在SPECint基准测试中,M=8M = 8大约导致0.1%–0.5%的周期因掩码位耗尽而暂停,M=16M = 16则几乎消除了这一暂停。

硬件描述 3 — 分支掩码清除的硬件实现

分支掩码清除的硬件实现涉及三个关键组件:

(1)掩码位分配器。掩码位分配器管理MM个掩码位的分配和释放。它维护一个MM位的"可用掩码"(available mask)寄存器,每位指示对应的掩码位是否空闲。当一条分支指令进入重命名阶段时,分配器使用优先级编码器从可用掩码中选择编号最低的空闲位,将该位清零(标记为已分配),并将位编号作为分支指令的mask_bit字段写入ROB。

分配器的逻辑可以用如下伪代码描述:

  1. avail_bitPriorityEncode(avail_mask)\text{avail\_bit} \leftarrow \text{PriorityEncode}(\text{avail\_mask})

  2. avail_mask[avail_bit]0\text{avail\_mask}[\text{avail\_bit}] \leftarrow 0

  3. active_mask[avail_bit]1\text{active\_mask}[\text{avail\_bit}] \leftarrow 1

  4. avail_bit\text{avail\_bit}写入分支指令的ROB条目

当所有MM位都已分配时(avail_mask=0\text{avail\_mask} = 0),分配器产生stall信号,暂停后续分支指令的分配。

(2)掩码广播网络。当BRU报告分支BkB_k的解析结果时,掩码位kk被广播到所有使用branch mask的结构:

  • 如果预测正确:广播"clear bit kk"信号,所有条目的branch_mask[kk]位被清零。

  • 如果预测失败:广播"kill on bit kk"信号,所有branch_mask[kk]=1的条目被无效化。

这两种广播操作的硬件实现几乎相同——区别只在于:clear操作修改branch_mask位但保留条目的valid位,kill操作根据branch_mask位来清除valid位。对于发射队列中的96个条目,kill操作的逻辑为:

validivalidibranch_maski[k]i[0,95]\text{valid}_i \leftarrow \text{valid}_i \wedge \overline{\text{branch\_mask}_i[k]} \quad \forall i \in [0, 95]

这是96个并行的AND门操作,延迟仅为1个门延迟,可以轻松在1个周期内完成。

(3)活跃掩码继承逻辑。重命名阶段维护一个MM位的"活跃分支掩码"(active branch mask),反映当前所有未解析分支的掩码位。每条新分发的非分支指令继承当前的active_mask作为自己的branch_mask:

branch_masknew_instactive_mask\text{branch\_mask}_{\text{new\_inst}} \leftarrow \text{active\_mask}

每条新分发的分支指令也继承active_mask,但它自身占用的位(如位kk包含在自己的branch_mask中——因为它不在自己的错误路径上。分支预测失败时,分支指令本身被保留(它需要更新预测器等簿记操作),只有它之后的指令被清除。

物理寄存器释放

错误路径上的指令在重命名阶段分配了物理寄存器,这些物理寄存器需要被回收到空闲列表(free list)中。释放操作的具体方式取决于所采用的恢复机制——检查点恢复或WALK恢复。在检查点恢复中,空闲列表通过检查点中保存的头指针回退来隐式释放寄存器;在WALK恢复中,每撤销一条指令的重命名操作,该指令分配的新物理寄存器被显式推入空闲列表。

Load/Store队列清除

Load Queue和Store Queue中属于错误路径的条目也需要被清除。Store Queue的清除相对简单,因为store指令在提交前不会将数据写入缓存,只需要移动Store Queue的尾指针即可。Load Queue的清除也类似,但如果某些推测性的load已经从缓存中读取了数据并通过旁路网络转发给了后续指令,则需要确保这些后续指令也在恢复范围内——这由分支掩码机制自然保证。

RAT恢复——后端恢复的核心挑战

寄存器别名表(Register Alias Table,RAT)记录了每个逻辑寄存器到物理寄存器的最新映射。错误路径上的指令修改了RAT——它们分配了新的物理寄存器并更新了RAT中的映射。恢复时需要将RAT恢复到分支点的状态,即分支指令刚完成重命名后、但错误路径上第一条指令尚未修改RAT之前的映射。

RAT恢复有三种机制:检查点恢复(Checkpoint Recovery)、WALK恢复(Walking Recovery)和架构状态恢复(Architecture State Recovery),下面分别详细讨论。

分支预测失败后的ROB恢复过程。错误路径上的指令(I4--I6)被清除,尾指针回退到分支指令之后的位置。
分支预测失败后的ROB恢复过程。错误路径上的指令(I4--I6)被清除,尾指针回退到分支指令之后的位置。

检查点恢复的实现

检查点恢复(Checkpoint Recovery)是一种以空间换时间的RAT恢复策略。其核心思想是:在每个可能导致恢复的指令(主要是分支指令)被重命名时,保存一份RAT的完整快照(snapshot),即将整个映射表复制到一个检查点寄存器中。当该分支被检测到预测失败时,直接用保存的快照覆盖当前的RAT,在1个时钟周期内完成恢复。

检查点的存储内容

一个检查点需要保存的信息包括:

(1)RAT映射。每个逻辑寄存器对应的物理寄存器编号。对于RV64(32个整数逻辑寄存器 + 32个浮点逻辑寄存器),如果物理寄存器编号为8位(最多256个物理寄存器),则RAT映射部分需要64×8=51264 \times 8 = 512位。如果整数和浮点使用分离的物理寄存器堆(如64个整数物理寄存器 + 64个浮点物理寄存器),则编号只需6位,映射部分为64×6=38464 \times 6 = 384位。

(2)空闲列表状态。物理寄存器空闲列表的头指针或完整的空闲位图。恢复RAT后,空闲列表也需要恢复到分支点的状态,否则某些物理寄存器可能被遗漏或重复分配。如果空闲列表实现为循环FIFO队列,只需保存头指针(log2P\lceil\log_2 P\rceil位,如8位)。如果实现为位图,需要保存完整的PP位位图(如256位)。

(3)队列指针。ROB尾指针、Store Queue尾指针、Load Queue尾指针等,用于恢复各结构到分支点的状态。这些指针各需log2N\lceil\log_2 N\rceil位。

(4)分支掩码活跃状态。当前活跃的分支掩码位向量(MM位),用于恢复后正确继承活跃分支信息。

空闲列表的恢复策略

空闲列表的恢复是检查点恢复中容易被忽视但至关重要的部分。这里有两种实现方案,恢复策略各不相同。

FIFO方案。如果空闲列表实现为循环FIFO队列(头指针指向下一个可分配的物理寄存器),则检查点只需保存头指针即可——恢复时将头指针回退到检查点记录的位置。这依赖于一个关键前提:已分配但属于错误路径的物理寄存器在空闲列表的FIFO中恰好位于检查点头指针之后。

具体而言,FIFO空闲列表的指针关系如下:

  • HfreeH_{\text{free}}:空闲列表头指针,指向下一个将被分配的物理寄存器。

  • TfreeT_{\text{free}}:空闲列表尾指针,指向下一个将被回收的位置。

  • 检查点kk保存的头指针为HkH_k

  • 恢复时,将HfreeHkH_{\text{free}} \leftarrow H_k,则HkH_k到当前HfreeH_{\text{free}}之间的物理寄存器自动"归还"到空闲列表中。

这种方案的优点是恢复极快(只需写一个指针),缺点是要求物理寄存器的分配和释放严格遵循FIFO顺序——这在正常提交时由ROB的顺序提交机制保证,但在恢复场景中需要特别设计以避免指针错位。

位图方案。如果空闲列表实现为位图(bitmap)——每位对应一个物理寄存器的空闲状态——则检查点需要保存完整的位图。对于256个物理寄存器,位图为256位,16个检查点共需16×256=409616 \times 256 = 4096位的额外存储。这也是为什么许多实现选择FIFO而非位图来管理空闲列表——FIFO不仅分配速度快(每周期从头部弹出WW个物理寄存器),而且检查点的存储开销更小(只需保存一个指针而非完整位图)。

检查点缓冲区的结构与管理

检查点存储在一个固定大小的检查点缓冲区(Checkpoint Buffer)中,该缓冲区通常实现为循环队列,容量为CC个检查点。

检查点的分配流程。每当一条分支指令进入重命名阶段时:

  1. 从检查点缓冲区中分配一个空闲条目,获得检查点编号kk

  2. 将当前推测RAT的全部映射关系复制到检查点kk的存储中。这是一个宽度为L×log2PL \times \lceil\log_2 P\rceil位的并行复制操作(LL为逻辑寄存器数量)。

  3. 将空闲列表的当前头指针记录到检查点kk中。

  4. 将ROB、Store Queue、Load Queue的当前尾指针记录到检查点kk中。

  5. 将检查点编号kk作为分支指令的元数据,随指令在流水线中传播。

检查点的释放。检查点释放的时机需要谨慎处理:

  1. 分支正确解析:当BRU报告分支BkB_k预测正确时,检查点kk可以被释放——但前提是BkB_k之前没有更老的未解析分支。如果存在更老的未解析分支BjB_jj<kj < k),而BjB_j尚未解析,则BkB_k的检查点暂时不能释放,因为BjB_j可能预测失败,此时需要恢复到BjB_j的检查点,而BkB_k的检查点在BjB_j的恢复范围内也需要被清除。

  2. 简化策略:一种实现上更简单的策略是在分支指令提交时才释放检查点。提交时该分支之前的所有指令已经安全提交,不会再有更老的分支触发恢复。缺点是检查点的占用时间更长,可能导致检查点缓冲区更早耗尽。

  3. 折中策略:当分支正确解析且它是检查点缓冲区中最老的检查点时,可以立即释放。这通过比较检查点年龄(检查点缓冲区的head指针)来实现。

检查点耗尽时的处理。由于检查点缓冲区的容量CC有限,当所有检查点都已分配时,新的分支指令无法获得检查点。此时处理器必须暂停分配(stall allocation),等待某个检查点被释放后才能继续处理新的分支指令。这一暂停会严重影响性能——特别是在分支密集的代码中。

硬件描述 4 — 检查点缓冲区的面积开销

检查点缓冲区的面积开销主要取决于检查点的数量CC和每个检查点的大小SS。以一个RV64处理器为例:

参数计算
RAT映射(整数+浮点)64×864 \times 8 bits512 bits
空闲列表头指针log2256\lceil\log_2 256\rceil bits8 bits
ROB尾指针log2512\lceil\log_2 512\rceil bits9 bits
Store Queue尾指针log264\lceil\log_2 64\rceil bits6 bits
Load Queue尾指针log2128\lceil\log_2 128\rceil bits7 bits
分支掩码活跃状态MM bits16 bits
GHR快照64 bits64 bits
单个检查点总计\approx622 bits
检查点数量CC8–16
缓冲区总容量622×16622 \times 16\approx9952 bits \approx 1.22 KB

从面积上看,16个检查点约需1.2 KB的存储,在现代工艺下面积开销较小。但检查点缓冲区的真正复杂性在于它的写端口——每个周期需要同时将整个RAT(512位)写入一个检查点,这要求极宽的写总线和高扇出的驱动能力。此外,恢复时需要将一个检查点的512位在1个周期内写入RAT,要求同等宽度的读端口。这些宽端口在物理布局上带来较大的导线拥塞和时序压力。

在物理设计中,检查点缓冲区通常被放置在RAT附近以减少互联延迟。一种优化是将检查点缓冲区实现为register file而非SRAM——虽然面积稍大,但读写速度更快,更适合1周期恢复的时序要求。

检查点写入的时序挑战。在一个6-wide处理器中,最坏情况下一个周期内可能有6条分支指令同时被重命名(虽然概率极低)。但由于检查点缓冲区通常只有1个写端口(每周期最多保存1个检查点),如果多条分支在同一周期被重命名,需要序列化检查点的保存操作——后续分支指令暂停等待检查点写入完成。实际中,由于代码中分支指令的密度有限(平均每5–7条指令才有1条分支),6条分支同时被重命名的情况极为罕见。

恢复路径的关键路径分析。检查点恢复的1周期延迟约束意味着以下操作必须在一个时钟周期内完成:

  1. BRU的mispredict信号传播到检查点缓冲区的地址端口(信号传播延迟)。

  2. 检查点编号kk驱动CC选1的多路选择器,选出正确的检查点(MUX延迟)。

  3. 选中的检查点数据(512位)从缓冲区读出并写入sRAT的写端口(读延迟+互联延迟)。

  4. sRAT完成写入(写延迟)。

在一个3 GHz(周期时间约0.33 ns)的处理器中,上述操作链的总延迟必须小于0.33 ns。如果16选1 MUX的延迟约为0.05 ns,register file读取延迟约为0.08 ns,互联和建立时间约为0.1 ns,则总延迟约为0.23 ns,有约30%的时序余量。这解释了为什么检查点恢复在现代高频处理器中是可行的,但也说明了其时序紧张的程度。

恢复过程的详细步骤

当BRU检测到分支BkB_k预测失败时,检查点恢复执行以下操作(均在1个时钟周期内完成):

  1. RAT恢复:从检查点缓冲区中读出检查点kk的RAT快照,将其写入推测RAT,覆盖当前映射。硬件上,这是一个512512位的并行多路选择器(MUX)操作:CC个检查点通过检查点编号kk选择一个,输出直接连接到推测RAT的写端口。

  2. 空闲列表恢复:将空闲列表的头指针恢复到检查点kk记录的值。这意味着在检查点kk之后分配的所有物理寄存器自动"归还"。

  3. ROB尾指针恢复:将ROB尾指针设置为检查点kk记录的值(即分支BkB_k之后的ROB位置)。

  4. LSQ尾指针恢复:将Store Queue和Load Queue的尾指针恢复到检查点kk记录的值。

  5. 发射队列清除:通过分支掩码广播第kk位,一次性清除发射队列中所有属于错误路径的条目。

  6. 检查点释放:释放检查点kk以及所有在BkB_k之后分配的检查点(它们都属于错误路径)。在循环队列实现中,这只需将检查点缓冲区的尾指针回退到k+1k+1的位置。

  7. 分支掩码恢复:将活跃分支掩码恢复到检查点kk记录的状态,释放BkB_k及其后所有分支占用的掩码位。

检查点恢复的核心优势在于恢复延迟恒定为1个周期,不受错误路径上指令数量的影响。即使错误路径上已经执行了100条指令并修改了RAT中的大量映射,恢复仍然只需要1个周期——因为完整的正确映射已经保存在检查点中。

检查点恢复的具体数值例子。以一个具体场景说明检查点恢复的完整过程。假设一个RV64处理器,32个整数逻辑寄存器,128个物理寄存器,16个检查点。当前推测RAT的状态为:x0\toP0(硬连线零),x1\toP45,x2\toP67,x3\toP102,...。

分支指令BR在重命名时分配检查点k=3k=3,此时检查点3保存了当前sRAT的完整快照:{x0\toP0, x1\toP45, x2\toP67, x3\toP102, ...},以及空闲列表头指针H3=47H_3 = 47、ROB尾指针T3=180T_3 = 180等。

BR之后,错误路径上的50条指令陆续修改了sRAT。例如,某条指令将x1重映射为P88,另一条将x3重映射为P91,如此等等。此时sRAT已经面目全非。

当BRU检测到BR预测失败时,恢复过程在1个周期内完成:

  1. 检查点缓冲区用k=3k=3索引,读出512位的RAT快照。

  2. 快照被并行写入sRAT的全部32个条目(每条目7位,共32×7=22432 \times 7 = 224位整数部分)。

  3. 空闲列表头指针被重置为H3=47H_3 = 47。这意味着在H3H_3之后分配的所有物理寄存器(P47, P48, ...直到当前头指针位置)自动回到空闲状态。

  4. ROB尾指针回退到T3=180T_3 = 180

  5. 分支掩码广播:所有branch_mask中第3位为1的发射队列条目被清除。

下一个周期,前端开始从正确目标地址取指,sRAT已经恢复到分支点的正确状态,新指令可以正确地使用sRAT进行重命名。

案例研究 1 — MIPS R10000的检查点恢复

MIPS R10000(1996年)是最早采用完整检查点恢复机制的商用处理器之一。R10000使用统一PRF方案进行寄存器重命名,拥有64个物理寄存器(逻辑寄存器为32个整数 + 32个浮点),采用4个检查点来支持4条未解析的in-flight分支。

检查点内容。R10000的每个检查点保存:

  • 整数映射表:32×6=19232 \times 6 = 192位(32个逻辑寄存器,每个映射到64个物理寄存器之一,编号需6位)。

  • 浮点映射表:32×6=19232 \times 6 = 192位。

  • 空闲列表状态:使用位图方式,6464位整数 + 6464位浮点 = 128128位。

  • 总计每个检查点约512512位。

4个检查点的限制。R10000将检查点数量限制为4——这意味着当4个检查点全部被未解析分支占用时,后续分支指令必须暂停在重命名阶段。在分支密集的代码中(如条件判断嵌套、switch-case展开),这一限制可能导致显著的性能损失。分析显示,在SPECint95基准测试中,约2%–5%的周期因检查点耗尽而暂停。

空闲列表的位图恢复。R10000的一个独特之处在于它使用位图(而非FIFO)管理空闲列表。每个检查点保存一份64位的空闲位图。恢复时,处理器将检查点中的位图直接写入空闲列表,精确恢复每个物理寄存器的空闲/占用状态。这种方案的正确性不依赖于分配顺序,更加鲁棒,但每个检查点需要额外的128128位存储(整数+浮点各64位)。

后续的MIPS R12000将检查点数量增加到8个以缓解这一瓶颈。

现代处理器的检查点数量通常在8–16个之间。例如,ARM Cortex-A77使用8个检查点,而某些RISC-V开源核心(如香山处理器)使用基于检查点的方案,并将检查点数量与ROB中同时存在的in-flight分支数量挂钩。

选择性检查点与完整检查点

并非所有分支指令都需要创建检查点。一种优化策略是选择性检查点(Selective Checkpointing):只为"高风险"分支创建检查点,而对"低风险"分支跳过检查点创建。高风险分支的判断依据包括:

  • 基于置信度的选择:分支预测器在预测时同时产生一个置信度(confidence)估计,表示预测正确的概率。对于置信度低的分支(预测不确定),分配检查点;对于置信度高的分支(几乎确定会被正确预测),跳过检查点。如果高置信度的分支最终预测失败(虽然概率很低),则需要使用WALK恢复作为后备方案。

  • 基于分支类型的选择:间接分支和循环退出分支的预测失败率通常高于一般的条件分支。可以只为这些高风险类型的分支分配检查点。

选择性检查点的优势在于减少了检查点缓冲区的压力——只有部分分支占用检查点,因此相同大小的缓冲区可以支持更多的in-flight分支。但代价是偶尔需要使用WALK恢复(当未分配检查点的分支预测失败时),增加了实现复杂度(需要同时支持两种恢复路径)。

增量检查点。另一种优化是增量检查点(Incremental Checkpoint):不保存完整的RAT快照,而只保存自上一个检查点以来被修改的RAT条目。例如,如果分支BkB_kBk+1B_{k+1}之间只有3条指令修改了RAT中的3个条目,则Bk+1B_{k+1}的检查点只需保存这3个条目的差异信息(逻辑寄存器编号+旧映射),而非完整的64条RAT映射。恢复时,从最近的完整检查点开始,按序应用增量来恢复到目标状态。

增量检查点显著减少了存储需求——平均每个检查点可能只需3×(5+8)=393 \times (5+8) = 39位(假设平均3个RAT条目被修改),而非完整的512位。但恢复延迟不再恒定——需要按序应用多个增量,延迟取决于从最近完整检查点到目标检查点之间的增量数量。这是存储与延迟的又一权衡。

基于置信度的混合方案。结合选择性检查点和增量检查点,一种实用的混合方案如下:

  • 为低置信度的分支分配完整检查点——这些分支最可能预测失败,需要最快的恢复。

  • 为中等置信度的分支分配增量检查点——如果预测失败,恢复延迟为几个周期(应用增量),可以接受。

  • 为高置信度的分支不分配任何检查点——如果极少数情况下预测失败,使用WALK恢复。

这种三级方案在存储、延迟和实现复杂度之间提供了灵活的权衡。但它也使恢复控制逻辑更加复杂——需要根据分支的检查点类型选择不同的恢复路径。在工程实践中,大多数处理器选择更简单的"完整检查点+WALK后备"两级方案,以降低验证复杂度。

WALK恢复的实现

WALK恢复(Walking Recovery),也称为逆序遍历恢复,是另一种RAT恢复策略。与检查点恢复直接用快照覆盖不同,WALK恢复通过从ROB的尾部向分支点逆序遍历,逐条撤销(undo)错误路径上每条指令对RAT的修改,最终将RAT恢复到分支点的状态。

WALK恢复的原理与ROB旧映射

每条指令在重命名阶段修改RAT时,处理器将该指令的"旧映射"(old mapping)——即该指令覆盖前逻辑寄存器对应的物理寄存器编号——记录在ROB表项中。具体而言,每个ROB表项需要包含以下WALK恢复所需的字段:

  • LogDstlog2L\lceil\log_2 L\rceil位):目标逻辑寄存器编号。对于RV64的32个整数寄存器,需要5位。

  • OldPhyDstlog2P\lceil\log_2 P\rceil位):该逻辑寄存器在重命名前映射到的物理寄存器编号。对于256个物理寄存器,需要8位。

  • NewPhyDstlog2P\lceil\log_2 P\rceil位):该逻辑寄存器在重命名后映射到的新物理寄存器编号。恢复时该寄存器需要被释放回空闲列表。

  • HasDst(1位):该指令是否有目标寄存器。store指令和分支指令没有目标寄存器,WALK时跳过。

例如,指令"add x1, x2, x3"在重命名时将x1从物理寄存器P5映射到新分配的P32,则ROB中记录LogDst=x1\text{LogDst} = \text{x1}OldPhyDst=P5\text{OldPhyDst} = \text{P5}NewPhyDst=P32\text{NewPhyDst} = \text{P32}HasDst=1\text{HasDst} = 1。WALK恢复时,处理以下操作:

sRAT[LogDst]OldPhyDst;FreeList.push(NewPhyDst)\text{sRAT}[\text{LogDst}] \leftarrow \text{OldPhyDst} \quad ; \quad \text{FreeList.push}(\text{NewPhyDst})

即将逻辑寄存器的映射恢复为该指令修改前的值,同时将该指令分配的新物理寄存器回收到空闲列表。

WALK过程的逐步操作

WALK恢复的具体硬件流程可以分解为以下步骤:

步骤1:初始化WALK指针。处理器设置一个WALK指针WW,初始指向ROB尾指针减1的位置(即最后一条被分配的错误路径指令):

W(T1)modNROBW \leftarrow (T - 1) \mod N_{\text{ROB}}

步骤2:逐周期撤销。每个周期,从WALK指针指向的位置开始,读出最多WwalkW_{\text{walk}}条ROB表项(WwalkW_{\text{walk}}通常等于重命名/分配宽度)。对于每条读出的表项ii

  1. 检查HasDsti\text{HasDst}_i:如果为0(store/分支/nop),跳过RAT恢复,但仍需释放该指令占用的Store Queue或Load Queue条目。

  2. 如果HasDsti=1\text{HasDst}_i = 1

    1. sRAT[LogDsti]OldPhyDsti\text{sRAT}[\text{LogDst}_i] \leftarrow \text{OldPhyDst}_i

    2. NewPhyDsti\text{NewPhyDst}_i推入空闲列表。

步骤3:移动WALK指针。将WALK指针向head方向移动WwalkW_{\text{walk}}个位置:

W(WWwalk)modNROBW \leftarrow (W - W_{\text{walk}}) \mod N_{\text{ROB}}

步骤4:终止检测。当WALK指针到达(或越过)分支指令的ROB索引bb时,恢复完成。终止条件为:

(Wb)modNROB<Wwalk(W - b) \mod N_{\text{ROB}} < W_{\text{walk}}

此时需要注意最后一批可能不足WwalkW_{\text{walk}}条,只处理从WWb+1b+1之间的条目。

WALK恢复的延迟分析

WALK恢复所需的周期数为:

Twalk=NwrongWwalkT_{\text{walk}} = \left\lceil \frac{N_{\text{wrong}}}{W_{\text{walk}}} \right\rceil

其中NwrongN_{\text{wrong}}是错误路径上的指令数,WwalkW_{\text{walk}}是每周期可以撤销的指令数。

错误路径指令数 NwrongN_{\text{wrong}}WALK宽度 WwalkW_{\text{walk}}恢复周期数场景描述
661最佳:分支很快解析
3065典型:L1命中延迟
60610中等:L2命中延迟
100617较差:L3命中延迟
300650最差:DRAM延迟
60415窄核WALK
6088宽核WALK

可以看到,WALK恢复的延迟与错误路径长度成正比,在最坏情况下可能达到50个周期甚至更多。这是WALK恢复相对于检查点恢复的主要劣势。

WALK恢复的具体数值例子。为了更直观地理解WALK恢复过程,考虑以下指令序列在一个4-wide处理器中的WALK恢复过程。假设分支BR位于ROB索引5,错误路径上有6条指令(I6–I11),当前sRAT状态和每条指令的ROB记录如下:

ROB索引指令LogDstOldPhyDstNewPhyDstHasDst
11I11: add x3,x1,x2x3P7P221
10I10: sw x5,0(x3)0 (store)
9I9: sub x1,x3,x4x1P14P211
8I8: add x5,x1,x3x5P9P201
7I7: slli x3,x1,2x3P3P191
6I6: add x1,x2,x3x1P12P141
5BR: beq x1,x0,...0 (branch)

WALK过程(4-wide,从ROB索引11向索引6逆序遍历):

WALK周期1(处理ROB索引11, 10, 9, 8):

  1. ROB[11]: HasDst=1, 执行sRAT[x3]P7\text{sRAT}[\text{x3}] \leftarrow \text{P7},释放P22到空闲列表。

  2. ROB[10]: HasDst=0(store),跳过RAT恢复,释放Store Queue条目。

  3. ROB[9]: HasDst=1, 执行sRAT[x1]P14\text{sRAT}[\text{x1}] \leftarrow \text{P14},释放P21。

  4. ROB[8]: HasDst=1, 执行sRAT[x5]P9\text{sRAT}[\text{x5}] \leftarrow \text{P9},释放P20。

WALK周期2(处理ROB索引7, 6):

  1. ROB[7]: HasDst=1, 执行sRAT[x3]P3\text{sRAT}[\text{x3}] \leftarrow \text{P3},释放P19。

  2. ROB[6]: HasDst=1, 执行sRAT[x1]P12\text{sRAT}[\text{x1}] \leftarrow \text{P12},释放P14。

  3. WALK指针到达分支ROB索引5,WALK完成。

恢复完成后,sRAT中x1映射为P12、x3映射为P3、x5映射为P9——这正是分支BR重命名完成后的正确映射状态。整个WALK过程消耗了2个周期(6条指令 / 4-wide = 2周期)。

注意在WALK周期1中,ROB[9]将x1恢复为P14,但紧接着在WALK周期2中,ROB[6]又将x1恢复为P12。这展示了"同一逻辑寄存器的多次恢复"的正确处理——逆序遍历保证了最终结果是分支点时的正确映射。

WALK恢复的时序示意图(部分阻塞式)。WALK恢复与前端取指并行进行,WALK完成后正确路径指令立即开始重命名。
WALK恢复的时序示意图(部分阻塞式)。WALK恢复与前端取指并行进行,WALK完成后正确路径指令立即开始重命名。

WALK恢复期间的流水线行为

WALK恢复的一个重要问题是:在WALK进行期间,处理器能否继续执行正确路径上的指令?答案取决于具体实现:

  • 阻塞式WALK:在WALK恢复期间,前端暂停取指,后端暂停分配。整个流水线等待WALK完成后才从正确路径恢复执行。这是最简单的实现,但恢复期间流水线完全空闲,浪费了大量执行带宽。总恢复延迟为Twalk+TredirectT_{\text{walk}} + T_{\text{redirect}}(WALK时间加上前端重新填充时间)。

  • 部分阻塞式WALK:前端可以从正确路径开始取指和解码(利用恢复信号中的correct_target重定向PC),但新取回的指令不能进入重命名阶段——因为RAT正在被WALK过程修改,此时的RAT状态是不一致的。新指令在解码缓冲中等待,直到WALK完成后才开始重命名。这种方式可以"预热"前端流水线,使得WALK完成后能立即开始分配新指令,节省约TredirectT_{\text{redirect}}个周期。

  • 非阻塞式WALK:在WALK恢复的同时,允许已经在后端中的正确路径指令(分支之前的指令)继续执行和提交。这要求WALK逻辑与正常的提交逻辑共享ROB的读端口,实现较为复杂。但由于正确路径上的指令继续提交,ROB、物理寄存器等资源被释放,有助于减轻WALK完成后的资源压力。

WALK恢复的边界情况处理

WALK恢复的实现还需要处理几个微妙的边界情况:

(1)没有目标寄存器的指令。store指令和分支指令没有目标逻辑寄存器,不修改RAT。WALK遍历到这类指令时跳过RAT恢复步骤,但仍需释放store/branch占用的Store Queue/Load Queue条目和分支掩码资源。在硬件实现中,HasDst\text{HasDst}位为0的条目不触发RAT写端口,但仍然消耗一个WALK槽位(即不能跳过它以处理下一条有目标寄存器的指令——因为WALK是按ROB顺序逆序进行的,不能跳跃)。

(2)同一逻辑寄存器的多次重命名。如果错误路径上有多条指令写入同一个逻辑寄存器(例如x1被I4重命名为P10,又被I7重命名为P15),WALK逆序恢复的正确性不受影响:WALK先处理I7(将x1从P15恢复为P10),再处理I4(将x1从P10恢复为P5,其中P5是分支点时x1的映射)。逆序处理自然保证了最终结果的正确性。

(3)WALK期间发现更早的预测失败。如果在WALK恢复分支BjB_j的过程中,一条更早的分支BiB_ii<ji < j)也被检测到预测失败,处理器需要切换到对BiB_i进行恢复。由于BiB_i更早,BjB_j本身也在BiB_i的错误路径上。处理器可以直接将WALK的终点从BjB_j改为BiB_i,继续WALK直到BiB_i的位置。已经完成的WALK撤销操作(从ROB tail到当前WALK指针位置之间的指令)不需要重做——它们本来就需要被撤销。

(4)WALK与异常的交互。如果在WALK期间,ROB头部的一条指令到达提交状态并检测到异常,通常异常的处理优先级更高。处理器可以停止WALK,转而执行完整的异常恢复(用提交RAT覆盖推测RAT),因为异常恢复会清除所有推测状态,包括WALK正在恢复的分支。

案例研究 2 — Intel P6微架构的WALK恢复

Intel P6微架构(1995年,用于Pentium Pro/II/III)是采用WALK恢复的经典案例。P6选择WALK恢复而非检查点恢复,主要基于以下工程考量:

  • 面积约束:P6时代的晶体管预算有限(Pentium Pro约550万晶体管),检查点缓冲区的面积开销对整体芯片面积的影响较大。WALK恢复利用ROB中已有的旧映射信息(OldPhyDst字段),不需要额外的检查点存储。

  • 分支预测精度:P6的分支预测器已经相当精确(约95%–97%准确率),预测失败的频率较低。WALK恢复的可变延迟在大多数情况下可以接受。

  • ROB深度:P6的ROB只有40个条目,即使最坏情况下WALK恢复也只需约40/31440/3 \approx 14个周期(P6为3-wide设计)。

P6的WALK恢复以3-wide宽度进行——每周期从ROB读出3条表项的旧映射信息,更新RAT并释放物理寄存器。WALK期间前端暂停取指。

随着后续架构演进(特别是从Core微架构开始),Intel逐渐转向基于检查点的恢复方案,以应对更深的流水线(ROB从40项扩展到224项以上)带来的更长WALK延迟。

架构状态恢复

架构状态恢复(Architecture State Recovery)是第三种RAT恢复策略,也是最简单但延迟最大的方案。其核心思想是:使用已提交RAT(committed RAT,也称为architectural RAT或cRAT)作为恢复目标,而不是恢复到分支点的状态。

恢复过程

架构状态恢复的步骤如下:

  1. 暂停分发:停止向后端分配新指令。

  2. 等待排空:等待所有已经在后端中的指令完成执行(drain)。这包括等待所有cache miss的load返回数据、所有长延迟的除法/浮点运算完成。在此期间,已完成的正确路径指令正常提交,cRAT持续更新。

  3. 复制cRAT:当ROB中所有先于分支指令的指令都已提交后,将cRAT的内容覆盖到sRAT中。这一操作在1个周期内完成。

  4. 清空ROB:将ROB中分支指令及其之后的所有条目清除。

  5. 恢复空闲列表:基于cRAT重建空闲列表——所有未出现在cRAT映射中的物理寄存器都标记为空闲。

延迟分析

架构状态恢复的总延迟由排空时间主导:

Tarch=Tdrain+1(cRAT复制)T_{\text{arch}} = T_{\text{drain}} + 1\text{(cRAT复制)}

排空延迟TdrainT_{\text{drain}}是所有in-flight指令完成执行所需的时间,它等于ROB中最慢指令的执行延迟。在最坏情况下,如果ROB中有一条DRAM miss的load(约200周期延迟),则排空时间可能达到200个周期——这远远超过了检查点恢复的1个周期和WALK恢复的10–50个周期。

在平均情况下,假设ROB中指令的平均剩余执行延迟为Lˉ\bar{L}个周期,则排空时间约为Lˉ\bar{L}。对于一个典型的工作负载,Lˉ\bar{L}约为20–50个周期。

空闲列表重建的挑战

架构状态恢复的另一个复杂之处在于空闲列表的重建。排空完成后,cRAT中记录了每个逻辑寄存器对应的物理寄存器。所有不在cRAT映射中的物理寄存器都应该是空闲的。重建空闲列表的方法有两种:

方法1:位图扫描。维护一个PP位的位图BB,初始全1(所有寄存器空闲)。遍历cRAT的LL个条目,对每个cRAT[i]=pi\text{cRAT}[i] = p_i,将B[pi]B[p_i]清零。位图BB中仍为1的位对应的物理寄存器即为空闲寄存器。这个过程需要LL个周期(逐条扫描cRAT)或1个周期(并行标记,但需要LL个写端口的位图)。

方法2:使用cRAT同步维护的cFreeList。与cRAT同步维护一份committed free list(cFreeList)。每当cRAT更新时(指令提交时),被替换的旧物理寄存器被推入cFreeList。恢复时直接使用cFreeList的状态,无需重建。这种方法更常用。

案例研究 3 — Alpha 21264的恢复方案

Alpha 21264(1998年)采用了一种混合恢复策略,结合了架构状态恢复和检查点思想:

  • 分支预测失败:21264为每条分支维护一个RAT检查点(最多80个in-flight指令中约20条分支),但不保存完整的RAT快照。相反,它使用一种影子寄存器映射(shadow map)技术:维护一个额外的映射表,在分支提交时更新。预测失败时,使用这个影子映射表加上有限的WALK来恢复sRAT。

  • 异常/中断:使用完整的架构状态恢复——等待ROB排空,然后用cRAT覆盖sRAT。21264的ROB有80个条目,排空时间在最坏情况下可能很长,但异常发生频率很低,这个延迟可以接受。

  • 性能权衡:21264的设计团队认为,对于1990年代末的工艺水平和工作负载特征,这种混合方案在面积和性能之间提供了最好的平衡。21264的IPC在当时是业界领先的,证明了这一设计决策的有效性。

三种恢复机制的综合对比

设计权衡 1 — 检查点恢复 vs WALK恢复 vs 架构状态恢复

三种恢复机制各有优劣:

特性检查点恢复WALK恢复架构状态恢复
恢复延迟1周期(恒定)N/W\lceil N/W \rceil周期Tdrain+1T_{\text{drain}} + 1周期
最坏恢复延迟1周期\sim50周期\sim200周期
面积开销高(检查点存储)低(ROB旧映射)最低(cRAT)
额外存储C×SC \times S00
分支数限制CC限制
恢复粒度恢复到分支点恢复到分支点恢复到提交点
恢复期间可否继续提交可以部分支持必须等排空
适用场景高频恢复事件中频恢复事件低频恢复事件
典型应用分支预测失败分支预测失败异常/中断
代表处理器R10000, ZenP6, Cortex-A9Alpha 21264

在实践中,高性能处理器倾向于采用检查点恢复或混合方案。例如,Intel的Golden Cove和AMD的Zen 4都使用检查点恢复机制来应对分支预测失败,因为1周期的恢复延迟对于减少分支预测惩罚至关重要。而一些面积敏感的嵌入式处理器和中端核心可能采用WALK恢复,因为它不需要额外的检查点存储。

一种混合方案是同时维护少量检查点(如4–8个)用于分支预测失败的快速恢复,同时保留WALK恢复能力或架构状态恢复能力用于处理异常——因为异常发生的频率远低于分支预测失败(每百万条指令可能只有几十次异常),所以异常恢复的延迟不那么关键。这种混合方案在面积和性能之间取得了很好的平衡。

量化对比。考虑一个6-wide、ROB深度512的处理器,分支预测失败率为每200条指令一次,错误路径平均长度为40条指令。三种恢复方式的平均恢复延迟对比如下:

  • 检查点恢复:恢复延迟恒定1周期。加上前端重定向约8周期,总惩罚约9周期/次。每200条指令一次,IPC损失约9/200=4.5%9 / 200 = 4.5\%。额外面积:\sim1.2 KB检查点存储。

  • WALK恢复:平均恢复延迟40/6=7\lceil 40/6 \rceil = 7周期。如果使用部分阻塞式WALK(前端并行取指),总惩罚约max(7,8)=8\max(7, 8) = 8周期。加上WALK可变延迟的方差,平均总惩罚约10周期/次,IPC损失约5%5\%。无额外面积。

  • 架构状态恢复:平均排空延迟约30周期(取决于ROB中指令分布),总惩罚约38周期/次。IPC损失约19%19\%——这对于高频事件是不可接受的。无额外面积。

可以看出,对于分支预测失败这样的高频事件,检查点恢复和WALK恢复之间的差距约为0.5%0.5\% IPC,而架构状态恢复的性能惩罚是不可接受的。这也解释了为什么所有高性能处理器都使用检查点或WALK来处理分支预测失败,而将架构状态恢复留给低频的异常/中断事件。

三种RAT恢复机制的工作原理对比。检查点恢复在1个周期内用快照覆盖RAT;WALK恢复从ROB尾部逐条逆序撤销重命名映射;架构状态恢复等待排空后用提交RAT覆盖。
三种RAT恢复机制的工作原理对比。检查点恢复在1个周期内用快照覆盖RAT;WALK恢复从ROB尾部逐条逆序撤销重命名映射;架构状态恢复等待排空后用提交RAT覆盖。
::: details 案例研究 4 — 香山处理器的混合恢复机制

香山(XiangShan)是中国科学院计算技术研究所开发的开源高性能RISC-V处理器。香山处理器的雁栖湖(Yanqihu)和昆明湖(Kunminghu)微架构均采用了混合恢复方案

  • 分支预测失败:使用快照恢复。香山维护一组RAT快照(snapshot),在每条分支指令重命名时保存当前RAT状态。预测失败时,用对应快照在1个周期内恢复RAT。快照数量(称为snapshot buffer的容量)决定了同时支持的未解析分支数量上限。昆明湖微架构使用16个快照。

  • 异常/中断:使用提交RAT恢复。由于异常发生在提交阶段,此时提交RAT已经反映了最后一条已提交指令的正确映射,直接用提交RAT覆盖推测RAT即可。

  • Load违规(memory order violation):使用WALK恢复或重刷(replay/re-dispatch)。当检测到推测性load读到了错误的数据时(因为一条更早的store后来才计算出地址),需要从违规load处重新执行。

香山的这种混合方案体现了"高频事件用快方法、低频事件用省方法"的设计哲学——分支预测失败每几百条指令就发生一次,必须用1周期恢复;异常每百万条指令才发生几十次,用慢一点但更省面积的方案即可。

:::

异常的处理

异常(Exception),也称为同步异常(Synchronous Exception)或陷阱(Trap),是由指令执行本身触发的事件。与分支预测失败不同,异常是程序逻辑或硬件保护机制的正常组成部分——操作系统依赖异常来实现虚拟内存、系统调用、调试等关键功能。异常处理必须满足精确异常(Precise Exception)的要求,这是乱序执行处理器设计中的核心约束之一。

同步异常的种类与优先级

不同ISA定义了不同种类的同步异常,但从处理器微架构的角度,可以将它们按触发时机和处理方式分为以下几类:

按触发阶段分类

(1)指令获取阶段异常。在取指过程中发生的异常:

  • 取指页错误(Instruction Page Fault):指令的虚拟地址在页表中没有有效映射,或存在权限违规(如从非可执行页取指)。

  • 取指地址未对齐(Instruction Address Misaligned):PC不满足对齐要求。在RISC-V中,如果不支持C扩展(压缩指令),则PC必须4字节对齐。

  • 取指访问错误(Instruction Access Fault):物理内存不可访问(如访问了不存在的物理地址或PMA违规)。

(2)解码阶段异常。在指令解码时检测到的异常:

  • 非法指令(Illegal Instruction):指令编码不属于ISA定义的任何有效指令。在RISC-V中,任何全0或全1的32位编码都是非法指令。

  • 特权级违规:当前特权级不允许执行某条指令(如在U-mode执行sret)。

(3)执行阶段异常。在指令执行过程中发生的异常:

  • 数据页错误(Data Page Fault):load或store指令的虚拟地址在页表中没有有效映射或权限违规。这是最常见的异常,操作系统的虚拟内存管理(缺页处理、写时复制)都依赖此异常。

  • 数据地址未对齐(Data Address Misaligned):load/store的地址不满足自然对齐要求。

  • 数据访问错误(Data Access Fault):物理内存访问违规。

  • 断点异常(Breakpoint):指令地址或数据地址匹配了硬件断点寄存器的设置(RISC-V的ebreak或硬件断点触发器)。

  • 整数除零(在某些ISA中是异常;RISC-V的DIV/REM指令返回特定值而不触发异常)。

(4)环境调用异常。由显式指令触发的"预期异常":

  • 系统调用(Environment Call):RISC-V的ecall指令。用于从低特权级调用高特权级的服务(如用户程序调用操作系统)。

  • 环境断点(Environment Break):RISC-V的ebreak指令。用于调试器设置软件断点。

异常优先级

当一条指令同时触发多个异常时(例如一条非法指令的地址恰好在无效页上),需要根据异常优先级决定报告哪个异常。RISC-V规范定义的优先级如表表 39.6所示,取指阶段的异常优先级最高,因为如果取指本身就失败了,后续的解码和执行都没有意义。

优先级异常码异常名称触发阶段
最高1取指访问错误IF
12取指页错误IF
0指令地址未对齐IF
2非法指令ID
3断点ID/EX
8/9/11环境调用(U/S/M)ID
4/6数据地址未对齐(L/S)EX
5/7数据访问错误(L/S)EX
最低13/15数据页错误(L/S)EX

RISC-V异常类型及其优先级

异常的频率特征

不同类型的异常在实际工作负载中的发生频率差异很大。在一个典型的Linux应用程序执行中:

  • 系统调用(ecall):每百万条指令约100–10000次,取决于I/O密集程度。

  • 缺页异常:在应用启动阶段可能每百万条指令数百次(大量冷页面),稳态运行时通常少于每百万条指令10次。

  • 非法指令:正常运行时几乎不发生;但在模拟器/仿真器场景中可能故意使用非法指令来trap到仿真层。

  • 地址未对齐:在RISC-V中,如果硬件支持未对齐访问(可选),则不会触发异常;如果不支持,则每次未对齐的load/store都会触发异常由软件处理——这在某些C代码中可能相当频繁。

  • 断点:仅在调试场景中发生。

异常在ROB中的标记

在乱序执行处理器中,异常的处理采用延迟报告(deferred reporting)的策略:当一条指令在执行过程中触发异常时,处理器不立即处理该异常,而是将异常信息记录在该指令的ROB表项中,等到该指令到达ROB头部准备提交时才真正处理。

这一策略是实现精确异常的关键。在乱序执行环境中,指令的执行顺序与程序顺序不同——一条后面的指令可能先于前面的指令完成执行。如果在执行时立即处理异常,则可能出现"后面的指令已经提交了效果,但触发异常的指令之前还有指令尚未完成"的情况,违反精确异常的要求。

ROB中的异常标记字段

每个ROB表项通常包含以下与异常相关的字段:

  • exception_valid(1位):标记该指令是否触发了异常。

  • exception_cause(4–6位):异常原因编码,对应ISA定义的异常码。RISC-V定义了16种异常原因,需要4位编码。如果需要区分更多的异常子类型(如区分不同的页错误类型),可能需要5–6位。

  • exception_tval(64位,可选):异常附加值。对于页错误,这是触发错误的虚拟地址;对于非法指令异常,这是非法指令的编码值。在面积敏感的设计中,tval可能不存储在ROB中,而是在提交时从其他结构(如TLB miss缓冲)中获取。

在一个512条目的ROB中,异常字段的总存储开销为:

512×(1+4+64)=512×69=35328 bits4.3 KB512 \times (1 + 4 + 64) = 512 \times 69 = 35328 \text{ bits} \approx 4.3 \text{ KB}

如果省略tval(在提交时从其他结构获取),则开销降至512×5=2560512 \times 5 = 2560320\approx 320字节——显著减少。这也是为什么许多实现选择不在ROB中存储完整的tval。

硬件描述 5 — ROB表项的恢复相关字段完整布局

将本章讨论的所有恢复相关功能汇总,一个完整的ROB表项包含以下与恢复机制相关的字段(不包括正常执行所需的PC、操作码、结果等字段):

字段名用途位宽用于哪种恢复
LogDst目标逻辑寄存器编号5WALK
OldPhyDst旧的物理寄存器映射8WALK
NewPhyDst新分配的物理寄存器8WALK(释放)
HasDst是否有目标寄存器1WALK
branch_mask分支掩码向量16分支掩码清除
exception_valid是否触发异常1异常处理
exception_cause异常原因编码4异常处理
fp_flags浮点异常标志5提交时OR
checkpoint_id关联的检查点编号4检查点恢复
is_branch是否为分支指令1分支掩码管理
branch_mask_bit占用的掩码位编号4掩码位释放
恢复相关字段总计57位

对于512条目的ROB,恢复相关字段的总存储开销为512×57=29184512 \times 57 = 291843.6\approx 3.6 KB。加上ROB的其他字段(PC、操作码、执行状态、结果指针等,约50–80位),整个ROB的存储量约为512×(57+70)65512 \times (57 + 70) \approx 65 Kbits 8\approx 8 KB。

注意上表中的exception_tval(64位)未包含在内——如前所述,大多数实现不在ROB中存储完整的tval。如果需要tval,ROB的每条目将增加64位,总存储量增加32 KB,这对ROB的面积影响很大。一种折中方案是只存储tval的低12位(页内偏移),完整的虚拟地址在提交时从TLB miss buffer或LSU中获取。

异常标记的时序

不同类型的异常在不同流水线阶段被标记到ROB中:

(1)取指阶段异常的传播。当I-TLB报告页错误或访问错误时,取指单元将异常信息附加到指令包(fetch packet)中。这些指令继续通过解码和重命名流水线(它们的指令编码可能是无效的,但不影响——因为最终不会被提交),在分配ROB表项时将异常信息写入ROB。具体传播路径为:

  1. I-TLB报告异常\rightarrow取指单元将异常信息附加到fetch packet的metadata中。

  2. Fetch Buffer将带异常标记的指令传递给解码器。

  3. 解码器检测到异常标记,跳过正常解码(避免对无效编码进行解码),生成一条异常标记微操作(exception marker μ\muop)。

  4. μ\muop通过重命名阶段(不分配物理寄存器,因为它没有目标寄存器),在ROB分配时写入异常字段。

(2)解码阶段异常。解码器检测到非法指令或特权级违规后,将异常信息附加到微操作中,在分配ROB时写入异常字段。

(3)执行阶段异常。执行单元(ALU、LSU)在执行过程中检测到异常后,在写回阶段将异常信息与执行结果一起写入ROB的异常字段。对于LSU产生的页错误,异常信息可能需要从D-TLB的miss处理逻辑中获取。

设计提示

在某些微架构中,对于取指和解码阶段检测到的异常,触发异常的"指令"甚至可以不经过完整的解码和重命名——它可以被替换为一个特殊的异常标记微操作(exception marker μ\muop),这条微操作不执行任何计算,只是在ROB中占据一个位置并标记异常。当这条微操作到达ROB头部提交时,触发异常处理。这种优化避免了对无效指令编码进行重命名和发射的开销,也避免了在执行单元中处理无效操作码的复杂性。

推测异常的自然消解

在乱序执行中,一个重要的细节是:异常可能发生在推测执行的指令上。例如,一条分支预测为taken后的load指令触发了页错误,但该分支最终被证明预测失败——这条load指令根本不应该被执行。在延迟报告的策略下,这个问题自然解决了:该load指令的ROB表项虽然标记了页错误,但当分支预测失败被检测到后,该ROB表项会被清除——页错误永远不会被提交,也就永远不会被报告给操作系统。

这一特性是延迟报告策略的关键优势:推测异常不需要任何特殊的撤销逻辑——分支预测失败的正常恢复机制自然地丢弃了所有推测异常。

异常标记与执行结果的并存

一个值得注意的实现细节是:当一条指令触发异常时,该指令可能同时产生了执行结果。例如,一条load指令可能在TLB中检测到页错误,但如果TLB查找和缓存访问是并行进行的(某些实现中允许的一种推测优化),缓存可能返回了一个数据值。此时,ROB中该指令的表项同时包含了:

  • 执行结果(从缓存返回的数据,可能是错误的——因为地址翻译失败了)。

  • 异常标记(exception_valid=1, exception_cause=13表示load页错误)。

提交逻辑在处理时,先检查exception_valid。如果为1,则忽略执行结果,不将其写入架构寄存器——转而执行异常处理流程。如果为0,则正常将结果提交。

这种"异常优先于结果"的逻辑确保了即使执行单元产生了结果,异常指令的效果也不会反映到架构状态中。

缺页异常与虚拟内存的微架构影响

缺页异常(Page Fault)是最常见的异常类型,它对处理器微架构有几个值得关注的影响:

(1)TLB未命中 vs 页错误。并非所有TLB未命中都会导致页错误。TLB未命中首先触发硬件页表遍历器(PTW)查找页表。如果页表中有有效映射,PTW将其填入TLB,load/store可以继续执行——这只是TLB miss,不是异常。只有当PTW发现页表条目无效(页面不在物理内存中、或权限不足)时,才产生页错误异常。

从流水线角度,TLB miss处理的延迟约为20–200周期(取决于页表遍历的缓存命中情况),但不涉及流水线恢复——处理器可以继续执行其他不依赖于该load/store的指令。页错误才会触发本章讨论的完整流水线恢复流程。

(2)写时复制(Copy-on-Write)。当一个store指令写入一个只读映射的页面时,会触发store页错误。操作系统的处理方式通常是写时复制:复制该页面的内容到新的物理页,更新页表映射为可写,然后返回。返回后store指令被重新执行并成功完成。

从微架构角度,写时复制导致的页错误处理延迟特别长——因为它涉及页面复制(通常需要数千个周期来复制4KB页面),但每次fork之后只在第一次写入时发生,后续访问不再触发异常。

精确异常的实现

精确异常(Precise Exception)是现代处理器的基本要求。这一概念最早由Smith和Pleszkun在1985年的论文"Implementing Precise Interrupts in Pipelined Processors"中系统化地定义和实现。

精确异常的形式化定义

当处理器在指令IkI_k处报告异常时,必须保证:

  1. 程序顺序上IkI_k之前的所有指令(I0,I1,,Ik1I_0, I_1, \ldots, I_{k-1})的效果已经全部提交到架构状态中。

  2. 程序顺序上IkI_k及其之后的所有指令(Ik,Ik+1,I_k, I_{k+1}, \ldots)的效果完全没有反映到架构状态中。

  3. 异常处理程序可以看到一个一致的架构状态,仿佛处理器在IkI_k处"暂停"了。

精确异常是操作系统正确运行的基础。例如,当load指令触发缺页异常时,操作系统的缺页处理程序需要知道精确的异常地址来定位缺失的页面、从磁盘加载数据、更新页表,然后重新执行触发异常的load指令。如果异常不精确——即异常前的指令有未完成的、或异常后的指令有已提交的——操作系统将无法正确恢复执行。

多个异常共存的处理

在乱序执行的ROB中,可能同时有多条指令标记了异常。例如,load指令I5I_5触发了数据页错误,而在I5I_5之后的store指令I12I_{12}也触发了数据页错误。由于ROB的顺序提交机制,I5I_5会先到达ROB头部并触发异常处理——I12I_{12}的异常永远不会被看到,因为I12I_{12}在异常恢复时会被清除。只有当I5I_5的异常被操作系统处理后、I5I_5被重新执行并成功完成、I12I_{12}到达ROB头部时,I12I_{12}的异常才会被报告。这种"一次只报告一个异常"的行为正是精确异常语义的核心。

基于ROB的异常处理流程(8步)

ROB的顺序提交机制天然支持精确异常的实现。当ROB头部的指令准备提交时,提交逻辑检查该表项的exception_valid位:

  • 如果为0,正常提交:将结果写入架构寄存器文件或将store标记为可排出(committed)。

  • 如果为1,触发异常处理,执行以下8个步骤:

    1. 写入mepc:将异常指令的PC写入CSR的mepc(或sepc,取决于特权级)。mepc记录了异常发生时的指令地址,异常处理程序通过mret返回到这个地址继续执行。

    2. 写入mcause:将异常原因写入mcause(或scause)。异常原因编码对应表表 39.6中的异常码,异常处理程序通过读取mcause来判断异常类型并分发到对应的处理函数。

    3. 写入mtval:将异常附加值写入mtval(或stval)。对于页错误,这是触发错误的虚拟地址;对于非法指令异常,这是非法指令的编码值。

    4. 保存mstatus:将当前状态信息(如中断使能位MIE、当前特权级MPP)保存到mstatus的相关字段(MPIE和MPP)。同时将MIE清零以禁止中断嵌套。

    5. 清除ROB:清除ROB中该指令及其之后的所有表项(通过移动尾指针到头指针位置,实际上清空整个ROB)。

    6. RAT恢复:将RAT恢复到架构状态——使用提交RAT(committed RAT / architectural RAT)覆盖推测RAT。这一操作在1个周期内完成。

    7. 清空后端结构:清空发射队列、Load Queue、Store Queue中的所有条目。对于发射队列,将所有条目的valid位清零;对于LSQ,将head和tail指针复位。

    8. 前端重定向:将PC设置为异常处理向量地址(从mtvecstvec读取),前端从异常处理向量地址开始取指。

步骤1–4涉及多个CSR的写入。在某些实现中,这些CSR写入可以在1个周期内并行完成(因为它们写入不同的CSR,没有数据依赖)。在更保守的实现中,CSR写入可能需要2–3个周期序列化完成。

提交逻辑中的异常检测状态机。在硬件实现中,提交逻辑通常使用一个有限状态机(FSM)来管理异常处理过程。状态机包含以下状态:

  1. NORMAL:正常提交状态。每个周期检查ROB头部的WcommitW_{\text{commit}}条指令(WcommitW_{\text{commit}}为提交宽度,通常4–8),如果没有异常则正常提交。如果检测到异常,转入EXCEPTION状态。

  2. EXCEPTION:异常处理状态。在此状态中:

    1. 首先等待异常指令之前的所有指令提交完成(如果还没提交完)。

    2. 然后执行CSR写入(mepcmcausemtvalmstatus)。

    3. 发出flush信号清除ROB和后端结构。

    4. 发出redirect信号将PC重定向到异常向量。

    完成后转回NORMAL状态。

  3. INTERRUPT:中断处理状态。与EXCEPTION类似,但中断的触发条件不同(外部信号而非ROB标记)。

这个状态机需要处理多种优先级冲突。例如,在同一个周期中,可能同时发生以下事件:(a) ROB头部指令标记了异常;(b) 存在待决中断;(c) 分支预测失败正在恢复中。一般的优先级顺序为:异常 >> 中断 >> 分支预测失败恢复。

异常处理中的物理寄存器回收。异常处理时ROB被完全清空,所有分配给in-flight指令的物理寄存器都需要被回收。使用cRAT恢复后,空闲列表也需要恢复到与cRAT一致的状态。具体做法是:恢复时将空闲列表重置为"所有不在cRAT映射中的物理寄存器均为空闲"的状态。对于FIFO实现的空闲列表,可以保存一个与cRAT同步更新的"committed free list head pointer"来实现快速恢复。

精确异常的处理流程。I2触发异常,在I0和I1已正常提交后,I2到达ROB头部,触发异常处理,I2及其之后的所有指令被清除。
精确异常的处理流程。I2触发异常,在I0和I1已正常提交后,I2到达ROB头部,触发异常处理,I2及其之后的所有指令被清除。

异常恢复中的RAT处理

与分支预测失败不同,异常发生时需要将RAT恢复到架构状态——即最后一条已提交指令的映射状态。这通常不需要检查点:大多数处理器维护两份RAT——推测RAT(Speculative RAT)用于重命名阶段的快速查找,提交RAT(Committed RAT / Architectural RAT)随着指令提交而更新。异常发生时,用提交RAT覆盖推测RAT即可。提交RAT的更新是在提交阶段完成的——每当一条指令正常提交时,提交RAT中对应逻辑寄存器的映射被更新为该指令分配的物理寄存器。由于提交是按程序顺序进行的,提交RAT始终反映最新的架构状态。

使用cRAT恢复而非检查点恢复有一个重要区别:cRAT恢复后,RAT状态对应的是最后一条已提交指令之后的映射,而非异常指令之前的映射。由于异常指令本身不被提交(它触发了异常),cRAT恰好反映了异常指令之前的所有已提交效果——这正是精确异常所要求的状态。

异常恢复的开销分析

与分支预测失败恢复相比,异常恢复的开销通常更大:

  1. 整个ROB被清空(而不仅仅是分支之后的指令),流水线完全排空。

  2. 需要写入多个CSR(mepcmcausemtvalmstatus),这可能需要多个周期。

  3. 异常处理向量地址可能不在I-Cache中,导致额外的取指延迟。

但由于异常的发生频率远低于分支预测失败(通常每百万条指令只有几十到几百次),其对整体性能的影响较小。在某些工作负载中(如数据库系统大量使用缺页机制的场景),异常频率可能较高,此时异常处理的延迟会成为性能瓶颈。

性能分析 2 — 异常处理延迟的分解

一个典型的异常处理过程的延迟分解如下(以缺页异常为例):

阶段延迟(周期)
异常指令到达ROB头部(等待先序指令完成)10–100+
ROB清空 + RAT恢复 + CSR写入5–10
mtvec取指3–10
异常处理程序前几条指令的冷启动10–20
硬件恢复总延迟(不含OS处理)\approx30–140

注意最大的变数在于"异常指令到达ROB头部"的等待时间——如果异常指令前面有一条长延迟的cache miss load(可能需要几百个周期访问主存),则即使异常已经被标记在ROB中,也必须等待所有先序指令完成后才能处理异常。这是精确异常的代价:为了保证异常指令之前的所有指令效果已经提交,必须等待它们全部完成。

操作系统层面的异常处理(如页表遍历、磁盘I/O)远远超出硬件延迟的范畴,可能达到数千到数百万个周期。

异常返回与可重启性

异常返回指令

当操作系统完成异常处理后,通过mret(或sret)指令返回到被中断的程序。mret指令执行以下操作:

  1. mepc中读取返回地址,将其设为PC。

  2. 恢复mstatus中保存的先前状态(如中断使能位、先前特权级)。

  3. 处理器从返回地址开始取指,恢复被中断的程序执行。

从微架构角度看,mret本身也是一条序列化指令——它修改了特权级和中断使能状态,后续指令必须在新的特权级下执行。因此,mret的处理与其他序列化指令类似:等待所有先序指令提交后执行,修改架构状态,然后重定向前端到mepc中记录的地址。

异常的可重启性分类

并非所有异常都要求重新执行触发异常的指令。按照异常返回后PC的位置,异常可分为三类:

  • Fault(故障):返回到触发异常的指令本身(mepc指向异常指令的PC)。缺页异常、非法指令异常属于这一类。操作系统处理完异常原因后(如加载缺失的页面),期望重新执行相同的指令并成功完成。

  • Trap(陷阱):返回到触发异常的指令的下一条指令(mepc指向异常指令PC+4)。ecallebreak属于这一类——系统调用完成后不需要重新执行ecall指令本身。

  • Abort(中止):异常不可恢复,不期望返回。双重异常(double fault)或硬件不可纠正错误属于这一类。

在RISC-V中,所有异常都将mepc设为异常指令的PC(而非PC+4),由软件决定是返回到同一条指令(对于fault)还是跳过(对于trap,软件需要将mepc加4后再执行mret)。这与x86的区分fault/trap/abort的硬件行为略有不同。

异常委托与双重异常

异常委托。在RISC-V的多特权级系统中,异常可以被委托(delegate)给较低的特权级处理。medeleg(Machine Exception Delegation)寄存器的每一位对应一种异常类型:如果某位为1,则该类型的异常在S-mode或U-mode中发生时,直接委托给S-mode的异常处理程序处理(写入sepc/scause/stval,跳转到stvec),而不是先trap到M-mode。

从微架构角度,异常委托的实现增加了提交逻辑的复杂度:当异常指令到达ROB头部时,提交逻辑需要执行以下判断:

  1. 确定异常类型(从ROB的exception_cause字段读取)。

  2. 检查medeleg中对应位是否为1。

  3. 如果委托(且异常发生在S/U-mode),则将异常信息写入S-mode CSR(sepc等)并跳转到stvec

  4. 如果不委托,则写入M-mode CSR(mepc等)并跳转到mtvec

这个判断逻辑在提交阶段增加了一层多路选择器(2选1:M-mode CSR或S-mode CSR),但由于异常发生频率很低,这一额外延迟不影响正常提交路径的时序。

双重异常(Double Fault)。如果异常处理程序本身又触发了异常(例如mtvec指向的地址在无效页面上),就会发生双重异常。双重异常的处理取决于架构设计:

  • x86:x86有专门的双重异常处理机制——如果在处理异常时发生另一个异常,硬件检测到双重异常条件,跳转到双重异常处理程序(IDT中的第8号中断向量)。如果双重异常处理程序也失败(三重异常),处理器进入关机状态(shutdown)。

  • RISC-V:RISC-V没有硬件级别的双重异常检测机制。如果异常处理程序触发了新的异常,处理器会正常地处理新的异常——覆盖mepcmcause中原来的值。软件需要在异常处理程序入口处保存这些CSR的值到栈中,以防止被后续异常覆盖。

  • 微架构影响:从微架构角度,双重异常不需要特殊硬件支持——它只是正常的异常处理流程再次被触发。但如果异常处理程序有bug导致无限异常循环(每条指令都触发异常),处理器会不断地清空流水线和处理异常,有效IPC降为接近零。某些实现增加了看门狗计时器来检测这种情况。

浮点异常的特殊处理

浮点异常(Floating-Point Exception)在处理器的异常机制中占有特殊地位。IEEE 754浮点标准定义了五种浮点异常条件:无效操作(Invalid Operation)、除零(Division by Zero)、上溢(Overflow)、下溢(Underflow)和不精确(Inexact)。这些"异常"与前面讨论的页错误等异常有本质区别:在大多数情况下,它们不会导致控制流转移到异常处理程序,而只是在浮点状态寄存器(如RISC-V的fflags)中设置相应的标志位。

浮点异常标志的累积特性与顺序更新

IEEE 754规定浮点异常标志是粘性的(sticky)——一旦被置位,只有软件显式清除才会复位。这意味着浮点异常标志的语义是:标志位反映从上次清除以来所有已执行浮点指令中是否发生过相应的异常条件。这一累积特性对乱序执行有重要影响:

  1. 浮点异常标志的更新必须按程序顺序进行——如果指令IjI_j先于IiI_i完成执行(乱序执行),但IiI_i在程序顺序上先于IjI_j,则IjI_j的异常标志更新不能先于IiI_i的。

  2. 推测执行的浮点指令如果触发了异常标志,这些标志不能被提前写入架构状态——因为推测指令可能最终被清除。

实现方案

在乱序处理器中,浮点异常标志的处理通常采用以下策略:

  • 浮点执行单元在计算结果的同时产生5位的异常标志。

  • 异常标志与计算结果一起写入ROB(或物理寄存器文件的附加字段)。

  • 在提交阶段,按程序顺序将每条浮点指令的异常标志OR到架构浮点状态寄存器fflags中。

  • 如果分支预测失败或异常导致指令被清除,其浮点异常标志也被丢弃,不会影响架构状态。

每条浮点指令在ROB中占用额外的5位用于存储异常标志。对于一个512条目的ROB,如果假设约30%的指令是浮点指令,则平均浮点异常标志的存储开销为512×5=2560512 \times 5 = 2560位(实际上不论是否为浮点指令,这5位字段都存在于ROB中——因为ROB表项的格式是统一的)。

浮点异常的trap模式

虽然大多数程序不使用浮点异常trap(陷阱模式),但IEEE 754标准允许软件启用浮点异常的trap——当启用时,发生浮点异常的指令应该像页错误一样触发控制流转移到异常处理程序。在乱序处理器中实现浮点trap模式面临一个根本性的挑战:浮点指令通常具有较长的执行延迟(如浮点除法可能需要10–30个周期),在执行完成之前无法知道是否会触发异常。如果在指令完成后才发现需要trap,此时后续的许多指令可能已经执行甚至修改了架构状态(在某些实现中),导致无法精确恢复。

解决这一挑战的方式有两种:

  1. 精确浮点trap:将浮点异常与其他异常同等处理——执行完成时标记异常,在提交时处理。由于ROB保证了顺序提交,这自然满足精确异常的要求。代价是浮点trap的处理延迟较大。

  2. 不精确浮点trap(Imprecise Floating-Point Trap):某些历史架构(如早期的Alpha)允许浮点异常以不精确方式处理——异常报告时的PC可能不是触发异常的那条浮点指令的PC,而是程序中某个"附近"位置。这简化了硬件实现,但给软件(特别是调试器)带来困难。现代处理器和ISA(包括RISC-V)通常不采用不精确浮点trap。

设计提示

RISC-V的浮点异常设计相对简洁:fflags是一个5位的CSR,每位对应IEEE 754的一种异常条件。RISC-V规范目前不定义浮点异常的trap机制——浮点异常只会设置fflags中的标志位,不会触发陷阱。这一设计极大地简化了乱序处理器的浮点异常处理:只需在提交时将异常标志OR到fflags即可,无需考虑浮点trap的精确性问题。如果未来需要浮点trap支持,可以通过扩展来添加,但核心规范保持了硬件实现的简洁性。

中断的处理

中断(Interrupt),也称为异步异常(Asynchronous Exception),是由外部事件触发的、与当前执行的指令无直接关联的事件。典型的中断包括定时器中断、外部设备中断(如网卡、磁盘控制器)和处理器间中断(Inter-Processor Interrupt,IPI)。中断的异步特性使其处理方式与同步异常有显著区别。

异步中断的接收与同步化

中断信号的传播路径

中断信号到达CPU核的路径通常经过以下步骤:

  1. 外部设备通过中断控制器(如RISC-V的PLIC)向目标CPU核发出中断请求。

  2. CPU核的中断输入引脚(或内部中断线)被拉高。

  3. CPU核内部的中断待决逻辑(interrupt pending logic)采样中断信号,并与当前的中断使能状态(mstatus.MIE)和中断使能寄存器(mie)进行比较。

  4. 如果中断被使能且优先级足够高,则产生内部的"take interrupt"信号。

与同步异常不同,中断不与任何特定指令关联——它可以在任何时刻到达。这意味着处理器需要选择一个中断点(Interrupt Point),即在程序中选择一条指令作为中断边界,使得该指令之前的所有指令在中断处理开始前已经提交,该指令及其之后的指令在中断处理后重新执行。

中断信号的同步化

中断信号来自外部时钟域(设备时钟),需要经过同步器(synchronizer)才能安全进入CPU核的时钟域。典型的同步器由2–3级触发器(flip-flop)串联组成,引入2–3个周期的同步延迟。这一延迟是中断响应延迟的固有组成部分,无法通过微架构优化消除。

对于来自CLINT的定时器中断和软件中断(它们在CPU核本地产生,可能已经在同一时钟域中),同步延迟可以更短或完全省略。

中断屏蔽机制

在某些情况下,中断必须被暂时屏蔽。RISC-V通过mstatus.MIE位提供全局中断使能控制,通过mie寄存器提供逐中断源的使能控制。在中断处理程序执行期间,MIE被硬件自动清零,防止中断处理程序被再次中断(除非软件显式重新使能中断以支持嵌套中断)。

从微架构角度,mstatusmie的值用于提交阶段的中断检查逻辑——只有当两者都指示中断使能时,待决中断才会被接受。

中断的采样时机

在现代流水线处理器中,中断信号通常在提交阶段被采样。具体而言,每当ROB头部的指令准备提交时,提交逻辑同时检查是否有待决中断。如果有,则不提交该指令,而是:

  1. 将该指令的PC写入mepc(这条指令将是中断返回后第一条重新执行的指令)。

  2. 将中断原因写入mcause(最高位为1表示中断)。

  3. 保存当前状态到mstatus

  4. 清除ROB中的所有表项(与异常恢复相同)。

  5. 将PC设置为中断处理向量地址。

这种在提交阶段采样中断的方式自然实现了中断的精确化——因为ROB头部的指令就是程序顺序上"最早的尚未提交的指令",它之前的所有指令已经提交,形成一个一致的架构状态。

中断与推测执行的微妙交互。一个值得注意的微架构细节是:中断信号的采样必须在非推测状态下进行。考虑以下场景:处理器正在推测执行一段错误路径上的代码,该代码包含一条csrw mie, zero指令(关闭中断使能)。如果在推测状态下采样了mie的值来决定是否接受中断,则可能错误地拒绝了一个本应接受的中断。因此,中断使能状态的采样必须基于提交后的架构状态mstatusmie的架构值),而非推测值。在实现中,这通常通过在提交阶段检查中断来自然满足——提交阶段只操作架构状态。

中断的精确化

虽然中断本质上是异步的,但现代处理器要求中断的处理也是精确的——即中断处理程序看到的架构状态必须对应程序中某个确定的指令边界。这一要求被称为中断的精确化(Precise Interrupt)。

中断点的选择策略

精确中断的实现在概念上比精确异常更为直接:由于中断不与任何特定指令绑定,处理器有自由选择中断点的位置。在基于ROB的实现中,中断点自然选择为ROB头部当前准备提交的指令。这一选择的优势在于:

  • ROB头部之前的所有指令已经提交,架构状态是一致的。

  • ROB头部及之后的指令可以通过标准的ROB清空机制丢弃。

  • 不需要额外的中断点选择逻辑。

另一种可能的选择是:不等待ROB头部指令就绪,而是选择ROB中最老的已完成指令作为中断点——提交该指令及其之前的所有已完成指令,然后进入中断处理。但这种方案更复杂,因为可能存在"最老的已完成指令之前还有未完成的指令"的情况,违反顺序提交的要求。因此,绝大多数实现选择在ROB头部采样中断的简单方案。

中断响应延迟分析

从中断信号到达CPU核到第一条中断处理程序指令开始执行的延迟,称为中断响应延迟(Interrupt Response Latency)。这个延迟由以下部分组成:

  1. 中断同步:中断信号从外部时钟域同步到核时钟域的延迟,通常2–3个周期。

  2. 中断识别:中断信号从输入引脚传播到提交逻辑的延迟。在现代处理器中,这通常是1–2个周期。

  3. 等待可提交:如果ROB头部的指令尚未完成执行,需要等待其完成。在最坏情况下,如果头部是一条cache miss的load指令,等待时间可能长达几百个周期。

  4. 流水线排空:清除ROB、恢复RAT、写入CSR的延迟,约5–10个周期。

  5. 取指和启动:从中断向量取指并开始执行的延迟,约5–15个周期。

设计提示

中断响应延迟对实时系统至关重要。在硬实时应用中(如工业控制、汽车电子),中断响应延迟必须有确定的上界。然而,乱序执行处理器的中断响应延迟具有很大的不确定性——它取决于ROB头部指令的执行延迟,而这可能从1个周期(ALU指令)到几百个周期(cache miss load)不等。这就是为什么许多实时系统选择使用顺序执行的简单处理器(如ARM Cortex-M系列),以获得可预测的中断响应延迟。

对于乱序处理器,一种减少最坏情况中断响应延迟的技术是中断强制排空(interrupt-forced drain):当检测到待决中断时,如果ROB头部指令是一条长延迟操作,处理器可以选择取消该操作(如果可以安全重试)并立即进入中断处理。但这增加了硬件复杂度,且不适用于所有类型的长延迟操作。

中断向量表的寻址方式

中断处理程序的入口地址通过中断向量表(Interrupt Vector Table)来确定。RISC-V通过mtvec寄存器支持两种向量模式:

(1)直接模式(Direct Mode, MODE=0)。所有异常和中断都跳转到同一个入口地址(mtvec的BASE字段)。异常处理程序通过读取mcause来判断异常/中断类型,并手动跳转到对应的处理函数。这种模式实现简单,但增加了中断分发的软件延迟。

(2)向量模式(Vectored Mode, MODE=1)。不同的中断类型跳转到不同的入口地址。具体而言,中断ii的入口地址为BASE+4i\text{BASE} + 4i。每个向量位置只有4字节(足够放一条跳转指令),用于跳转到实际的中断处理函数。异常仍然跳转到BASE地址(不使用向量化)。这种模式减少了中断分发的延迟(无需软件读取mcause并跳转),但需要更大的向量表空间。

从微架构角度,向量模式的地址计算需要一个加法器:target_pc=BASE+4×cause\text{target\_pc} = \text{BASE} + 4 \times \text{cause}。由于cause编码最多约16种,4×cause4 \times \text{cause}只需将cause左移2位,然后与BASE相加——这个计算可以在1个周期内完成,不增加额外的重定向延迟。

x86的中断描述符表(IDT)和ARM的中断向量表提供了类似的功能,但语义更复杂(x86的IDT条目包含段选择子和偏移量,ARM的向量表使用分支指令数组)。

中断优先级与仲裁

当多个中断同时待决时,处理器需要根据优先级选择最紧急的中断进行处理。RISC-V定义的中断优先级从高到低为:

  1. 机器外部中断(MEI)

  2. 机器软件中断(MSI)

  3. 机器定时器中断(MTI)

  4. 监管者外部中断(SEI)

  5. 监管者软件中断(SSI)

  6. 监管者定时器中断(STI)

在处理器内部,优先级编码器根据mip(中断待决寄存器)和mie(中断使能寄存器)的位来选择最高优先级的待决且使能的中断。硬件实现为:

pending_enabled=mipmie{MIE repeat}\text{pending\_enabled} = \texttt{mip} \wedge \texttt{mie} \wedge \{\text{MIE repeat}\}

然后对pending_enabled\text{pending\_enabled}的各位按上述优先级进行固定优先级编码,选出最高优先级的中断源。

嵌套中断

在进入中断处理程序时,RISC-V硬件自动将mstatus.MIE位清零,禁止新的中断打断当前的中断处理程序。如果操作系统需要支持嵌套中断(nested interrupt)——即允许高优先级中断打断低优先级中断的处理——需要在中断处理程序中手动将MIE位置1。嵌套中断的支持对微架构没有特殊要求(中断处理机制与普通异常处理相同),但对软件栈的设计提出了更高要求:每层嵌套都需要保存和恢复上下文(mepcmcause等CSR),这增加了中断处理延迟和栈空间消耗。

中断控制器接口

中断控制器是连接外部设备和CPU核的中间层,负责汇聚多个设备的中断信号、执行优先级仲裁、并将中断分发到目标CPU核。三大主流架构使用不同的中断控制器标准。

x86: APIC

x86平台的中断控制器由两部分组成:

  • Local APIC:每个CPU核内部一个,处理本地定时器中断、处理器间中断(IPI)等。

  • I/O APIC:位于芯片组中,汇聚来自外部设备的中断信号,通过系统总线将中断消息发送到目标CPU核的Local APIC。

Local APIC与CPU核的流水线接口通常是一组信号线:中断请求(INTR)、中断向量号(vector)和中断确认(EOI)。现代x86处理器的Local APIC通过MSR(Model-Specific Register)接口访问,支持通过wrmsr/rdmsr指令读写APIC寄存器。

ARM: GIC

ARM的GIC规范目前已演进到GICv4,支持虚拟化场景下的中断直通。GIC由以下组件构成:

  • Distributor:汇聚所有中断源(SPI、PPI、SGI),执行优先级排序和目标CPU选择。

  • Redistributor(GICv3+):每个CPU核一个,管理该核的中断状态。

  • CPU Interface:与CPU核流水线直接交互的接口。在GICv3中,CPU Interface通过系统寄存器(ICC_*寄存器)访问,而不是内存映射。

RISC-V: PLIC/CLINT/AIA

RISC-V的中断架构由以下组件构成:

  • CLINT(Core-Local Interruptor):提供每个核的定时器中断和软件中断。CLINT通过内存映射接口访问mtimemtimecmp寄存器。

  • PLIC:汇聚来自外部设备的中断信号,执行优先级仲裁,并向目标核发送外部中断(MEI/SEI)。PLIC通过内存映射寄存器访问。

PLIC与CPU核的接口相对简单:PLIC通过一根中断线向CPU核报告有待决中断,CPU核通过内存映射读取PLIC的claim寄存器获取中断号并确认中断。

案例研究 5 — RISC-V AIA(Advanced Interrupt Architecture)

RISC-V AIA是对原有PLIC的重大升级,引入了以下关键特性:

  • IMSIC(Incoming MSI Controller):每个hart(硬件线程)拥有一个IMSIC,支持基于消息的中断(MSI),取代了传统的中断线方式。外部设备通过向IMSIC的内存映射地址写入消息来触发中断,无需专门的中断线。

  • APLIC(Advanced PLIC):取代传统PLIC,支持将传统线中断转换为MSI,并支持更灵活的中断路由和优先级管理。

  • 中断文件(Interrupt File):每个hart的每个特权级都有一个中断文件,记录该特权级的所有待决外部中断。通过新的CSR(如mtopei)可以在1–2个周期内获取最高优先级的待决中断号,大幅减少中断分发延迟。

  • 虚拟化支持:AIA原生支持虚拟中断,允许hypervisor将中断直接注入guest OS,减少虚拟化中断的trap开销。

从微架构角度,AIA的IMSIC和新CSR接口比传统PLIC更高效:传统PLIC需要通过内存映射MMIO读写来claim/complete中断(每次MMIO操作可能需要几十到上百个周期),而AIA通过CSR访问可以在1–2个周期内完成中断识别,显著降低了中断响应延迟。

RISC-V中断架构:CLINT提供核本地的定时器和软件中断,PLIC汇聚外部设备中断并分发到目标CPU核。
RISC-V中断架构:CLINT提供核本地的定时器和软件中断,PLIC汇聚外部设备中断并分发到目标CPU核。
::: info 硬件描述 6 — 中断控制器与CPU核的硬件接口

从微架构实现的角度,中断控制器与CPU核之间的硬件接口可以分为两类:

(1)电平/脉冲信号接口。传统的中断接口使用一根或数根物理信号线连接中断控制器和CPU核。RISC-V的CLINT和PLIC使用这种方式:PLIC通过一根中断线(meipseip)向CPU核报告有待决的外部中断。CPU核在提交阶段采样该信号:如果meip为高且mstatus.MIE为1且mie.MEIE为1,则触发外部中断处理。这种接口简单可靠,但缺点是CPU核需要通过MMIO读取PLIC的claim寄存器来获取具体的中断号——这个MMIO读操作可能需要几十个周期(通过片上互联到达PLIC并返回)。

(2)消息信号中断(MSI)接口。现代中断架构(如x86的MSI/MSI-X、RISC-V AIA的IMSIC)采用基于消息的中断:设备通过向CPU核本地的特定内存地址写入一个包含中断号的消息来触发中断。IMSIC在接收到消息后,在本地的中断文件中设置对应的pending位,并通过内部信号通知CPU核。CPU核通过读取新的CSR(如mtopei)即可获取最高优先级的待决中断号,无需发起MMIO事务。这种方式将中断识别延迟从几十个周期缩短到1–2个周期。

对于面积敏感的设计,IMSIC的中断文件(可能包含数百到数千个中断位)的存储开销需要纳入考虑。典型的IMSIC实现为每个hart提供一个2048位的中断文件(支持2048个中断源),约占256字节的SRAM。

:::

中断与多核的交互

在多核系统中,中断控制器需要决定将中断发送到哪个核。这涉及中断的亲和性(affinity)和负载均衡(load balancing)。PLIC允许软件为每个中断源配置目标核列表(enable bits),并在多个目标核中选择一个接收中断——这种选择通常基于"先到先得"(first-claim wins)的策略:多个核同时读取claim寄存器时,只有一个核能成功获取中断号。GICv3/v4和x86 APIC提供了更灵活的路由机制,包括1-of-N分发(在一组核中选择一个)和广播模式。

从微架构角度,中断路由对单个核的流水线没有影响——它是中断控制器和系统软件的职责。但对于整体性能,合理的中断路由可以避免"中断风暴"(所有中断集中到一个核)导致某个核的频繁流水线冲刷,提高系统吞吐量。

处理器间中断(IPI)

处理器间中断(Inter-Processor Interrupt, IPI)是多核系统中一个核向另一个核发送的中断信号。IPI在操作系统中有多种用途:

  • TLB shootdown:当一个核修改了页表映射后,需要通知其他核刷新相应的TLB条目。发送IPI到其他核,其他核在中断处理程序中执行sfence.vma

  • 调度唤醒:当一个核上的线程唤醒了另一个核上正在等待的线程时,发送IPI通知目标核执行调度。

  • 远程函数调用:在核间执行同步操作。

在RISC-V中,IPI通过CLINT的软件中断寄存器实现:核A向核B的msip(Machine Software Interrupt Pending)寄存器写入1,即可向核B发送机器软件中断(MSI)。核B在中断处理程序中读取IPI的payload(通常通过共享内存传递),完成请求的操作,然后清除msip位。

从微架构角度,IPI与其他中断的处理方式完全相同——都是在提交阶段采样、清空流水线、跳转到中断向量。但IPI的频率可能比外部设备中断更高(特别是在TLB shootdown频繁的场景中),因此IPI的处理延迟对多核系统的整体性能影响更大。一些高性能处理器优化了IPI的响应路径——例如使用专用的IPI寄存器接口(而非MMIO),或在检测到IPI时优先排空ROB中的当前指令。

性能分析 3 — IPI驱动的TLB shootdown的性能影响

TLB shootdown是多核系统中最频繁的IPI使用场景之一。其完整流程如下:

  1. 核A修改页表映射(例如释放一个页面)。

  2. 核A执行本地sfence.vma刷新自己的TLB。

  3. 核A通过IPI通知其他核(核B, C, D)需要刷新TLB。

  4. 核B/C/D接收IPI,进入中断处理程序,执行sfence.vma

  5. 核B/C/D完成TLB刷新,通过共享内存变量通知核A。

  6. 核A等待所有核完成TLB刷新,然后继续执行。

每个核接收IPI并完成TLB刷新的延迟约为:

  • 中断响应延迟:\sim20–50周期(等待ROB头部指令完成+流水线清空)。

  • 中断处理程序执行:\sim50–100周期(保存上下文+执行sfence.vma+恢复上下文)。

  • 总延迟:\sim70–150周期/核。

在一个4核系统中,如果TLB shootdown频率为每100微秒一次(10 kHz),且每次shootdown需要3个核响应,则每个核因IPI导致的流水线冲刷开销约为:

3×100 cycles×10000 Hz3×109 Hz0.1%\frac{3 \times 100 \text{ cycles} \times 10000 \text{ Hz}}{3 \times 10^9 \text{ Hz}} \approx 0.1\%

这个开销看起来很小,但在数据库等频繁修改页表映射的工作负载中,TLB shootdown的频率可能高出数倍,导致不可忽视的性能损失。RISC-V的sfence.vma支持指定ASID和虚拟地址范围,可以减少不必要的TLB刷新范围。

序列化指令的处理

不是所有指令都可以像普通ALU指令那样在执行完成后简单地等待提交。某些指令在离开流水线(提交或排出)时有特殊的约束条件,这些约束保证了程序的正确性和内存一致性模型的要求。

什么是序列化指令

序列化指令(Serializing Instruction)是要求其之前所有指令的效果已经全局可见后、才能开始执行的指令,同时它之后的指令必须等到它完成后才能开始执行。序列化指令在乱序流水线中形成一个严格的"屏障"(barrier),将指令流分成"之前"和"之后"两部分。

序列化指令的种类

不同ISA定义了不同的序列化指令:

  • Fence指令:RISC-V的fence指令用于强制内存操作的顺序。fence的参数指定了需要排序的操作类型(如fence rw, rw要求所有先前的读写操作在所有后续的读写操作之前完成)。在微架构层面,fence指令通常需要等待Store Buffer排空(所有已提交的store都已排出到缓存),然后才允许后续的load/store进入内存系统。

  • Fence.i指令:RISC-V的fence.i用于同步指令缓存和数据缓存。在支持自修改代码(self-modifying code)的场景中,fence.i确保之前的store(可能修改了代码区域的数据)对后续的取指可见。这通常需要刷新I-Cache或至少使相关缓存行失效。

  • CSR指令:RISC-V的CSR读写指令(csrrwcsrrscsrrc)在修改影响处理器行为的控制状态寄存器时,需要序列化处理。例如,修改satp(页表基地址寄存器)后,后续的所有内存访问必须使用新的地址映射——这要求flush TLB并等待所有先前的内存操作完成。

  • SFENCE.VMA:RISC-V的TLB刷新指令,需要同步TLB和页表。它通常实现为序列化指令:等待所有先前指令提交,刷新指定范围的TLB条目,然后才允许后续指令执行。

  • x86的CPUID/WRMSR:在x86中,cpuid是完全序列化的指令(serializing instruction),所有先前指令必须完成、所有先前store必须对外可见、所有先前的修改都必须生效。wrmsr写入MSR时也通常需要序列化。

序列化指令的流水线实现

在乱序处理器中,序列化指令的处理通常分为两个阶段:

阶段一:排空(Drain)

序列化指令被重命名并分配ROB表项后,暂停后续指令的分配(allocation stall),等待所有先于该指令的指令提交。同时,如果序列化指令要求Store Buffer排空(如fence指令),还需要等待所有已提交的store排出到缓存。这一阶段的持续时间取决于ROB中现存指令的数量和执行延迟。

排空过程中的关键时序关系:

  • 排空开始时间:序列化指令到达重命名阶段(或ROB分配阶段)。

  • 排空结束条件:序列化指令成为ROB中最老的指令(到达ROB头部),且所有需要排空的结构(如Store Buffer)已清空。

  • 排空期间:正确路径上的先序指令继续正常执行和提交,但不分配新指令。

阶段二:执行与恢复

所有先序指令提交后,序列化指令本身被执行(如CSR读写、TLB刷新)。执行完成后,序列化指令提交。此时后续指令的分配恢复,它们可以看到序列化指令的效果(如新的页表映射、新的CSR值)。

需要注意的是,序列化指令的"排空"与异常/中断的"流水线冲刷"有本质区别:排空是一个渐进过程——处理器不主动丢弃任何指令,只是等待已有指令自然完成和提交;而异常导致的冲刷是立即的——所有推测状态被强制清除。排空期间,ROB中的先序指令仍然正常执行和提交,只是不再分配新的指令。当ROB最终变为空(或序列化指令到达头部)时,排空完成。

对流水线性能的影响

性能分析 4 — 序列化指令的性能代价

序列化指令的性能代价取决于排空阶段的时间长度。在最坏情况下,排空需要等待ROB中最慢的指令完成执行——如果ROB中有一条L3 cache miss的load(可能需要几十到上百个周期),则序列化指令也需要等待这么久。排空期间,后端没有新的指令被分配,流水线几乎空转。

以RISC-V的sfence.vma为例,在操作系统上下文切换时,典型的排空时间约为50–100个周期(取决于ROB深度和当前负载)。如果上下文切换频率为每毫秒一次(1000 Hz),在一个3 GHz处理器上,sfence.vma的排空开销约为:

100 cycles3×106 cycles/ms0.003%\frac{100 \text{ cycles}}{3 \times 10^6 \text{ cycles/ms}} \approx 0.003\%

这个开销很小。但在某些高频率的场景中(如虚拟化环境中频繁的VM exit/entry),序列化指令的累积开销可能变得显著。

更直观地理解排空代价:一个6-wide、ROB深度512的处理器,如果ROB中有200条指令需要排空,平均每周期提交4条指令,则排空需要约50个周期。在此期间,本来可以分配的6×50=3006 \times 50 = 300条新指令被延迟了——这300条指令的执行被推迟了至少50个周期。

序列化的优化技术

完全序列化的实现过于保守——它要求所有先序指令完成后才执行序列化指令,这可能导致流水线长时间空转。某些微架构优化可以减轻这一开销:

选择性排空

对于fence指令,只需要等待Store Buffer排空即可,不需要等待所有ROB中的指令(如纯ALU指令)完成。更进一步,fence r, r(只排序读操作)甚至不需要排空Store Buffer,只需要确保所有先前的load已经从缓存获取了数据。

RISC-V的fence指令支持细粒度的排序控制:fence指令有predecessor(前驱)和successor(后继)两组操作类型位,分别指定需要排序的先前操作和后续操作。例如:

  • fence w, w:只排序写操作——等待Store Buffer中的先前store排出,后续store在此之前不能排出。不影响load操作。

  • fence r, rw:先前的load必须在后续的load和store之前完成。不需要排空Store Buffer。

  • fence rw, rw:完全屏障——等价于x86的mfence

  • fence.tso:TSO语义——等价于fence rw, rw但允许store后跟load的重排。这是RVTSO扩展隐含的语义。

选择性排空可以显著减少fence的性能开销。例如,fence w, w只需要等待Store Buffer排空(通常10–30个周期),而不需要等待ROB中可能存在的长延迟load完成。

非阻塞fence实现

某些高级微架构(如ARM的某些核心)实现了非阻塞fence——fence指令被重命名和分配后不暂停后续指令的分配,而是在LSU内部通过标记机制确保fence之前和之后的内存操作不会乱序。具体实现方式:

  1. Fence指令被分配一个ROB条目和一个Store Queue条目(或专用的fence条目)。

  2. 后续的load/store指令正常进入发射队列和LSQ,但被标记为"在fence之后"。

  3. LSU的调度逻辑确保"在fence之后"的内存操作不会在fence之前的内存操作完成前执行。

  4. 这种排序约束通过LSQ中的fence标记和年龄比较实现,不需要暂停分配。

这种实现显著减少了fence的性能惩罚,但硬件复杂度更高——LSU需要额外的排序逻辑来处理fence标记。

CSR重命名

对于频繁读写的CSR(如浮点fflags),可以将其纳入寄存器重命名体系,像普通寄存器一样处理,避免序列化。RISC-V的fflags/frm在某些实现中被重命名以提高浮点代码的性能。

CSR重命名的挑战在于CSR的语义与普通寄存器不同:

  • fflags的更新是OR操作(粘性标志),不是简单的覆盖。

  • 某些CSR读写有副作用(如读取mtime返回当前时间)。

  • CSR空间中有大量寄存器(RISC-V定义了4096个CSR地址),不可能全部重命名。

实践中,只有少数高频CSR(fflagsfrmfcsr)值得重命名,其他CSR仍然使用序列化方式处理。

fflags重命名的具体实现。由于fflags是粘性标志(OR语义),其重命名比普通寄存器更复杂。一种实现方案如下:

  1. fflags分配独立的物理寄存器。每条浮点指令在重命名阶段,除了分配普通的结果物理寄存器外,还分配一个"fflags物理寄存器"。

  2. 浮点执行单元将计算产生的5位异常标志写入新的fflags物理寄存器。但写入的值不是简单的5位标志,而是旧fflags值与新标志的OR结果:

new_fflags_phys=old_fflags_physthis_inst_flags\text{new\_fflags\_phys} = \text{old\_fflags\_phys} \lor \text{this\_inst\_flags}
  1. 后续的csrr fflags指令读取最新的fflags物理寄存器即可获取累积的异常标志。

  2. 分支预测失败时,fflags的物理寄存器映射通过正常的检查点/WALK机制恢复。

这种方案的性能优势在于:浮点密集代码中的csrr fflags指令不再需要序列化——它只是读取一个物理寄存器,可以像普通load一样被调度和执行。

设计权衡 2 — 序列化实现的复杂度与性能

序列化指令的实现在复杂度和性能之间存在明显的权衡:

  • 完全排空:实现最简单——检测到序列化指令时,暂停分配,等待ROB清空,然后执行。性能最差,因为流水线完全排空再重新填充的代价很高。

  • 选择性排空:根据序列化指令的语义,只等待必要的先序操作完成。例如,fence w, w只需等待Store Buffer中所有先前store排出,而不需要等待load和ALU指令。实现复杂度中等,性能显著优于完全排空。

  • 非阻塞实现:序列化指令不暂停后续指令的分配和执行,而是在内存子系统内部通过标记和排序机制保证语义。实现最复杂(需要在LSU、Store Buffer、Load Queue中增加排序逻辑),但性能最好——后续指令可以在序列化指令之后立即开始执行(只要它们不与序列化约束冲突)。

现代高性能处理器通常对不同类型的序列化指令采用不同的策略:fence指令趋向非阻塞或选择性排空,CSR写入采用完全排空(因为CSR修改可能影响整个处理器行为),sfence.vma采用完全排空加TLB刷新。

值得注意的是,RISC-V的弱内存模型(RVWMO)相比x86的TSO模型天然减少了对fence指令的需求。在RVWMO下,只有当软件需要确保特定的内存顺序时才使用fence,而x86的TSO模型虽然对普通load/store提供更强的顺序保证,但其mfence指令(完全屏障)的代价也更高,因为它需要排空整个Store Buffer并确保所有缓存一致性操作完成。

序列化指令(fence)的流水线效果。Fence指令将指令流分为先序和后续两部分,在排空阶段流水线近乎空转,产生性能气泡。
序列化指令(fence)的流水线效果。Fence指令将指令流分为先序和后续两部分,在排空阶段流水线近乎空转,产生性能气泡。

Store指令的排出约束

Store指令是流水线中最特殊的指令类型之一。它的核心约束是:store的数据在提交前不能对其他处理器核或外部观察者可见。这是因为提交前的store可能处于错误的推测路径上——如果分支预测失败或异常导致store被清除,则该store不应该在内存系统中留下任何痕迹。

Store Buffer的角色

为了满足这一约束,乱序处理器使用Store Buffer(也称为Store Queue的已提交部分)来缓存已执行但尚未提交的store数据。store指令的执行过程如下:

  1. 地址计算:store指令在执行阶段计算目标地址,结果写入Store Queue的地址字段。

  2. 数据就绪:store的数据操作数就绪后写入Store Queue的数据字段。地址和数据可以在不同时刻就绪。

  3. 提交:当store指令到达ROB头部准备提交时,提交逻辑将其标记为"已提交"(committed)。此时store的数据仍然在Store Buffer中,尚未写入缓存。

  4. 排出(Drain):已提交的store按照先进先出(FIFO)顺序从Store Buffer排出,通过缓存写端口将数据写入L1 D-Cache。排出过程可能因为D-Cache端口冲突、缓存未命中、或一致性协议阻塞而延迟。

Store-to-Load转发

在store数据尚未排出到缓存期间,如果有后续的load指令访问相同地址,处理器通过Store-to-Load转发(Store-to-Load Forwarding)机制直接从Store Buffer中将数据转发给load,避免load读到旧的缓存数据。转发逻辑需要在Store Buffer中用load的地址进行内容可寻址匹配(CAM lookup),找到程序顺序上最近的、地址匹配的store条目。

转发匹配的条件为:

  1. Store在程序顺序上先于load(通过ROB年龄比较确定)。

  2. Store的地址已经计算完成(地址字段有效)。

  3. Store的地址与load的地址在有效覆盖范围内匹配(例如,一条word store可以转发给一条byte load,只要byte落在word范围内)。

  4. Store的数据已经就绪。

如果存在多个地址匹配的store,选择程序顺序上最近(最新)的那个。如果最新的匹配store的数据尚未就绪,load必须等待。

Store Buffer的排出策略与溢出

Store Buffer的排出(drain)策略影响存储系统的带宽和延迟:

  • 逐条排出:每个周期排出一条store到缓存。这是最简单的实现,但在store密集的代码中可能成为瓶颈。

  • 多条并行排出:某些处理器支持每周期排出2条或更多store,但这需要D-Cache有多个写端口,面积和功耗开销较大。

  • Store合并(Store Coalescing / Write Combining):将多条连续地址的store合并为一次缓存写入,提高带宽利用率。这在初始化大数组或memset操作中特别有效。

硬件描述 7 — Store Buffer溢出与流水线暂停

Store Buffer的容量有限(典型值为32–72个条目)。当Store Buffer满时,新的store指令无法在ROB中被分配(因为没有Store Queue条目可用),导致流水线暂停(allocation stall)。更严重的是,即使ROB中有已提交的store等待排出,如果D-Cache持续busy(例如因为缓存替换导致的writeback),排出速率可能低于store提交速率,导致Store Buffer逐渐填满。

在store密集的工作负载中(如矩阵写入、内存复制),Store Buffer溢出是常见的性能瓶颈。缓解方法包括:

  • 增大Store Buffer容量(但受面积约束,现代处理器的Store Buffer通常不超过72–128个条目)。

  • 提高排出带宽(多端口D-Cache或non-temporal store绕过缓存直接写入Write Combining Buffer)。

  • 使用non-temporal store(如x86的movnti、ARM的STNP)绕过缓存层次,直接写入Write Combining Buffer并以全缓存行粒度排出到下级缓存或内存。

Store Queue在恢复中的角色

Store Queue在流水线恢复中扮演着特殊角色,因为它同时管理着推测状态(未提交的store)和架构状态(已提交但未排出的store)。

Store Queue的双重区域。在大多数实现中,Store Queue被逻辑上分为两个区域:

  • 推测区域(Speculative Region):从Store Queue的"提交指针"(commit pointer)到尾指针之间的条目。这些store尚未提交,可能被分支预测失败或异常清除。

  • 已提交区域(Committed Region):从Store Queue的头指针到提交指针之间的条目。这些store已经提交,是架构状态的一部分,不会被任何恢复事件清除,等待排出到D-Cache。

恢复事件对Store Queue的影响取决于恢复范围:

  • 分支预测失败:将Store Queue尾指针回退到检查点保存的位置(或WALK过程中逐步回退),推测区域中属于错误路径的条目被丢弃。已提交区域不受影响。

  • 异常/中断:清除推测区域中的所有条目(尾指针回退到提交指针位置)。已提交区域不受影响——已提交的store在异常处理期间继续排出到D-Cache。

Store Queue排空的等待。对于序列化指令(如fence),不仅要等待推测区域中的所有store提交,还需要等待已提交区域中的所有store排出到D-Cache。这两个等待条件不同:

  1. 推测区域清空:等待ROB按序提交,将store从推测区域移到已提交区域。

  2. 已提交区域清空:等待所有已提交的store通过D-Cache写端口排出到缓存。

如果已提交区域中有一条store的目标地址D-Cache miss(需要从L2/L3获取缓存行并分配替换),排出延迟可能达到几十个周期。在这种情况下,即使ROB已经清空,fence仍需等待Store Buffer排空才能完成。

Store Queue与恢复操作的并发处理。在恢复事件发生时,Store Queue需要同时处理两个方向的操作:

  1. 推测侧清除(从尾部收缩):将尾指针回退到检查点保存的位置,丢弃错误路径上的store条目。

  2. 排出侧继续(从头部排出):已提交区域中的store继续正常排出到D-Cache。这些store是架构状态的一部分,不受恢复事件影响。

这两个操作可以并行进行——推测侧的指针回退是1周期操作(检查点恢复)或逐步操作(WALK恢复),排出侧的D-Cache写入在独立的端口上进行。它们操作Store Queue的不同区域,不存在冲突。这种并行性是Store Queue分离推测区域和已提交区域的设计优势之一。

硬件描述 8 — Store Queue恢复的指针操作

Store Queue使用三个指针管理其状态:

  • Head指针HSQH_{\text{SQ}}):指向最老的已提交但未排出的store。当一条store被排出到D-Cache后,HSQH_{\text{SQ}}前进1位。

  • Commit指针CSQC_{\text{SQ}}):指向最老的未提交store。当一条store被ROB提交时,CSQC_{\text{SQ}}前进1位。CSQC_{\text{SQ}}始终满足HSQCSQTSQH_{\text{SQ}} \le C_{\text{SQ}} \le T_{\text{SQ}}(模NN比较)。

  • Tail指针TSQT_{\text{SQ}}):指向下一个可分配的空闲位置。新的store指令分配时从此处获取条目,TSQT_{\text{SQ}}前进1位。

恢复操作对这些指针的影响:

  • 分支预测失败(检查点恢复):TSQTkT_{\text{SQ}} \leftarrow T_{k}(检查点中保存的尾指针)。HSQH_{\text{SQ}}CSQC_{\text{SQ}}不变。

  • 异常/中断:TSQCSQT_{\text{SQ}} \leftarrow C_{\text{SQ}}(清除所有未提交的store)。HSQH_{\text{SQ}}不变(已提交的store继续排出)。

Store Queue的容量约束为:TSQHSQ<NSQT_{\text{SQ}} - H_{\text{SQ}} < N_{\text{SQ}}(模NN),其中NSQN_{\text{SQ}}是Store Queue的条目数。当Store Queue满时,新的store指令被暂停分配。

Store指令与异常/中断的交互

Store指令的延迟排出特性使得它与异常和中断的交互需要特别注意:

  • 已提交的store不受影响:一旦store指令在ROB中被正常提交,它就成为了架构状态的一部分。即使在store排出到缓存之前发生了中断,store仍然保留在Store Buffer中等待排出——中断处理程序返回后,这些store会继续排出。中断不会也不应该丢弃已提交的store。

  • 未提交的store被清除:如果异常或分支预测失败导致store指令被清除,其在Store Queue中的条目也一并清除,store的数据永远不会进入缓存系统。

  • store引起的异常:如果store指令的地址触发了页错误(例如写入只读页面),这个异常的处理与load的页错误相同——标记在ROB中,等到提交时精确处理。在store地址计算阶段发现的页错误意味着store数据根本不会被写入Store Buffer的数据字段(或即使写入也会在异常处理时被清除),保证了精确异常的语义。

原子操作与Store Buffer的特殊约束

对于原子内存操作(如RISC-V的AMO指令和LR/SC对),Store Buffer的管理需要额外的约束:

  • AMO指令:原子读-修改-写操作(如amoadd.w)需要在执行时同时完成读和写,并且保证原子性。在乱序处理器中,AMO指令通常被拆分为一对微操作:先执行一个原子load,然后执行计算和原子store。这对微操作必须在L1 D-Cache的同一端口上原子地完成,期间不允许其他核的缓存一致性请求(snoop)介入。

  • LR/SC(Load-Reserved/Store-Conditional):LR指令在目标地址上设置一个保留标记(reservation),后续的SC指令检查该标记是否仍然有效。如果有效,SC成功执行store并返回0;否则SC失败,不执行store,返回非0值。保留标记在以下情况下被清除:其他核写入了保留地址所在的缓存行(通过缓存一致性协议的invalidation)、或者发生了中断/异常(RISC-V规范允许中断清除保留标记)。在微架构层面,保留标记通常实现为一个特殊的寄存器,记录LR的地址和有效位。

存储器顺序违规的恢复

除了分支预测失败和异常之外,还有第四类需要流水线恢复的事件:存储器顺序违规(Memory Order Violation)。在乱序执行的处理器中,load指令可能在更早的store指令之前执行。如果这条推测提前执行的load读取的地址与后来计算出的store地址重叠,则load读到了错误的数据——这就是存储器顺序违规。

违规的检测

存储器顺序违规的检测发生在store指令的地址计算完成时。当一条store的地址被计算出来后,LSU将该地址与Load Queue中所有更年轻已执行的load的地址进行比较。如果发现某条load的地址与该store重叠,且该load已经从缓存(而非Store Buffer)中读取了数据,则检测到存储器顺序违规。

具体比较逻辑为:对于Load Queue中的每个条目ii

violationi=validiexecutediyoungeriaddr_match(i,store)\text{violation}_i = \text{valid}_i \wedge \text{executed}_i \wedge \text{younger}_i \wedge \text{addr\_match}(i, \text{store})

其中youngeri\text{younger}_i通过ROB年龄比较确定,addr_match\text{addr\_match}检测地址是否重叠(考虑load和store的不同宽度)。

违规的恢复方式

检测到存储器顺序违规后,处理器有以下恢复选项:

(1)从违规load处重新执行。最常见的方案是将违规load及其之后的所有指令清除,然后从违规load处重新取指和执行。这等价于在违规load的位置触发了一个"类异常"事件。恢复可以使用检查点(如果违规load对应一个检查点)或WALK(从ROB tail走到违规load的位置)来恢复RAT。

(2)仅重新执行违规load。一种更精细的方案是只重新执行违规load本身(从Store Buffer中获取正确的数据),然后重新执行所有依赖于该load结果的后续指令。这种方案避免了丢弃不依赖于违规load的无辜指令,但实现复杂度很高——需要追踪数据依赖链并选择性地重新执行。

(3)对性能的影响。存储器顺序违规在大多数工作负载中较少发生(每百万条指令约10–100次),因为现代处理器在store地址已知前会推迟可能冲突的load(通过store-set预测器或地址预测)。但在某些模式中——如多线程共享数据结构、或store地址计算依赖于长延迟操作——违规率可能较高,此时恢复开销对性能的影响不可忽视。

性能分析 5 — 存储器顺序违规的频率与代价

在SPECint2017基准测试中,存储器顺序违规的发生频率约为每百万条指令10–200次,远低于分支预测失败(约每百万条指令2000–5000次)。但每次违规的恢复代价可能较大:

  • 如果使用"从违规load处重新执行"方案,恢复代价与分支预测失败类似(10–20周期的重定向延迟)。

  • 如果没有为load维护检查点(通常只有分支指令才有检查点),则需要使用WALK恢复或架构状态恢复,延迟更大。

  • 现代处理器(如Intel Golden Cove)使用"机器清除"(machine clear)来处理存储器顺序违规——这实际上是一次完整的流水线冲刷,类似于异常处理,代价约30–100周期。

减少存储器顺序违规的关键技术是store-set预测器:它记录哪些load/store对历史上发生过违规,在后续遇到相同的load时,将其调度到相关store之后执行,主动避免违规。

存储器顺序违规的具体例子。考虑以下代码序列:

  1. I0: sw x5, 0(x1) ——store,地址依赖于x1

  2. I1: add x1, x2, x3 ——计算x1(I0的地址依赖于此!但在乱序中I0先被分配)

  3. I2: lw x6, 0(x4) ——load,地址已知为0x1000

  4. I3: add x7, x6, x8 ——依赖于I2的结果

假设I0的地址(0(x1))最终等于0x1000(与I2的地址相同),但在乱序执行中,I2先于I0计算出地址并从D-Cache读取了旧数据。当I0最终计算出地址0x1000时,LSU检测到I2(更年轻)已经读取了这个地址——但I2读到的是I0写入前的旧值,应该读到I0写入后的新值。

此时LSU报告存储器顺序违规,处理器需要从I2开始重新执行。I2和I3(以及之后的所有指令)被清除,I0正常提交其store,然后I2重新执行时通过Store-to-Load转发获取I0写入的正确值。

设计提示

存储器顺序违规的恢复是乱序处理器中最隐蔽的性能杀手之一。与分支预测失败不同,存储器顺序违规的发生不易从代码分析中预测——它取决于运行时的地址值,而地址值可能受输入数据的影响。在调优高性能代码时,硬件性能计数器中的"machine clear"或"memory disambiguation clear"事件可以帮助识别存储器顺序违规热点。解决方法通常是在源代码层面调整数据布局(避免store和后续load访问同一地址),或使用编译器屏障提示(如volatile限定符)来防止编译器产生可能导致违规的代码。

推测执行与安全:恢复机制的微架构副作用

近年来,Spectre和Meltdown系列漏洞揭示了一个深刻的问题:即使推测执行的指令最终被恢复机制丢弃,它们在执行期间对微架构状态(如缓存、TLB、分支预测器)产生的副作用仍然存在——这些副作用可以被攻击者通过侧信道(side channel)观测到,从而泄露敏感信息。

恢复机制的不完整性

本章前面讨论的恢复机制——无论是检查点恢复、WALK恢复还是架构状态恢复——都只恢复架构状态(寄存器映射、ROB、发射队列等),而不恢复微架构状态(缓存内容、TLB内容、分支预测器状态等)。这意味着推测执行的指令虽然在架构层面"消失"了,但它们对缓存的影响(加载了特定的缓存行)仍然保留。

Spectre v1利用分支预测失败恢复前的推测内存访问,将越界读取的数据"编码"到缓存状态中;Spectre v2利用间接分支预测污染来执行攻击者选择的代码片段。攻击者通过后续的cache timing侧信道来"解码"推测执行泄露的信息。

硬件缓解措施

这些安全问题对恢复机制的设计提出了新的要求:不仅要正确恢复架构状态,还要尽量减少推测执行对微架构状态的可观测影响。典型的硬件缓解措施包括:

  • 推测屏障(如x86的lfence、ARM的CSDB):阻止推测执行跨越屏障。在屏障之后的指令不能在屏障之前的分支解析完成前开始执行。

  • 推测性缓存访问限制:推测load不写入缓存(或写入但在恢复时撤销)。这种方案称为"invisible speculation",可以完全消除缓存侧信道,但性能代价很大——推测load无法利用缓存的数据,需要重新从下级存储获取。

  • 分支预测器隔离:不同安全域之间隔离分支预测器状态(如Intel的IBRS/STIBP)。这防止攻击者通过训练受害者的分支预测器来控制推测执行方向。

  • 延迟转发:推测load的结果延迟转发给后续的地址计算指令,防止推测数据影响后续访问模式。这在Apple的M系列处理器中有类似的实现。

安全与性能的权衡是现代处理器设计面临的重大挑战。完全消除推测执行的微架构副作用意味着禁止推测——这将使处理器性能退回到顺序执行的水平。现实中的解决方案通常是有针对性地限制最危险的推测行为,同时保留大部分推测执行的性能优势。

恢复与缓存子系统的交互

流水线恢复需要考虑与缓存子系统的交互。当分支预测失败或异常导致指令被清除时,这些指令可能已经在缓存子系统中触发了各种操作(cache miss请求、预取请求、TLB miss请求等)。这些"悬挂"(outstanding)的请求应该如何处理?

(1)L1 D-Cache miss请求。如果错误路径上的一条load指令触发了L1 D-Cache miss,该miss请求可能已经被分配到Miss Status Holding Register(MSHR)中,并向L2 Cache发出了请求。恢复时有两种处理方式:

  • 让请求完成:不取消已经发出的请求,让数据返回后正常写入L1 D-Cache。虽然这条load指令被丢弃了,但其加载的数据可能在将来被正确路径的指令使用——提前将数据加载到缓存中实际上起到了预取效果。但缺点是可能导致"cache pollution"——无用的数据占据了缓存行,驱逐了可能更有用的数据。

  • 取消请求:尝试取消MSHR中属于错误路径的miss请求。但这在实现上很困难,因为:(a)请求可能已经通过互联发送到L2,取消需要发送cancel消息;(b)如果正确路径的其他load也在访问同一缓存行(MSHR合并),取消会误伤正确的请求;(c)取消逻辑增加了MSHR的复杂度。

大多数现代处理器选择让请求完成的策略——虽然这可能导致少量的cache pollution,但实现简单且在很多情况下还能受益于预取效果。一项对SPECint2017的分析表明,错误路径上的cache miss请求中约30%–40%在正确路径重新执行时会被再次访问——也就是说,让这些请求完成实际上起到了"免费预取"的作用。但剩下的60%–70%确实是cache pollution,在某些情况下可能驱逐了正确路径将要使用的缓存行。

MSHR(Miss Status Holding Register)的管理。当错误路径的load触发L1 miss时,该miss请求占用一个MSHR条目。如果MSHR被错误路径的请求填满,正确路径的cache miss将无法获得MSHR条目,导致性能下降。这是错误路径执行的一个隐藏代价。某些实现在MSHR条目中标记"推测级别"——高推测级别的请求可以被低推测级别的请求驱逐,确保正确路径的miss总能获得MSHR资源。

(2)TLB miss请求。错误路径上的指令可能触发了TLB miss,启动了硬件页表遍历器(Hardware Page Table Walker, PTW)。处理方式与cache miss类似:通常让PTW完成遍历,将新的映射写入TLB。但如果PTW遍历本身会修改页表条目的Accessed/Dirty位(某些架构要求硬件设置这些位),则可能导致错误路径的指令修改了架构状态(页表),违反了"推测执行不影响架构状态"的原则。

RISC-V通过将Accessed/Dirty位的管理留给软件(通过异常触发)来避免这个问题——PTW不会修改页表条目,只是读取。但x86和ARM需要特别小心推测PTW对A/D位的影响。

(3)预取请求。错误路径上的指令可能触发了硬件预取器。这些预取请求通常是"尽力而为"(best-effort)的——它们不保证执行,可以被安全地忽略或丢弃。恢复时不需要特别处理预取请求。

特殊指令的流水线处理

除了序列化指令外,还有几类特殊指令在与恢复机制交互时需要特殊处理。

WFI指令的处理

RISC-V的wfi(Wait For Interrupt)指令是一类需要特殊处理的指令。wfi指示处理器进入低功耗等待状态,直到有中断待决时才被唤醒。从微架构角度,wfi的处理方式有两种:

  • 作为序列化指令处理:等待所有先序指令提交后,处理器进入空闲状态,关闭时钟门控(clock gating)以降低功耗。当中断到达时,恢复时钟并从wfi的下一条指令继续取指。具体实现步骤为:(1)等待ROB排空;(2)关闭取指、解码、执行单元的时钟;(3)保持提交逻辑中的中断采样逻辑活跃(这部分不能被clock gate);(4)当中断信号被检测到时,恢复时钟,从wfi之后的PC开始取指。

  • 作为NOP处理:某些实现允许将wfi当作空操作,不实际进入低功耗状态。RISC-V规范允许这种实现——wfi只是一个"提示"(hint),处理器不一定要真正进入等待状态。这种实现的优势是简单,但在等待中断的场景中会浪费功耗。

WFI的特权级检查。在S/U-mode下执行wfi时,如果mstatus.TW位为1,则wfi会触发非法指令异常而非进入等待状态。这一机制允许hypervisor拦截guest OS的wfi调用,以便在虚拟化环境中管理CPU资源。

WFI与时钟门控的微架构实现。当处理器决定真正进入wfi等待状态时,时钟门控的实现需要精心设计:

  • 可以关闭的时钟域:取指单元、解码器、重命名逻辑、发射队列、执行单元、ROB。这些模块在等待期间没有指令需要处理,关闭时钟可以将动态功耗降为零。

  • 必须保持活跃的时钟域:中断检测逻辑(需要采样外部中断信号)、定时器比较逻辑(需要比较mtimemtimecmp)、调试接口(需要响应调试请求)。

  • 唤醒逻辑:当中断检测逻辑发现有待决中断时,产生wakeup信号,该信号异步地使能所有被关闭的时钟域。由于时钟域恢复需要几个周期的稳定时间(PLL重新锁定等),wfi的唤醒延迟通常比正常的中断响应延迟多3–10个周期。

MRET/SRET指令的处理

异常/中断返回指令(mretsret)是另一类与恢复机制密切相关的特殊指令。它们的微架构处理需要注意以下要点:

  1. 序列化mret修改特权级和中断使能状态,必须作为序列化指令处理——等待所有先序指令提交后再执行。

  2. PC重定向:执行mret时,PC被设置为mepc的值。由于mepc是一个CSR,其值在mret到达执行阶段时才能确定。前端在解码mret时无法预测返回地址(除非使用专门的异常返回地址预测器,但这很罕见)。

  3. TLB一致性:如果mret从M-mode返回到S/U-mode,可能伴随着地址空间的切换(satp可能在异常处理程序中被修改)。某些实现在mret执行时隐式执行TLB刷新以确保一致性。

  4. 中断使能恢复mretmstatus.MPIE的值恢复到MIE位,重新使能中断。如果此时有待决中断,处理器可能在mret返回后的第一条指令处立即响应中断——导致"中断抖动"(interrupt jitter)。在某些实现中,mret之后允许至少执行一条指令再响应新的中断。

  5. 状态恢复的原子性mret需要同时修改多个架构状态字段(PC、特权级、中断使能),这些修改必须原子地生效——不能出现"PC已经跳转到用户态代码但特权级仍然是M-mode"的中间状态。在硬件实现中,这通过将所有状态修改放在mret的提交阶段同一周期内完成来保证。

正在执行的指令的取消

当恢复事件(分支预测失败或异常)发生时,除了ROB、发射队列、LSQ中的指令外,还有一些指令正在执行单元的流水线中——它们已经被发射但尚未写回结果。这些"正在飞行"(in-flight)的指令如何处理?

(1)已发射但在ALU流水线中的指令。ALU通常是单周期执行,已发射的ALU指令在当前周期结束时会产生结果。如果该指令属于错误路径(通过branch mask检查),其结果在写回时被丢弃——不写入物理寄存器文件,也不唤醒等待该结果的后续指令。在硬件上,这通过在写回阶段增加一个有效性检查来实现:

write_enable=result_valid¬flush¬branch_mask_killed\text{write\_enable} = \text{result\_valid} \wedge \neg\text{flush} \wedge \neg\text{branch\_mask\_killed}

(2)已发射但在多周期执行单元中的指令。乘法器(3–5周期)、除法器(10–30周期)、浮点单元(4–6周期)等多周期执行单元可能有指令正在执行的中间阶段。处理方式有两种:

  • 延迟取消:让指令完成执行,但在写回时检查有效性并丢弃结果。这种方式简单,不需要中断执行单元的流水线,但浪费了执行带宽和功耗。

  • 立即取消:在检测到恢复事件后,立即清除执行单元流水线中的错误路径指令(通过冲刷执行流水线寄存器)。这释放了执行单元资源,使其可以更快地处理正确路径上的指令。但增加了执行单元的控制复杂度——每级流水线都需要flush逻辑。

大多数现代处理器对ALU和简单整数操作采用延迟取消(因为只有1周期延迟,浪费很少),对除法器和浮点除法采用立即取消(因为这些操作延迟很长,让它们空转会浪费宝贵的执行带宽)。

(3)已发射的load/store指令。已发射但尚未完成的load指令可能正在访问D-Cache或等待D-TLB响应。如果该load属于错误路径,处理器可以:

  • 让D-Cache/TLB访问正常完成,但丢弃返回的数据。

  • 如果D-Cache miss已经向L2发出请求,该请求通常不会被取消——取消一个已经在总线上的请求比让它完成更复杂。数据返回后被丢弃。

  • 错误路径的load可能将不需要的数据加载到缓存中(cache pollution),但考虑到正确路径可能也需要相同的数据,这种"无辜"的缓存加载不一定是有害的。

(4)已发射但被唤醒的依赖指令。当一条指令被发射执行后,它的结果通过旁路网络转发给等待该结果的后续指令。如果该指令属于错误路径,其结果被丢弃后,已经被它"唤醒"(wakeup)的后续指令会怎样?这些被唤醒的指令可能已经读取了"即将到达"的旁路数据,但该数据实际上不会到达(因为来源指令的写回被取消了)。

在检查点恢复方案中,这不是问题——所有错误路径的指令都会被分支掩码一次性清除,包括被错误唤醒的指令。但在某些实现中(特别是WALK恢复期间),已经被唤醒但尚未被清除的指令可能读到错误的旁路数据。解决方案包括:

  • 在恢复信号有效时,禁止旁路网络转发数据(将旁路MUX的选择信号强制设为"从寄存器文件读取")。

  • 在发射队列中增加"poison"位——当源指令被取消时,所有依赖于该源的指令被标记为poisoned,即使被唤醒也不会被发射。

性能分析 6 — 恢复期间的功耗浪费

流水线恢复期间,错误路径上正在执行的指令会浪费能量。在一个典型的高性能处理器中,分支预测失败恢复期间的功耗浪费可以量化如下:

假设分支预测失败率为0.5%/指令,平均错误路径长度为40条指令,每条指令的平均执行能耗为EinstE_{\text{inst}}。则每条指令因预测失败导致的平均能耗浪费为:

Ewaste=0.005×40×Einst=0.2×EinstE_{\text{waste}} = 0.005 \times 40 \times E_{\text{inst}} = 0.2 \times E_{\text{inst}}

这意味着约17%0.21+0.217%\frac{0.2}{1 + 0.2} \approx 17\%)的执行能耗被浪费在了最终被丢弃的指令上。在移动处理器中,这一比例对电池寿命有显著影响。这也是移动处理器倾向于使用更保守的推测策略(如更浅的流水线、更少的in-flight指令)的原因之一——减少推测深度可以显著降低能耗浪费。

某些先进的实现通过选择性时钟门控(selective clock gating)来缓解这一问题:在恢复信号产生后,立即关闭执行单元中错误路径指令的时钟,避免它们继续切换消耗动态功耗。但这需要快速的信号传播(恢复信号必须在1–2周期内到达所有执行单元的时钟门控逻辑),增加了设计复杂度。

恢复控制逻辑的整体架构

前面各节分别讨论了分支预测失败、异常、中断和存储器顺序违规的恢复机制。本节将这些机制整合在一起,讨论恢复控制逻辑的整体架构设计。一个设计良好的恢复控制架构需要处理多种事件的优先级仲裁、信号路由和时序约束。

恢复事件的统一处理框架

虽然不同类型的恢复事件触发条件不同,但它们的恢复操作有大量重叠。一个设计良好的恢复控制逻辑应该将这些重叠部分统一处理,以减少硬件复杂度。

恢复操作的共性。所有恢复事件都需要执行以下共同操作:

  1. 前端重定向:将PC设置为新的目标地址(正确分支目标/异常向量/中断向量)。

  2. 前端流水线清空:丢弃取指缓冲和解码流水线中的指令。

  3. 发射队列清除:清除部分或全部发射队列条目。

  4. LSQ清除:回退Load Queue和Store Queue的指针。

  5. RAT恢复:将sRAT恢复到某个一致状态。

恢复操作的差异。不同事件在恢复范围和RAT恢复方式上有关键区别:

操作分支预测失败异常中断存储器违规
ROB清除范围分支之后全部全部违规load之后
RAT恢复目标分支点sRATcRATcRAT违规点sRAT
RAT恢复方式检查点/WALKcRAT覆盖cRAT覆盖检查点/WALK
CSR写入4个CSR3个CSR
需要更新预测器
Store Buffer处理不影响已提交store不影响不影响不影响
重定向目标正确分支目标mtvecmtvec违规load的PC

恢复优先级与冲突解决

在同一个周期中,可能同时发生多个恢复事件。例如:

  • BRU报告分支预测失败,同时ROB头部的指令检测到异常。

  • 两条不同的分支同时被检测到预测失败。

  • 分支预测失败正在WALK恢复中,此时ROB头部检测到异常。

  • 存储器顺序违规被检测到,同时有待决中断。

这些冲突的解决遵循以下优先级规则(从高到低):

  1. 异常:最高优先级。异常发生在提交阶段,涉及架构状态的修改(CSR写入),必须立即处理。异常清除整个ROB,因此其他恢复事件自然被覆盖。

  2. 中断:次高优先级。中断也在提交阶段处理,清除整个ROB。

  3. 分支预测失败(程序顺序最早):当多条分支同时预测失败时,选择程序顺序上最早的。后续分支在早期分支的错误路径上,会被一起清除。

  4. 存储器顺序违规:通常在store地址计算完成时检测到,优先级低于分支预测失败(如果违规load在预测失败的分支的错误路径上,则不需要处理)。

优先级实现的关键判断。判断两个恢复事件的优先级的核心问题是确定"程序顺序上更早的事件"。对于异常和中断(总是在ROB头部处理),它们总是程序顺序上最早的未提交指令,因此自然拥有最高优先级。对于两个分支预测失败事件,通过比较ROB索引的年龄(如前文所述的模NN比较)来确定先后。

硬件描述 9 — 恢复控制器的硬件结构

恢复控制器(Recovery Controller)是处理器中协调所有恢复操作的中央模块。它的输入信号包括:

  • 来自BRU的mispredict信号、correct_target、rob_index、checkpoint_id。

  • 来自提交逻辑的exception_valid、exception_cause、exception_pc。

  • 来自提交逻辑的interrupt_pending信号。

  • 来自LSU的mem_violation信号和violation_rob_index。

  • 当前正在进行的WALK恢复状态(如果有)。

恢复控制器的输出信号包括:

  • flush_frontend:前端冲刷信号。

  • redirect_pc(64位):前端重定向目标PC。

  • flush_backend:后端冲刷信号(发射队列、LSQ)。

  • rob_flush_to(ROB索引宽度):ROB尾指针回退目标。

  • rat_restore_mode(2位):RAT恢复模式——00=无恢复,01=检查点恢复,10=WALK恢复,11=cRAT覆盖。

  • checkpoint_id(检查点编号):用于检查点恢复。

  • csr_write_enable(4位):CSR写入使能(mepc/mcause/mtval/mstatus各1位)。

  • walk_start:启动WALK恢复过程的信号。

恢复控制器的核心逻辑是一个优先级仲裁器:在每个周期中,评估所有待决的恢复事件,选择优先级最高的事件,生成相应的恢复信号。对于异常和中断,恢复控制器还负责驱动CSR写入逻辑。

恢复控制器的面积和时序。恢复控制器的面积很小(主要是组合逻辑和少量状态寄存器),但它位于多个模块的交汇点,其输出信号需要扇出到前端、后端、ROB、发射队列、LSQ等多个结构。在物理设计中,恢复控制器通常被放置在芯片的中心位置,以均衡到各模块的互联延迟。恢复信号的传播延迟直接影响恢复的速度——如果信号在1个周期内无法到达所有目标模块,可能需要在信号路径上插入流水线寄存器,增加1周期的恢复延迟。

恢复延迟的综合分析

将恢复过程的各个阶段延迟汇总,可以得到不同恢复事件的端到端延迟:

延迟组成分支预测失败异常中断
检查点WALK(平均)
恢复信号传播1111
RAT恢复1711
CSR写入001–31–3
前端重定向1111
I-Cache访问3–53–53–53–5
解码+重命名2–32–32–32–3
总延迟(周期)8–1114–179–149–14

注意:对于WALK恢复,如果使用部分阻塞式实现(前端取指与WALK并行),则总延迟可以缩短到max(Twalk,Tredirect)\max(T_{\text{walk}}, T_{\text{redirect}})而非两者之和。在上表中已考虑了这一优化。

恢复延迟对IPC的综合影响。考虑一个运行SPECint2017基准测试的6-wide处理器,各恢复事件的频率和延迟如下:

  • 分支预测失败:每200条指令一次,每次10周期惩罚\Rightarrow每条指令平均损失10/200=0.0510/200 = 0.05周期。

  • 异常:每100000条指令一次,每次40周期惩罚\Rightarrow每条指令平均损失40/100000=0.000440/100000 = 0.0004周期。

  • 中断:每1000000条指令一次,每次40周期惩罚\Rightarrow每条指令平均损失0.000040.00004周期。

  • 存储器顺序违规:每50000条指令一次,每次30周期惩罚\Rightarrow每条指令平均损失0.00060.0006周期。

可以清楚地看到,分支预测失败的性能影响比其他恢复事件高出2–3个数量级。这解释了为什么分支预测失败的恢复机制(检查点vs WALK)的选择如此重要,而异常和中断的恢复效率相对不那么关键。

恢复事件的性能监控

现代处理器提供硬件性能计数器(Hardware Performance Counters, HPCs)来监控各类恢复事件的发生频率和性能影响。这些计数器对于性能调优至关重要。与恢复机制相关的典型性能计数器包括:

  • BR_MISP_RETIRED:已提交的分支指令中预测失败的次数。注意只有已提交的预测失败才被计数——错误路径上的分支预测失败不计入。

  • BR_MISP_EXEC:所有被执行的分支中预测失败的次数(包括推测执行的)。

  • MACHINE_CLEARS:流水线被完全清除的次数(包括异常、中断、存储器顺序违规等引起的清除)。

  • MEM_ORDER_VIOLATION:存储器顺序违规导致的流水线清除次数。

  • EXCEPTION_TAKEN:异常处理的次数。

  • RECOVERY_CYCLES:因恢复事件导致流水线无法分配新指令的总周期数。

  • CHECKPOINT_STALL:因检查点缓冲区满而暂停分配的周期数。

  • BRANCH_MASK_STALL:因分支掩码位耗尽而暂停分配的周期数。

RISC-V的HPM(Hardware Performance Monitor)。RISC-V通过mhpmcounter3mhpmcounter31提供最多29个可配置的性能计数器,每个计数器通过对应的mhpmevent寄存器配置计数的事件类型。具体的事件编码由实现定义。在一个完整的高性能RISC-V处理器实现中,上述恢复相关事件应该被分配到可用的HPM计数器中。

使用性能计数器进行恢复效率评估。性能工程师可以通过以下公式评估恢复机制的效率:

恢复开销比=RECOVERY_CYCLESCPU_CYCLES×100%\text{恢复开销比} = \frac{\text{RECOVERY\_CYCLES}}{\text{CPU\_CYCLES}} \times 100\%

在典型的SPECint工作负载中,分支预测失败导致的恢复开销约为5%–15%的总CPU周期,而异常和中断的贡献通常不到0.1%。如果恢复开销异常高,通常表示:

  • 分支预测器精度不足(需要改进预测算法或增大预测器表大小)。

  • 代码中存在大量难以预测的间接跳转(如虚函数调用密集的C++代码)。

  • 存储器顺序违规频繁发生(需要改进store-set预测器或调整代码布局)。

  • 系统调用频率过高(需要优化用户态/核心态切换路径)。

恢复机制的设计指南

基于本章的分析,可以总结出以下恢复机制的设计指南:

指南1:分支预测失败恢复使用检查点。对于主流高性能处理器(IPC目标>3> 3,ROB深度>256> 256),分支预测失败的恢复必须使用检查点方案。WALK恢复在ROB较深时可能产生30–50周期的恢复延迟,对IPC的影响不可接受。检查点的面积开销(\sim1 KB)在现代工艺下微不足道。

指南2:异常/中断恢复使用cRAT覆盖。异常和中断的发生频率低2–3个数量级,不需要为它们维护专门的快速恢复路径。使用cRAT覆盖sRAT的方案面积最小、实现最简单,且延迟可以接受。

指南3:检查点数量选择8–16个。根据工作负载分析,8个检查点可以覆盖95%以上的场景而不产生暂停;16个几乎消除了所有检查点相关的暂停。超过16个的收益递减。

指南4:分支掩码宽度选择16位。16位的分支掩码在SPECint中几乎不产生因掩码位耗尽而暂停的情况,且面积开销可控。如果面积极度受限,8位也是可接受的选择。

指南5:WALK恢复保留作为备用路径。即使主要使用检查点恢复,保留WALK恢复能力作为存储器顺序违规的恢复路径是有价值的。存储器顺序违规的恢复不需要检查点(load指令通常不关联检查点),WALK恢复的可变延迟在此场景下可以接受。

指南6:恢复控制逻辑放置在芯片中心。恢复控制逻辑的输出信号需要扇出到前端、后端多个模块,将其放置在芯片布局的中心位置可以均衡互联延迟,避免恢复信号成为关键路径。

指南7:保留WALK恢复作为存储器顺序违规的处理路径。即使主要恢复使用检查点,对于存储器顺序违规(通常不关联检查点),WALK恢复提供了一种不需要额外存储的恢复方案。由于违规发生频率低,WALK的可变延迟可以接受。

指南8:监控恢复事件的性能计数器。通过HPM计数器监控各类恢复事件的频率和影响,为微架构调优提供数据支持。重点关注分支预测失败率、检查点耗尽暂停和存储器顺序违规。

指南9:恢复信号的时序约束。恢复信号(特别是branch mask kill信号)需要在1个周期内到达发射队列的所有条目。对于大型发射队列(96+条目),branch mask的比较逻辑可能成为时序关键路径。可以通过将发射队列分成多个bank并行处理来缓解。

指南10:错误路径的功耗管理。在恢复信号产生后,应尽快对错误路径上正在执行的指令实施选择性时钟门控,减少无用的功耗浪费。这在移动和嵌入式处理器中尤为重要,可以将推测执行的能耗浪费从约17%降低到5%以下。


本章讨论了乱序执行处理器中三类流水线恢复事件的处理机制,表表 39.11对它们进行了全面对比。

特性分支预测失败同步异常异步中断
触发原因BRU检测到预测错误指令执行异常外部设备事件
发生频率高(\sim1/200指令)低(\sim1/10410^4指令)低(\sim1/10510^5指令)
处理时机分支执行时提交时提交时
清除范围分支之后的指令所有ROB内指令所有ROB内指令
RAT恢复方式检查点/WALK提交RAT覆盖提交RAT覆盖
恢复延迟1周期(检查点)5–10周期5–10周期
前端重定向目标正确分支目标异常向量中断向量

三类流水线恢复事件的对比总结

本章的核心技术内容可以从以下几个维度总结:

分支预测失败的恢复。分支预测失败是最高频的恢复事件(约每200条指令一次),其处理包括前端重定向和后端状态恢复两个维度。前端重定向涉及PC更新、取指缓冲清空、分支预测器更新(TAGE/BTB/RAS/GHR恢复)等操作。后端恢复的核心是RAT恢复,本章详细分析了三种方案:

  • 检查点恢复:每条分支分配一个RAT快照(约512–622位),恢复延迟恒定1周期,面积开销约1.2 KB(16个检查点),适合高频恢复事件。以MIPS R10000的4个检查点为代表。

  • WALK恢复:利用ROB中记录的旧映射信息逆序撤销,恢复延迟N/W\lceil N/W \rceil周期,无额外面积开销,适合面积敏感设计。以Intel P6的3-wide WALK为代表。

  • 架构状态恢复:等待流水线排空后用cRAT覆盖sRAT,延迟最大(Tdrain+1T_{\text{drain}} + 1),面积最小,适合低频事件。以Alpha 21264的异常恢复为代表。

分支掩码机制使得发射队列和LSQ中错误路径指令的清除可以在1个周期内完成(通过MM位掩码向量的并行检测),与RAT恢复机制的选择无关。分支掩码的位宽MM(典型值8–16)决定了最大同时未解析分支数量。

异常的处理。同步异常通过"延迟报告、提交时处理"的策略实现了精确异常——异常信息(exception_valid + exception_cause + exception_tval)被记录在ROB表项中,等到异常指令到达ROB头部时才触发8步异常处理流程(CSR写入、ROB清空、RAT恢复、前端重定向等)。ROB的顺序提交机制天然保证了精确异常的三个条件:异常前的指令全部提交、异常后的指令全部撤销、架构状态一致。推测执行中的异常通过正常的恢复机制自然消解,不需要额外的撤销逻辑。

中断的处理。异步中断通过在提交阶段采样的方式实现了精确化——中断点自然选择为ROB头部的指令,之前的全部提交,之后的全部清除。中断控制器(APIC/GIC/PLIC/AIA)负责中断的汇聚、仲裁和分发。随着RISC-V AIA标准的推进,中断控制器与CPU核之间的接口正在从传统的MMIO方式(几十周期延迟)向基于CSR的低延迟方式(1–2周期延迟)演进。

其他恢复事件。存储器顺序违规、序列化指令排空、Store Buffer管理等机制确保了内存一致性和架构状态的正确性。推测执行的安全侧信道(Spectre/Meltdown)问题提醒我们,恢复机制在架构层面是"完美"的,但在微架构层面并不完美——推测执行对缓存、TLB等微架构状态的副作用仍然存在。

恢复机制的整体设计哲学。本章反复出现的一个主题是"频率决定策略"——高频事件(分支预测失败)使用最快的恢复方式(检查点,1周期),中频事件(存储器顺序违规)使用中等速度的方式(WALK),低频事件(异常、中断)使用最简单的方式(cRAT覆盖)。这种分层策略在面积和性能之间取得了最优的平衡。

这些恢复机制是乱序执行引擎能够大胆推测的安全网。没有高效的恢复机制,处理器就无法进行激进的分支预测和推测执行——正是因为有了"出错时能快速修复"的能力,处理器才能在"赌对时获得高性能"的赛道上全力奔跑。恢复控制逻辑作为处理器中的"消防系统",虽然大部分时间不需要启动,但它的存在使得处理器可以放心地进行各种推测——这就是"推测与恢复"范式的核心:处理器以性能为目标大胆推测,以正确性为底线精确恢复。ROB作为这一范式的基石,同时扮演了结果缓冲、顺序提交和恢复锚点的三重角色。

回顾本章讨论的所有恢复机制,可以看到它们共同构成了乱序执行引擎的"安全网"。这个安全网的设计遵循以下层次结构:

  1. 最内层——分支掩码:1周期内清除发射队列和LSQ中的错误路径指令。所有恢复事件都使用此机制。

  2. 第二层——检查点/WALK:恢复RAT到分支点状态。主要用于分支预测失败和存储器顺序违规。

  3. 第三层——cRAT覆盖:恢复RAT到架构状态。用于异常和中断处理。

  4. 最外层——完整流水线冲刷:清除ROB、发射队列、LSQ的所有内容,重置前端PC。用于异常、中断和不可恢复错误。

这种层次化设计确保了:常见事件(分支预测失败)使用最轻量级的恢复路径,罕见事件(异常/中断)使用最彻底的恢复路径。每一层都建立在更内层机制的基础之上——例如,异常恢复使用了分支掩码清除(清除发射队列)和cRAT覆盖(恢复RAT),但不使用检查点(因为不需要恢复到分支点)。

性能分析 7 — 恢复延迟对IPC的影响量化

本算例量化不同恢复延迟对处理器IPC的影响。

已知条件:

  • 处理器配置:6-wide分配,IPCideal=4.0\text{IPC}_{\text{ideal}} = 4.0(无mispredict时)

  • 分支指令比例:fb=15%f_b = 15\%(每100条指令中15条为分支)

  • 分支预测错误率:rm=0.5%r_m = 0.5\%(每条分支的mispredict概率为0.5%,即200条分支错一次)

  • 恢复延迟:LrL_r(从检测到mispredict到正确路径的第一条指令被取指的周期数)

第一步:计算两次mispredict之间的平均指令数。

平均每1/(fb×rm)=1/(0.15×0.005)=13331 / (f_b \times r_m) = 1 / (0.15 \times 0.005) = 1333条指令发生一次mispredict。

第二步:计算两次mispredict之间的平均周期数。

在无mispredict时,执行1333条指令需要1333/IPCideal=1333/4.0=333.31333 / \text{IPC}_{\text{ideal}} = 1333 / 4.0 = 333.3个周期。

第三步:加入恢复延迟的总周期数。

每次mispredict增加LrL_r个恢复周期。总周期数为333.3+Lr333.3 + L_r

第四步:计算有效IPC。

IPCeff=1333333.3+Lr\text{IPC}_{\text{eff}} = \frac{1333}{333.3 + L_r}

第五步:对比不同恢复延迟。

恢复延迟 LrL_r总周期IPCeff\text{IPC}_{\text{eff}}IPC下降
3336.33.961.0%
10343.33.883.0%
15348.33.834.3%
20353.33.775.8%
30363.33.678.3%

结论:在0.5%的mispredict率下,恢复延迟从3周期增加到20周期,IPC下降从1.0%恶化到5.8%。看似差异不大,但如果mispredict率提升到2%(整数控制流密集代码),差距将放大4倍:Lr=20L_r = 20时IPC下降超过20%。这解释了为什么现代高性能处理器不惜投入\sim1.2 KB的面积用于16个RAT检查点(如第 24.0 章所述),以获得Lr=1L_r = 1周期的极速恢复——在高mispredict率场景下,这个面积投资的回报是巨大的。

至此,第五篇"乱序执行引擎"的所有核心机制已全部讨论完毕。在第六篇中,我们将通过真实处理器的案例研究来验证和深化这些理论。第 40.0 章首先登场的是Alpha 21264——一颗1998年的处理器,却已经完整实现了本篇讨论的几乎所有技术:Cluster架构(第 35.0 章)、Store Wait Table投机消歧(第 36.0 章的前身)、检查点恢复(本章的核心主题)。21264用1520万晶体管和约50人的设计团队,创造了当时最高的SPECint分数,是"投机与并行"设计哲学的里程碑式作品。

至此,第五篇"乱序执行引擎"的所有核心机制——寄存器重命名、发射与调度、执行单元、存储器子系统、ROB与提交、以及异常恢复——已经全部讨论完毕。下一篇将转向缓存层次和内存系统,探讨处理器如何高效访问数据。

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