存储器指令的加速
在一个典型的程序中,每45条指令就有一条Load或Store指令。当一条Store的地址尚未计算出来时,后面的Load能否提前执行?如果Load和Store的地址恰好相同,提前执行的Load会读到过时的值——这是乱序执行中最微妙的“投机”。
从本书的统一视角来看,存储器消歧是对内存依赖关系的“投机”——处理器假设一条Load与前面地址未知的Store不冲突,从而让Load提前执行以降低有效访存延迟。这种投机与第 14.0 章第 15.0 章中讨论的分支预测在哲学上完全一致:分支预测是对控制流依赖的投机,存储器消歧是对数据流依赖的投机。两者都需要在投机失败时进行恢复(分别对应第 38.0 章中的ROB违规检测和第 39.0 章中的恢复机制),且都从预测器中获益巨大。Store Set预测器与分支预测器的设计方法论惊人地相似:通过记录历史冲突信息来预测未来的内存依赖关系,正如分支预测器通过记录历史跳转行为来预测未来的控制流。第 27.0 章中讨论的IQ依赖等待机制为Store Set预测器提供了硬件基础——Load在发射队列中可以等待特定Store的地址计算完成。
读完本章,你将理解存储器消歧的三种策略(保守、乐观推测、预测引导)及其性能权衡,掌握Store-to-Load转发的微架构实现,并深入理解Store Set预测器这一处理器设计中最精妙的预测机制之一。
在超标量处理器的乱序执行引擎中,存储器指令(Memory Instructions)——即Load和Store指令——的处理是整个微架构中最复杂的部分之一。其复杂性根植于一个本质矛盾:乱序执行要求尽可能多的指令并行执行以提高IPC,而存储器指令之间的地址相关性只有在地址计算完成之后才能确定。一条Load指令是否可以越过前面的Store指令提前执行?答案取决于两者的访存地址是否重叠——而这个地址可能依赖于一连串尚未执行的计算指令。
与寄存器相关性不同,存储器相关性无法通过寄存器重命名来消除。寄存器的数量是有限的、可枚举的,硬件可以在译码阶段就精确地判断两条指令是否存在RAW/WAW/WAR相关性。但存储器地址空间是巨大的(64位地址空间包含个字节地址),而且地址通常由基址寄存器加偏移量计算得出,在指令执行之前无法得知精确地址。这使得存储器指令的消歧(Memory Disambiguation)成为一个需要预测和推测的问题。
本章围绕存储器指令的加速展开,详细讨论四个核心问题:如何判断Load和Store之间是否存在地址冲突(Memory Disambiguation),如何通过Store Buffer实现Store-to-Load数据转发,Load/Store队列的微架构组织,以及Load违规检测与恢复机制。这些机制共同决定了处理器的存储器指令吞吐量——在典型工作负载中,约30%40%的指令是存储器指令,其执行效率直接影响整体IPC。
在讨论具体的加速技术之前,有必要明确存储器指令的几个基本特征。首先,存储器指令具有两阶段执行特征——Store指令不仅需要计算地址(第一阶段),还需要将数据写入存储器层次结构(第二阶段),且只有在指令提交后才能执行第二阶段。其次,存储器指令的延迟具有高度不确定性——L1 Cache命中可能只需4个周期,而Cache全部未命中需要200+个周期,这种不确定性给调度和资源管理带来了巨大挑战。第三,存储器指令与所有其他存储器指令之间存在潜在的隐式依赖——两条指令即使使用完全不同的寄存器,只要它们的计算地址恰好相同,就存在数据相关性。这种隐式依赖是存储器指令区别于寄存器指令的根本特征。
为了具象化这些挑战的严重程度,考虑一个实际的数字:在一个6发射、256项ROB的处理器中,乱序窗口中平均同时有90条飞行中指令,其中32条是存储器指令(19条Load + 13条Store)。这32条存储器指令之间存在对潜在的Load-Store相关性——每对都需要硬件在某个时刻判断是否存在地址冲突。这247对判断中的大多数(95%)结果是"不冲突",但硬件必须能够精确地识别出那少数冲突的对。这就是存储器子系统硬件复杂度的根本来源。
在整个处理器的性能模型中,存储器指令子系统的效率可以用有效Load延迟来衡量。理想情况下,每条Load的延迟等于L1 Cache命中延迟(4周期)。但实际上,由于消歧等待、STLF延迟、Cache未命中和违规恢复等因素,有效Load延迟通常为58周期。将有效Load延迟从8周期降低到5周期,可能带来10%20%的整体IPC提升——这就是存储器指令加速技术的巨大价值。
表表 36.1概览了存储器指令在乱序引擎各阶段的处理流程及相关硬件结构。
| 阶段 | 操作 | 硬件结构 |
|---|---|---|
| 分配(Allocate) | 为Load/Store分配队列项 | Load Queue / Store Queue |
| 地址计算(AGU) | 计算有效地址 | 地址生成单元 |
| 消歧(Disambiguate) | 检查地址冲突 | CAM比较逻辑 |
| 执行(Execute) | Load访问Cache / Store写入SB | Cache + Store Buffer |
| 转发(Forward) | Store数据旁路给后续Load | Store Buffer转发逻辑 |
| 提交(Commit) | Store写入Cache | Store Buffer Cache |
存储器指令在乱序引擎中的处理流程
Memory Disambiguation
存储器消歧(Memory Disambiguation)是指硬件判断一条Load指令与程序序中在它之前的Store指令是否存在地址冲突的过程。如果Load的地址与某个先序Store的地址重叠,那么Load必须从该Store获取数据(而不是从Cache中读取过期的值);如果不重叠,Load可以安全地从Cache读取数据。
消歧问题之所以困难,是因为乱序执行可能导致Load在先序Store之前执行。考虑以下代码序列:
sd a0, 0(s0) # Store: M[s0] = a0
ld a1, 0(s1) # Load: a1 = M[s1]如果s0和s1恰好相等,则Load应该得到Store写入的值,而不是Cache中的旧值。但在乱序执行中,如果s1的值已经就绪而s0的值尚未就绪,处理器可能先执行Load——这时Load从Cache读到的是旧值,产生了错误的结果。
存储器地址相关性的本质
存储器地址相关性分为三类,类比寄存器相关性:
RAW(Read After Write):Load读取的地址与先序Store写入的地址重叠。这是真相关性,Load必须获取Store的数据。这是消歧问题的核心。
WAR(Write After Read):Store写入的地址与先序Load读取的地址重叠。在乱序执行中,如果Store先于Load执行并将数据写入Cache,那么后续的Load就会读到错误的值。但由于Store通常在提交(commit)时才写入Cache,WAR相关性在现代处理器中不是问题——Store Buffer天然地解决了WAR。
WAW(Write After Write):两个Store写入相同的地址。同样,由于Store按程序序提交,WAW也通过Store Buffer的提交顺序得到保证。
因此,存储器消歧的核心问题是检测Load与先序Store之间的RAW相关性。具体而言,硬件需要判断:对于当前要执行的Load,在它之前(按程序序)是否存在地址重叠的Store尚未写入Cache?
这个问题的本质困难在于信息不完整。当一条Load准备发射执行时,它的先序Store可能处于以下几种状态:(1) 地址和数据都已就绪,已完成执行;(2) 地址已就绪但数据未就绪;(3) 地址未就绪(基址寄存器尚在计算中)。对于状态(1)和(2),硬件可以进行精确的地址比较来判断是否冲突;但对于状态(3),硬件完全不知道这条Store将会写到什么地址,因此无法判断是否与当前Load冲突。这就是消歧问题的核心挑战——在信息不完整的情况下做决策。
地址重叠的判断条件
地址重叠的判断不仅仅是"地址是否相等"。考虑一个Store写入4字节(sw)和一个Load读取8字节(ld)的情况:只要两者的地址范围有任何字节的交集,就存在相关性。具体地,设Store的地址为、大小为,Load的地址为、大小为,则重叠条件为:
在硬件中实现完整的重叠比较代价较高——需要两个加法器(计算和)和两个比较器,总共约200300门。如果Store Buffer有64项,则需要64组这样的逻辑并行工作,总门数达到约15K20K门。因此许多处理器简化为低位地址匹配——只比较地址的低12位或低20位,如果低位匹配就认为可能冲突。这种简化会导致假阳性(false positive),即实际不冲突的Load被误判为冲突而延迟执行,但不会导致功能错误(只影响性能)。
进一步的简化是只比较地址的字对齐部分,忽略最低3位(对于8字节对齐的访问)。这意味着比较的是8字节块级别的地址,而不是字节级别。如果两条指令的8字节块地址相同,就认为可能冲突。这种简化增加了假阳性的概率(两条指令可能访问同一8字节块的不同字节而实际不冲突),但大幅减少了比较器的位宽——从48位减少到45位。
地址比较器的硬件实现
地址比较器是消歧逻辑中最基本的构件。每个Store Buffer/Store Queue条目中的比较器将其存储的地址与广播的Load地址进行逐位比较。比较器的详细实现包含以下部分:
位级比较:对于每一位(,为比较位宽),使用一个异或非(XNOR)门比较Store地址的第位与Load地址的第位:
表示第位匹配。XNOR门需要4个晶体管(2个NMOS + 2个PMOS)。
匹配线归约:将所有个信号通过一个输入的AND门合并为最终的匹配信号:
输入AND门可以使用AND树实现(级2输入AND门),延迟为级门延迟。对于,需要级AND门。
在CAM结构中,AND归约通常使用动态逻辑(Dynamic Logic)实现而非静态逻辑。动态逻辑的匹配线(Match Line)初始被预充电到高电平(),然后每个不匹配的位通过一个下拉NMOS将匹配线拉到低电平。如果所有位都匹配,匹配线保持高电平。这种方式的优势是所有位的比较在一个门延迟内完成(单级评估),而不是级——显著减少了延迟。代价是需要预充电周期(每个时钟周期的前半段预充电,后半段评估),且功耗较高(每个时钟周期都需要预充电,即使匹配线最终被下拉)。
对于一个64项Store Queue、20位比较宽度的CAM:
比较器晶体管数:个(XNOR门)
下拉晶体管数:个(匹配线下拉NMOS)
预充电晶体管数:个(每条匹配线一个PMOS)
总晶体管数:约个,面积约@7nm
评估延迟: ps(单级动态评估)
预充电时间: ps
虚拟地址vs物理地址的比较
地址比较还涉及虚拟地址vs物理地址的选择。Store Buffer在存储数据时,通常同时记录虚拟地址和物理地址。STLF的地址比较可以使用虚拟地址(速度快,不需要等待TLB翻译)或物理地址(精确,不会因地址别名导致假阳性/假阴性):
虚拟地址比较:优点是速度快——Load的虚拟地址在AGU完成后立即可用,不需要等待TLB翻译。缺点是可能存在同义问题(synonyms)——两个不同的虚拟地址映射到同一个物理地址,导致假阴性(miss a real conflict)。假阴性是功能错误,必须避免。
物理地址比较:精确无误,但需要等待TLB翻译完成后才能进行比较,增加了STLF和违规检测的延迟。TLB查找通常需要12个周期,这会使STLF的总延迟增加到57个周期。
混合方案:先用虚拟地址的低位(页面偏移量,低12位,虚拟地址和物理地址相同)进行快速初步匹配,如果匹配则等待TLB完成后用物理地址确认。这种方案在速度和精确性之间取得平衡,是大多数现代处理器的选择。
混合方案的关键洞察是:虚拟地址的低12位(页内偏移)与物理地址的低12位完全相同(因为页面大小为4KB = 字节)。因此,通过比较低12位可以在不需要TLB翻译的情况下排除大多数不冲突的情况。只有当低12位匹配时,才需要等待TLB完成后用完整的物理地址进行确认。在实际程序中,低12位匹配但高位不匹配的概率约为,因此混合方案很少需要等待TLB确认。
设计提示
地址比较的位宽选择是一个面积-性能权衡。比较全部的虚拟地址(通常为4857位)可以最大程度减少假阳性,但CAM比较器的面积和功耗与位宽成正比。许多设计选择只比较低1220位地址,因为高位地址冲突的概率极低,且页面偏移量(低12位)通常足以区分不同的变量。Alpha 21264比较低12位地址(一个页面内的偏移),Pentium 4和Core系列比较全部物理地址。对于一个64项的Store Buffer,比较12位地址需要个晶体管(每位比较约4个晶体管:2个异或门晶体管 + 2个或归约晶体管),而比较48位地址需要约12288个晶体管。
保守式消歧
最简单的消歧策略是保守式消歧(Conservative Disambiguation):一条Load指令必须等到所有程序序中在它之前的Store指令的地址都已计算完成之后,才能执行。这确保了在Load执行之前,硬件已经知道所有先序Store的地址,可以精确判断是否存在冲突。
保守式消歧的实现相对简单:
每条Store在地址计算完成后,设置一个"地址就绪"标志位。
每条Load在发射前检查:所有比自己更老的Store是否都已设置"地址就绪"标志。
如果有任何先序Store的地址尚未就绪,Load必须等待。
当所有先序Store的地址都就绪后,Load执行地址比较,确认无冲突后发射执行。
保守式消歧的实现逻辑
在硬件中,步骤2的"检查所有先序Store的地址是否就绪"可以通过一个简单的逻辑实现:维护一个与Store Queue等长的位向量addr_valid[0..M-1],当Load准备发射时,提取从Store Queue头部到该Load分发时Store Queue尾指针之间的所有位,检查它们是否全部为1。这可以通过一个与归约(AND reduction)电路实现——将选定范围内的所有addr_valid位进行与操作,结果为1表示所有先序Store地址已就绪。
与归约电路的延迟为——对于一个64项的Store Queue,需要6级与门,约1ns@3GHz。这个延迟位于Load的发射判断关键路径上,可能限制处理器的时钟频率。优化方案包括:(1) 将与归约分为多级流水线;(2) 使用前缀与(prefix AND)结构预计算部分结果;(3) 只检查最近的条先序Store(),忽略更老的Store(假设它们的地址已经就绪)。
保守式消歧的性能代价
性能分析 1 — 保守式消歧的性能损失量化分析
在典型的乱序处理器中,约35%的指令是存储器指令,其中Load约占60%、Store约占40%。SPEC CPU基准测试中的统计数据表明,每4条指令中约有1条是Load指令。如果每条Load都必须等待所有先序Store的地址计算完成,性能损失有多大?
考虑一个典型场景:6发射处理器,ROB大小256项,指令窗口中平均有90条飞行中指令,其中14条是Store。这14条Store中,假设平均有3条的地址尚未就绪(因为基址寄存器依赖于前面的ALU指令链)。那么每条Load平均需要等待最后一条地址未就绪的Store完成计算,等待延迟约为24个周期。
具体量化分析:
每周期约分发条Load。
每条Load额外等待3个周期(等待最后一条先序Store的地址计算)。
这些等待的Load占据发射队列项,阻塞后续指令的分发。
有效Load延迟从4周期(L1 Cache命中)增加到7周期,增长75%。
考虑到Load延迟位于许多依赖链的关键路径上,IPC下降约15%25%。
在指针追踪密集的程序(如
mcf、链表遍历)中,损失可高达35%40%。
然而,实际上大多数Load与先序Store之间并不存在地址冲突——在SPEC CPU基准测试中,Load真正需要从Store获取数据(Store-to-Load转发)的比例仅为5%15%。保守式消歧为了保证这5%15%的正确性,让85%95%的无关Load白白等待。这是典型的"为少数情况惩罚多数情况"的设计,在高性能处理器中是不可接受的。
保守式消歧在有序(in-order)处理器中是合理的,因为有序处理器的指令按程序序执行,Store的地址在Load之前自然就绪。但在高性能乱序处理器中,这种策略的性能损失是不可接受的。
值得注意的是,"完全保守"与"完全推测"之间存在一系列中间策略。例如,部分保守式消歧只要求Load等待那些可能与之冲突的Store——即地址范围有可能重叠的Store。具体地,如果Store的地址偏移量(立即数部分)在编译时已知,处理器可以在译码阶段就初步判断两条指令的地址是否可能冲突。例如:
sd a0, 0(s0) # Store: M[s0 + 0]
ld a1, 128(s0) # Load: M[s0 + 128]即使s0的值未知,由于偏移量差为128字节(远大于单个Store的宽度8字节),硬件可以安全地推断这两条指令的地址不会重叠,Load可以提前执行。但这种基于偏移量的静态判断只适用于使用相同基址寄存器的Load-Store对,对于使用不同基址寄存器的情况无能为力。
等待计数器机制
部分保守式消歧的一种硬件实现是等待计数器(Wait Counter)机制。其原理是:在发射队列中为每条Load维护一个计数器,初始值等于该Load分发时Store Queue中地址未就绪的先序Store数量。每当一条先序Store的地址计算完成,该计数器递减1。当时,所有先序Store的地址都已就绪,Load可以安全地执行精确消歧。
等待计数器的硬件实现需要以下逻辑:
初始化:在Load分发时,扫描Store Queue中从SQ Head到该Load的SQ Tail Snapshot之间的所有条目,计数Address Valid = 0的条目数。这个扫描可以通过一个Population Count(popcount)电路实现——对64位的位向量求"0"的个数。对于64位位向量,popcount需要约个全加器级,面积约200门。
递减:当任何先序Store的地址计算完成时,广播一个"Store地址就绪"信号。发射队列中所有等待该Store的Load的计数器递减1。这需要在发射队列中加入与Store Queue等宽的匹配逻辑——判断哪些Load的先序Store范围包含刚完成地址计算的Store。
唤醒:当计数器递减到0时,Load被标记为"所有先序Store地址已就绪",可以进入发射仲裁。
等待计数器机制的优点是精确——Load在恰当的时刻被唤醒,不早也不晚。缺点是硬件复杂度较高——每条Load需要一个位的计数器(为Store Queue大小),以及复杂的递减逻辑。在实际设计中,大多数处理器选择了更简单的推测式消歧,避免了等待计数器的复杂性。
保守式与推测式消歧的性能对比
下面通过一个具体的代码例子对比保守式消歧和推测式消歧的性能差异:
# 循环体内的代码片段
sd a0, 0(s0) # Store1: M[s0] = a0
ld a1, 0(s1) # Load1: a1 = M[s1],与Store1可能冲突
add a2, a1, a3 # 依赖于Load1的结果
ld a4, 0(s2) # Load2: a4 = M[s2],与Store1可能冲突
add a5, a4, a6 # 依赖于Load2的结果假设Store1的基址s0来自前面的长延迟计算,在第周期才就绪。Load1和Load2的基址s1和s2在第周期就已就绪。
保守式消歧:Load1和Load2必须等到Store1的地址在第周期就绪后才能执行。Load1在周期执行(1周期AGU + 4周期Cache = 完成于),add a2在周期执行。Load2在周期执行(如果只有一个Load端口),完成于,add a5在周期执行。总执行跨度:周期。
推测式消歧:Load1在周期即可推测执行(基址在就绪,1周期AGU),完成于。add a2在周期执行。Load2在周期执行,完成于。add a5在周期执行。如果推测正确(s0 s1且s0 s2),总执行跨度:周期。节省3个周期。
如果推测错误(例如s0 == s1),则在周期检测到违规,冲刷并恢复。总代价约周期(含25周期恢复代价)。但由于违规概率极低(1%),期望总代价为周期——远优于保守式消歧的10周期。
推测式消歧
现代高性能处理器几乎无一例外地采用推测式消歧(Speculative Disambiguation):当一条Load的先序Store地址尚未完全就绪时,处理器推测该Load与这些Store不冲突,直接让Load提前执行。如果推测正确(绝大多数情况),Load节省了等待时间,显著提高了性能;如果推测错误(Load实际上与某个先序Store存在地址冲突),处理器需要检测出这种违规(violation)并进行恢复——通常是冲刷流水线并重新执行Load及其后续指令。
推测式消歧的基本流程如图图 36.1所示:
推测执行的详细步骤
推测式消歧的执行过程可以分解为以下精确步骤:
Load操作数就绪:Load指令的基址寄存器值可用,AGU计算出有效地址。
已知Store地址的检查:将与Store Queue中所有已计算出地址的先序Store进行比较。如果存在地址匹配,执行Store-to-Load转发(详见36.2.4 节)。
未知Store地址的推测:对于Store Queue中地址尚未计算的先序Store,推测它们与Load不冲突。Load直接访问Cache读取数据。
Load执行完成:Load的结果写入目的物理寄存器,后续依赖指令可以开始执行。同时,Load的地址和执行状态被记录在Load Queue中。
验证阶段:当某条先序Store的地址最终计算完成时(可能在Load执行后很多周期),Store的地址被广播到Load Queue。硬件检查是否有已执行的、比该Store更年轻的Load与之地址冲突。
违规检测:如果发现冲突(步骤5中地址匹配),说明Load在步骤3中的推测是错误的——Load从Cache读到的是过期值,应该从该Store获取数据。此时触发违规恢复。
违规恢复:冲刷从违规Load开始的所有后续指令,重新从Load的PC开始取指和执行。此次重新执行时,Store的地址已经就绪,Load将正确地从Store获取数据。
推测失败的代价分析
推测失败(违规发生)的代价包含以下几个组成部分:
冲刷代价:从违规Load到ROB尾部的所有指令的执行结果被丢弃。在一个256项ROB的处理器中,如果违规Load位于ROB中部(平均位置),约128条指令的工作被浪费。
前端重启延迟:将取指PC重定向到违规Load的地址,前端流水线需要57个周期才能重新开始产生有效指令。
流水线排空延迟:从冲刷开始到新指令填满执行流水线,需要约1520个周期。
资源浪费:违规期间占用的功能单元、Cache端口和带宽全部浪费。
预测器训练:违规信息被用于更新内存依赖预测器,下次同一Load将被预测为"需要等待",避免再次违规。
总计,每次违规的恢复代价约为2040个时钟周期——这与分支预测失败的代价量级相当,因为两者使用类似的恢复机制。
违规窗口分析
违规窗口(Violation Window)是指Load推测执行后到违规被检测到之间的时间间隔。在这个窗口内,处理器基于错误的Load结果执行了大量无用指令。违规窗口越长,浪费的指令越多,恢复代价越高。
违规窗口的大小取决于先序Store的地址计算延迟。具体地,如果Load在周期推测执行,而先序Store的地址在周期计算完成(),则违规窗口为个周期。在这个周期内,一个IPC为4的处理器平均执行了条指令——这些指令在违规恢复时全部被丢弃。
违规窗口的统计分布:在SPEC CPU中,约70%的违规的窗口10周期(Store的地址很快就绪),约20%的违规的窗口在1030周期(Store依赖于中等延迟的计算链),约10%的违规窗口30周期(Store依赖于长延迟操作如Cache未命中)。
违规窗口的大小也影响了是否值得使用Store Set预测器。如果违规窗口很短(5周期),那么即使不使用预测器(盲推测),违规的代价也很低——只浪费了20条指令。但如果违规窗口很长(30周期),一次违规浪费120条指令,预测器的价值就非常大。
推测式消歧的概率分析
从概率的角度分析推测式消歧的期望收益。设为一条Load与其先序Store发生地址冲突的概率,为违规恢复的代价(周期数),为保守式消歧的等待代价(周期数),为Store Set预测器正确预测时Load等待特定Store的代价(周期数)。
保守式消歧的期望代价:每条Load都等待,代价恒为。
盲推测式消歧的期望代价:。其中的情况下Load无需等待(推测正确),的情况下发生违规。
带预测器的推测式消歧的期望代价:假设预测器覆盖率为(能捕获的重复违规比例),过保守率为。代价为。
典型数值代入:(千分之一冲突率),周期,周期,周期,,。
保守式:周期/Load。
盲推测:周期/Load。
带预测器:周期/Load。
可以看到,盲推测(0.03周期/Load)比保守式(3周期/Load)好100倍。带预测器的代价(0.063周期/Load)虽然比盲推测略高(因为过保守的代价),但在违规频率更高的程序中,预测器可以将代价从降低到——当较大时(如),预测器的优势更为明显:盲推测代价为0.3周期/Load,带预测器代价为0.08周期/Load,改善约4倍。
推测的两个层面
需要注意的是,推测式消歧的推测发生在两个层面:
存在性推测:推测先序Store的地址与Load不冲突(即不存在RAW相关性)。这是消歧的核心推测。
数据推测:在某些激进的设计中,即使知道存在地址冲突,也可能推测Store的数据值(例如猜测Store写入的值为0),提前向Load提供推测数据。这种值预测(Value Prediction)在学术研究中被广泛探讨,但在商业处理器中尚未普遍采用。
本节聚焦于存在性推测,即Memory Disambiguation的核心问题。
设计权衡 1 — 推测式消歧的代价与收益
收益:在SPEC CPU基准测试中,推测式消歧相比保守式消歧可以带来15%30%的IPC提升,在Load密集型工作负载(如数据库查询、链表遍历)中提升更为显著。
代价:
需要Load Queue记录所有推测执行的Load的地址,用于违规检测(面积开销)。
违规发生时需要冲刷流水线,代价类似分支预测失败(约2040周期)。
在违规频率较高的程序中,反复的冲刷可能导致性能下降。
引入了安全隐患:Spectre v4攻击利用推测式消歧来泄露敏感数据(36.4.4 节)。
实际上,内存相关性违规的频率非常低(约千分之一到万分之一),因此推测式消歧的净收益极为可观。以每5000条指令一次违规、每次代价30周期计算,违规恢复的CPI额外开销仅为,即0.6%的CPI增加。与保守式消歧15%25%的IPC损失相比,推测式消歧是压倒性的优势策略。
Store Set预测器
盲目的推测式消歧(总是假设Load与先序Store不冲突)在违规频率极低的工作负载上效果很好,但在某些程序中,特定的Load-Store对会反复冲突。例如,一个函数通过指针写入某个全局变量,而另一个函数通过不同的指针读取同一个变量——这两条指令的地址总是相同,但编译器无法静态判断(因为使用了不同的指针变量),导致它们在硬件中反复触发违规和冲刷。
对于这类反复冲突的Load-Store对,更好的策略是预测它们会冲突,从而让Load等待相应的Store完成后再执行,避免违规恢复的代价。这就是Store Set预测器(Store Set Predictor)的设计动机。
Store Set预测器由Chrysos和Emer在1998年的论文[^1]中提出,其核心思想是:为每条Load维护一个Store Set——即该Load历史上曾经与之发生地址冲突的所有Store的集合。当这些Store的地址尚未计算完成时,Load不能推测执行。
Store Set的基本算法
Store Set预测器的工作流程如下:
初始状态:所有Load的Store Set为空,即所有Load默认可以推测执行(最激进的策略)。
违规检测:当检测到一条Load(PC为)与一条先序Store(PC为)发生地址冲突时,将加入对应的Store Set中。
预测:当Load再次被取指和分发时,如果其Store Set非空,则该Load必须等到Store Set中所有Store的地址计算完成后才能执行。
执行:如果Store Set中所有Store的地址都已就绪且与Load不冲突,则Load可以执行;如果冲突,则进行Store-to-Load转发。
Store Set的"学习"过程是渐进式的:第一次违规时,Load被标记为需要等待特定Store;如果同一Load后续与其他Store也发生冲突,则这些Store也被加入其Store Set。这与分支预测器的训练过程类似——通过历史行为来预测未来行为。
Store Set的合并操作
一个关键的设计问题是Store Set的合并(merging)。考虑以下情况:Load 最初被发现与Store 冲突,后来又被发现与Store 冲突。如果和属于不同的Store Set,那么需要同时属于两个Store Set——这要求硬件支持一条Load对应多个SSID。
为简化实现,Chrysos和Emer的原始方案采用Store Set合并:当一条Load需要加入第二个Store Set时,将两个Store Set合并为一个。具体的合并算法如下:
Load 已关联到Store Set (通过SSID ),包含Store 。
检测到Load 与Store 冲突,属于Store Set (通过SSID )。
如果,需要将和合并为一个Store Set。
合并方法:将所有SSIT中SSID为的条目改为(或反之),使得和现在属于同一个Store Set。
合并后,任何属于原的Load/Store现在都与中的Load/Store关联。
Store Set合并可能导致Store Set过大——多次合并后,一个Store Set可能包含许多实际上与目标Load不冲突的Store。过大的Store Set会导致Load不必要地等待无关Store,降低性能。例如,如果Store Set 包含,Store Set 包含,合并后包含。如果Load 只与和冲突,它现在却需要等待到的地址也就绪——这些等待完全是不必要的。
为此,硬件通常设置一个定期清除(periodic clear)机制——每隔若干千条指令清空所有的SSIT和LFST项,让预测器从零开始重新学习。清除频率需要在"避免过保守"和"保持预测准确性"之间权衡。典型的清除周期为100K1M条提交指令。
SSIT和LFST的硬件结构
直接为每条Load维护一个Store集合的存储开销太大。实际实现中使用一个间接映射结构,由两个表组成,如图图 36.2所示:
SSIT(Store Set ID Table):以指令PC的哈希值索引,将Load或Store的PC映射到一个Store Set ID(SSID)。如果一条Load和一条Store被分配了相同的SSID,则它们属于同一个Store Set,Load需要等待该Store。
LFST(Last Fetched Store Table):以SSID索引,记录该Store Set中最近被分发的Store在Store Queue中的编号。当Load被分发时,通过SSIT查到SSID,再通过LFST查到需要等待的Store的队列编号。
SSIT和LFST的位宽与面积分析
SSIT的每个条目包含以下字段:
有效位(Valid):1位,表示该PC是否有关联的Store Set。
SSID:位。如果LFST有256项,则SSID为8位。
因此每个SSIT条目为9位。如果SSIT有4096项(以PC[13:2]索引),则SSIT总大小为位 = 4.5 KB。
LFST的每个条目包含以下字段:
有效位(Valid):1位,表示该Store Set中是否有活跃的Store。
Store Queue编号:位。如果Store Queue有64项,则需要6位。
Store已分发标志:1位,表示该Store是否已被分发到执行引擎。
每个LFST条目为8位。如果LFST有256项,则LFST总大小为位 = 256 B。
Store Set预测器的查询流程
当一条Load被取指并进入分发阶段时,预测器的查询流程如下:
用Load的PC[13:2]索引SSIT,读出对应条目。
如果条目无效(Valid = 0),说明该Load没有已知的Store Set依赖,可以自由推测执行。
如果条目有效,读出SSID值。
用SSID索引LFST,读出对应条目。
如果LFST条目有效,读出Store Queue编号。这就是Load需要等待的Store。
Load被分发到发射队列时,携带作为其"内存依赖"源操作数。Load必须等到对应的Store地址计算完成后才能发射。
如果LFST条目无效(该Store Set中暂无活跃Store),Load可以正常推测执行。
当一条属于某个Store Set的Store被取指和分发时,更新LFST:用Store的SSID索引LFST,写入当前Store的Store Queue编号。这样,后续查询同一SSID的Load将看到最新的Store位置。
Store Set的建立与更新
当违规检测器发现一条Load()与一条Store()发生地址冲突时,预测器的更新流程如下:
读取SSIT[]和SSIT[]。
情况1:两者都无效。分配一个新的SSID(从空闲SSID池中取出),将两者的SSIT条目都设为这个新SSID。
情况2:一个有效,一个无效。将无效的条目设为有效条目的SSID,使两者属于同一个Store Set。
情况3:两者都有效且SSID相同。无需操作,它们已经在同一个Store Set中。
情况4:两者都有效但SSID不同。执行Store Set合并——将SSIT中所有SSID为的条目改为。这需要遍历整个SSIT,硬件开销较大。实际实现中通常简化为:只将的SSID改为的SSID,而不遍历整个SSIT。这种简化可能导致LFST中残留过时的映射,但定期清除机制会回收它们。
硬件描述 1 — Store Set预测器的典型参数
SSIT:4096项(),以Load/Store的PC[13:2]索引。每项包含一个有效位和一个SSID(810位),总存储约56 KB。
LFST:256项(),以SSID索引。每项包含一个Store Queue编号(67位)和一个有效位,总存储约256 B。
总面积:约6 KB,相比4896项的Load Queue和Store Queue(每项数十字节),Store Set预测器的面积开销较小。
查找延迟:SSIT查找 + LFST查找 = 2次SRAM读取,约12个周期。通常在分发阶段流水线化完成,不增加关键路径延迟。
清除周期:每100K1M条提交指令全部清除一次SSIT和LFST。
SSID分配:新SSID的分配使用简单的递增计数器,计数器宽度等于SSID位宽(810位)。当计数器回绕时,旧的SSID被自然覆盖。
LFST的链式依赖
Store Set预测器的一个精妙细节是LFST中Store Queue编号的链式更新。考虑一个循环中连续分发的多条属于同一Store Set的Store指令(按程序序)。当分发时,LFST被更新为;当分发时,LFST被更新为(覆盖的记录)。
这意味着后续查询LFST的Load将只看到最近的Store ——但Load可能与更老的冲突。这看似不正确,但实际上是安全的:如果Load与冲突,那么要么的地址已经就绪(此时通过正常的Store Queue CAM搜索可以检测到冲突并进行转发),要么的地址未就绪(但比更老,一般情况下会先完成地址计算,当地址就绪时必然也已就绪)。
然而,更保守的实现方式是让LFST维护链表结构——每个Store在分发时,不仅更新LFST中的编号为自己,还记住上一个Store的编号。这样Load可以通过LFST查到最新的Store,再通过链表回溯找到所有同一Store Set中的Store。但这种链表结构的硬件实现复杂度较高,大多数商业处理器选择了更简单的"只记住最新Store"方案。
Store Set预测器的完整工作示例
下面通过一个具体例子展示Store Set预测器从初始化到稳态的完整工作过程。假设SSIT有16项(以PC[5:2]索引),LFST有4项(SSID为2位),SSID计数器初始值为0。
第一轮执行(Store Set为空):
分发Store (PC = 0x1040, PC[5:2] = 0x0):查SSIT[0],无效。可正常分发到SQ#3。
分发Load (PC = 0x1058, PC[5:2] = 0x6):查SSIT[6],无效。可推测执行。
推测执行,从Cache读取地址0x2000的数据 = 0xOLD。
的地址计算完成:写入地址0x2000。
违规检测:地址(0x2000)广播到Load Queue,发现(已执行,地址=0x2000,比年轻)匹配。违规发生。
触发违规恢复:冲刷及其后续指令。
更新Store Set预测器:SSIT[[5:2]] = SSIT[6] = {Valid=1, SSID=0},SSIT[[5:2]] = SSIT[0] = {Valid=1, SSID=0}。SSID计数器递增到1。
第二轮执行(Store Set已建立):
分发Store (PC = 0x1040):查SSIT[0] = {Valid=1, SSID=0}。更新LFST[0] = SQ#5(当前的Store Queue编号)。
分发Load (PC = 0x1058):查SSIT[6] = {Valid=1, SSID=0}。查LFST[0] = SQ#5。被标记为依赖于SQ#5。
进入发射队列,但不发射——等待SQ#5(Store )的地址计算完成。
的地址计算完成:SQ#5的地址就绪信号唤醒。
检查SQ#5的地址(0x2000)与自己的地址(0x2000):匹配!执行Store-to-Load转发,获得的数据。
无违规发生,流水线正常继续。
可以看到,第二轮执行中Store Set预测器成功阻止了的推测执行,避免了违规和冲刷。虽然等待了几个周期(等Store 的地址就绪),但节省了2040周期的违规恢复代价。
Store Set预测器的局限性
Store Set预测器虽然是最成功的内存依赖预测方案之一,但仍有几个局限:
冷启动问题:第一次违规不可避免——预测器只能从违规中学习,无法预防首次违规。在程序启动阶段或代码模式变化时,可能出现一系列冷启动违规。
SSIT别名问题:由于SSIT以PC的低位索引,不同的Load/Store PC可能映射到同一SSIT条目,产生破坏性别名——一条Load的Store Set信息被另一条不相关的Load/Store覆盖。SSIT越小(条目越少),别名问题越严重。4096项的SSIT在实际程序中别名率约为1%5%。
合并导致的过保守:如前所述,Store Set合并可能导致Store Set过大,使Load等待不必要的Store。定期清除可以缓解但不能完全解决此问题。
间接跳转后的Store Set:如果同一PC的Load在不同的调用上下文中与不同的Store冲突(例如通过函数指针调用),Store Set预测器可能无法精确区分不同的上下文。这类似于分支预测中的上下文污染问题。
无法预测首次出现的冲突模式:对于只出现一次的冲突(如特定输入数据导致的地址碰撞),预测器无法提前学习到这种模式。
为什么Store Set优于替代方案:系统性分析
在理解Store Set预测器的设计之后,有必要系统地分析为什么它在众多替代方案中脱颖而出。这种分析不仅有助于理解Store Set的设计精髓,也为评估新的内存依赖预测方案提供了方法论框架。
为什么不用完全保守策略(所有Load等待所有先序Store的地址就绪)?这种策略消除了所有违规,但代价极其高昂。在一个6发射、256项ROB的处理器中,一条Load平均有约13条先序Store(乱序窗口中约1/3是Store)。最慢的Store地址可能需要等待一连串依赖计算,延迟可达2030个周期。完全保守策略将每条Load的有效延迟从45周期(L1命中)提升到2435周期,导致IPC损失15%25%。
为什么不用地址预测(直接预测Store的地址值)?地址预测器需要准确预测48位虚拟地址,难度远高于预测1位的分支方向。即使使用stride预测(预测地址增量),在非规则访问模式下精度也不足。更关键的问题是:当地址预测错误时,Load可能使用错误的转发数据而非Cache中的正确数据,导致功能性错误——这比分支预测错误严重得多(分支预测错误只导致性能损失,不导致功能错误)。因此地址预测需要额外的验证机制来确保正确性。
为什么Store Set优于“上次是否冲突”的简单标记?简单标记(如Alpha 21264的Wait Table)只记录“这条Load曾经冲突过”,但不记录与哪条Store冲突。一旦标记为冲突,Load需要等待所有先序Store。在以下场景中,Store Set的细粒度优势非常明显:
// 循环中有多条Store,但Load只与其中一条冲突
for (int i = 0; i < N; i++) {
A[i] = f(x); // Store S1 (地址已知,不与L冲突)
B[i] = g(y); // Store S2 (地址已知,不与L冲突)
C[ptr[i]] = h(z); // Store S3 (地址取决于ptr[i],可能与L冲突)
D[i] = p(w); // Store S4 (地址已知,不与L冲突)
result += *q; // Load L (与S3偶尔冲突)
}在Wait Table方案中,被标记为“需要等待”后,每次迭代都需要等待、、、全部完成地址计算。如果依赖于一个慢速的指针加载(ptr[i]来自Cache miss),则的等待时间由决定。但、、的地址计算通常很快(简单的基址+偏移),等待它们是浪费。
在Store Set方案中,只需要等待的地址就绪。当地址就绪后,如果与不冲突,立即执行;如果冲突,从获取转发数据。不需要等待、、——节省的延迟可以达到515个周期。
性能分析 2 — Store Set vs. 保守消歧的IPC对比
以SPEC CPU2006的gcc基准为例,定量分析Store Set预测器相对于保守消歧的IPC优势。
步骤1:基线特征。gcc是一个整型编译器,存储器指令占比约35%(20%Load + 15%Store)。在6发射处理器上,乱序窗口中约有90条飞行指令,其中约14条Store和约18条Load。
步骤2:保守消歧的性能。每条Load需要等待所有先序Store的地址就绪。平均先序Store数量约为7条(不是全部14条,因为有些Store在Load之后的程序序中)。最慢先序Store的地址就绪平均需要额外等待1218个周期(取决于依赖链深度)。这将Load的有效延迟从4周期(L1命中)提升到约1622周期。IPC约为3.2(相对于理想的4.55.0)。
步骤3:盲推测(无预测器)的性能。所有Load立即推测执行。违规率约为每10K条指令发生1次违规,每次违规的恢复代价约3040周期。IPC约为4.2,但因偶尔的违规恢复导致不稳定的流水线行为。
步骤4:Store Set预测器的性能。Store Set将95%的重复违规消除,残余违规率降至约每100K条指令1次。需要等待的Load只等待特定的12条Store(而非全部7条),平均额外等待35个周期。IPC约为4.4——接近盲推测但几乎没有违规。
步骤5:性价比。Store Set预测器以约6 KB的硬件面积(SSIT 4.5 KB + LFST 0.25 KB),将IPC从保守消歧的3.2提升到4.4(+37.5%),同时将违规率降低了10以上。6 KB的面积开销不到一个64项Store Queue面积的10%。这是处理器微架构中投资回报率最高的预测器之一。
Alpha 21264是首个在商用处理器中实现内存依赖预测的处理器之一(1998年)。其Store Wait Table设计虽然比完整的Store Set简单,但充分体现了“用最小硬件解决最大问题”的工程智慧。
Alpha 21264是一个4发射、80项ROB的乱序处理器。其内存依赖预测机制由一个1024位(128字节)的位向量表组成,以Load PC的低10位索引。工作流程如下:
Load首次执行时,推测性地绕过所有先序Store直接从Cache读取数据。
当先序Store的地址计算完成后,硬件检测是否与之前推测执行的Load冲突。
如果检测到冲突(违规),将Wait Table中对应Load PC索引位置的位设为1。
后续相同PC的Load在分发时检查Wait Table:如果对应位为1,该Load不推测执行,而是等待所有先序Store的地址就绪后再执行。
Wait Table每隔约100K条提交指令全部清零,防止长期过保守。
Alpha 21264的设计者Kessler在论文中报告:Wait Table将违规率降低了约10(从每5K条指令一次降低到每50K条指令一次),总IPC损失仅约0.5%(主要来自少数被保守等待的Load)。
这一设计的关键洞察是:在SPEC CPU基准中,只有不到0.5%的静态Load指令会发生重复违规——这些少数“坏”Load对应的PC在Wait Table中只占极少数位。128字节的存储足以覆盖这些PC,别名冲突的概率很低。这也解释了为什么一个如此简单的预测器就能取得90%以上的违规消除效果。
Alpha 21264的经验对后续处理器设计的影响深远:它证明了“内存依赖可以像分支一样被预测”这一基本命题,为后来Intel和AMD处理器中更精细的Store Set预测器奠定了基础。从第 14.0 章中讨论的分支预测历史来看,Alpha 21264在内存依赖预测领域的角色类似于Alpha 21064在分支预测领域的角色——两者都在商用处理器中首次验证了“预测+推测+恢复”这一设计范式的可行性。
:::
Store Set预测器的完整算法伪代码
为了精确描述Store Set预测器的行为,下面给出其四个核心操作的完整伪代码。这些伪代码直接对应硬件实现中的组合逻辑和状态机。
// 全局状态
SSIT[4096]; // Store Set ID Table, 以PC[13:2]索引
LFST[256]; // Last Fetched Store Table, 以SSID索引
uint8_t ssid_counter; // SSID分配计数器, 范围0-255
uint32_t commit_count; // 提交指令计数, 用于定期清除
// ==========================================
// 操作1: Load分发时的依赖查询 (每条Load执行)
// ==========================================
function OnLoadDispatch(load_pc, load_iq_entry):
idx = load_pc[13:2] // 提取PC索引位
if SSIT[idx].valid == 0:
// 无Store Set关联, Load可自由推测
load_iq_entry.store_wait = false
return
ssid = SSIT[idx].ssid
if LFST[ssid].valid == 0:
// Store Set存在但无活跃Store
load_iq_entry.store_wait = false
return
// Load需要等待指定Store的地址就绪
load_iq_entry.store_wait = true
load_iq_entry.wait_sq_num = LFST[ssid].sq_number
// ==========================================
// 操作2: Store分发时的LFST更新 (每条Store执行)
// ==========================================
function OnStoreDispatch(store_pc, store_sq_number):
idx = store_pc[13:2]
if SSIT[idx].valid == 1:
ssid = SSIT[idx].ssid
// 更新LFST: 记录此Store Set中最新的Store
LFST[ssid].valid = 1
LFST[ssid].sq_number = store_sq_number
LFST[ssid].dispatched = 1
// ==========================================
// 操作3: 违规检测后的Store Set建立/合并
// ==========================================
function OnViolation(load_pc, store_pc):
l_idx = load_pc[13:2]
s_idx = store_pc[13:2]
l_valid = SSIT[l_idx].valid
s_valid = SSIT[s_idx].valid
if !l_valid && !s_valid:
// 情况1: 都无关联, 分配新SSID
new_ssid = ssid_counter++
SSIT[l_idx] = {valid=1, ssid=new_ssid}
SSIT[s_idx] = {valid=1, ssid=new_ssid}
else if l_valid && !s_valid:
// 情况2: Load有关联, Store无关联
SSIT[s_idx] = {valid=1, ssid=SSIT[l_idx].ssid}
else if !l_valid && s_valid:
// 情况3: Store有关联, Load无关联
SSIT[l_idx] = {valid=1, ssid=SSIT[s_idx].ssid}
else if SSIT[l_idx].ssid != SSIT[s_idx].ssid:
// 情况4: 都有关联但SSID不同, 执行合并
// 简化合并: 将Store的SSID改为Load的SSID
SSIT[s_idx].ssid = SSIT[l_idx].ssid
// ==========================================
// 操作4: 定期清除 (每~500K条提交指令)
// ==========================================
function OnCommit():
commit_count++
if commit_count >= CLEAR_INTERVAL:
for i in 0..4095: SSIT[i].valid = 0
for i in 0..255: LFST[i].valid = 0
commit_count = 0上述伪代码中有几个值得关注的硬件实现细节:
操作1(Load查询)的时序。Load查询需要串行访问两个表(SSITLFST),总延迟为2个SRAM读取周期。在高频处理器中,这2个周期需要被安排在分发(dispatch)流水线的特定阶段。如果分发阶段本身只有12个周期,Store Set查询可能成为分发阶段的关键路径。优化方案是将SSIT查询提前到分配(allocate)阶段的前半部分,LFST查询放在分配阶段的后半部分,这样Store Set查询不会增加分发阶段的延迟。
操作3(违规更新)的原子性。违规更新涉及对SSIT的读-改-写操作——先读取两个条目,判断情况,再写入一个或两个条目。在硬件中,这需要一个多周期的更新序列。如果在更新过程中恰好有另一条Load正在查询被修改的SSIT条目,可能读到不一致的状态。实际实现中,违规更新的优先级低于Load/Store分发查询——如果存在端口冲突,违规更新被延迟一周期。这种延迟不影响正确性(只延迟了Store Set关联的建立),但需要在SSIT的端口设计中考虑仲裁逻辑。
操作4(定期清除)的实现。全清除不是在一个周期内完成的——4096项SSIT的逐项清除需要约1024周期(假设每周期清除4项)。在清除过程中,SSIT仍然可以被正常查询(查询到正在被清除的项时,其valid位已经被清零,等效于“无关联”)。更快的实现是为SSIT添加一个全局的epoch位——每次清除只翻转epoch位,条目中存储的epoch与全局epoch不匹配的条目被视为无效。这种方案将清除时间从1024周期减少到1周期,但每个SSIT条目需要额外存储1位epoch。
Store Set的Aging与拆分机制
定期全清除(periodic clear)虽然简单有效,但存在一个根本性的缺陷:清除是全局性的——它不区分“仍然有用的Store Set关联”和“已经过时的Store Set关联”。清除后的重新学习过程中,已有的正确关联也被丢弃,可能导致一段时间内违规率回升(“清除后违规尖峰”现象)。
更精细的替代方案是Aging机制——为每个SSIT/LFST条目添加一个位的年龄计数器(通常或):
计数器递增:每隔一定数量的提交指令(如每10K条),所有SSIT条目的年龄计数器递增1。
计数器复位:当某个Store Set关联被实际使用(Load查询SSIT命中并从LFST获取了Store依赖信息),或者违规检测触发了对该关联的更新时,年龄计数器复位为0。
条目回收:当年龄计数器达到最大值(如)时,该SSIT条目被标记为无效,释放SSID。
Aging机制的效果是:频繁使用的Store Set关联(对应热路径上的反复冲突模式)永远不会被回收,而偶尔建立但此后不再触发的关联会在若干个老化周期后自然消失。这比全清除更精细——避免了清除后的违规尖峰,同时仍能回收过时的条目。
Store Set拆分是解决合并过度保守问题的另一种手段。当定期清除或aging回收了一个过大的合并Store Set后,重新学习过程中,原来被合并在一起的多个Load-Store对可能被分配到不同的SSID——因为SSID计数器已经递增到了新值。这相当于隐式地“拆分”了之前过大的Store Set。更积极的拆分策略是:当检测到一个Store Set中的某条Load在若干次执行中都没有发生违规时,主动将该Load从Store Set中移除(清除其SSIT条目的有效位)。
Store Set预测器的工作负载敏感性
Store Set预测器的有效性高度依赖于工作负载的内存访问模式。不同类型的程序展现出截然不同的Store Set行为,理解这种工作负载敏感性对于评估预测器设计和调整参数至关重要。
数据库工作负载。数据库系统(如MySQL、PostgreSQL的查询处理器)是Store Set预测器最具挑战性的工作负载之一。原因在于数据库的核心数据结构——B+树索引——具有高度指针追踪(pointer chasing)的特征。在B+树的搜索过程中,每次节点访问涉及:(1) 从当前节点Load子节点指针;(2) 通过指针Store中间结果到栈帧或临时缓冲区;(3) 下一次迭代的Load访问上一次Store写入的地址。这种“Store写入地址X,随后Load从地址X读取”的模式反复出现,但地址X在不同迭代中不同(因为B+树的不同节点位于不同的内存地址)。
对Store Set预测器而言,这意味着:同一对Load-Store PC可能在某些迭代中冲突(当恰好访问同一节点或相邻节点时),而在其他迭代中不冲突。预测器在第一次冲突后建立关联,但后续大多数迭代不冲突——导致Load被不必要地等待(过保守)。数据库工作负载的违规频率通常为每5K20K条指令1次,高于科学计算但低于极端情况。Store Set预测器在数据库上的IPC收益约为8%15%(相对于盲推测的违规恢复开销)。
| 工作负载类型 | 违规率 | 活跃Store Set数 | 过保守率 | 典型模式 |
|---|---|---|---|---|
| 科学计算 | 极低 | 10 | 1% | 规则数组步进 |
| 整型编译器 | 低中 | 2050 | 3%5% | 寄存器溢出/恢复 |
| 数据库(OLTP) | 中高 | 50200 | 5%15% | 指针追踪 + B+树 |
| Web服务器 | 中 | 30100 | 5%10% | 散列表查找 |
| JIT编译代码 | 高 | 100300 | 8%20% | 间接分发 + 多态 |
不同工作负载类型的Store Set行为特征
科学计算工作负载。高性能计算(HPC)代码(如BLAS线性代数库、CFD流体模拟)中的存储器访问模式通常非常规则——数组访问的步长固定,Load和Store的地址在编译时就可以通过依赖分析确定。在这种工作负载中,Store Set预测器几乎不需要工作:违规率极低(每100K1M条指令才发生一次冲突),活跃的Store Set数量不超过个位数。
这并不意味着Store Set预测器在HPC中毫无价值。库函数(如malloc/free的堆管理代码、C++标准库的容器操作)仍然包含指针密集的数据结构操作,这些操作穿插在规则的计算循环之间。一个典型的HPC程序的时间分解可能是95%的规则计算(Store Set空闲)+ 5%的库调用/系统管理(Store Set活跃)。
JIT编译代码。Java/JavaScript等语言的JIT编译器产生的机器代码是Store Set面临的最困难场景。原因有三:(1) 虚函数调用(vtable dispatch)通过间接跳转实现,同一PC的Load在不同调用目标下可能与完全不同的Store冲突——Store Set无法区分不同的调用上下文。(2) 垃圾回收器(GC)会移动对象,导致之前学习到的Store Set关联在GC后完全失效。(3) JIT重编译(re-JIT)改变了机器代码的PC布局,使得SSIT中的旧条目全部失效。
对于JIT场景,一些处理器选择在检测到大量上下文变化时主动触发SSIT清除——类似于第 15.0 章中某些分支预测器在检测到“程序相变”(phase change)时主动重置预测器状态。
Store Set与IQ依赖等待机制的集成
Store Set预测器的输出——“Load 需要等待Store 的地址计算完成”——最终需要在第 27.0 章讨论的发射队列(Issue Queue, IQ)中落实为硬件依赖。这种集成涉及几个精细的微架构问题。
依赖表示。当Load 在分发(dispatch)阶段通过SSITLFST查找得到需要等待的Store 的Store Queue编号时,这个依赖关系需要被编码到IQ中。具体实现有两种策略:
伪寄存器依赖。为每条Store的地址计算结果分配一个“伪物理寄存器编号”,Load 在IQ中将此伪寄存器作为一个额外的源操作数。当Store 的地址计算完成并广播写回时,这个伪寄存器被标记为就绪,唤醒等待它的Load 。这种方案复用了IQ已有的依赖跟踪和唤醒机制,不需要额外的硬件——但代价是占用了一个源操作数槽位(通常限制为23个),减少了Load可以携带的真正数据依赖数量。
专用等待位。在IQ的每个条目中添加一个“Store等待位”和一个Store Queue编号字段。当Store等待位为1时,op不仅需要所有源操作数就绪,还需要指定的Store完成地址计算。Store Queue广播“地址就绪”信号时,与IQ中的Store Queue编号进行匹配,清除匹配条目的Store等待位。这种方案不占用源操作数槽位,但需要在IQ中添加专用的比较逻辑和唤醒通路。
现代商业处理器大多采用第二种方案的变体。在Intel处理器中,Load在IQ中可以同时等待两类事件:源操作数就绪(通过标准唤醒矩阵)和Store地址就绪(通过专用的Store Queue地址就绪广播)。两类事件的AND逻辑控制Load的发射时机——只有当源操作数和指定Store的地址都就绪时,Load才能被选中发射。
Store Set miss的处理。当Load在分发时查询SSIT未命中(Store Set为空或LFST无效),Load不携带任何Store依赖信息,可以在源操作数就绪后立即发射——这是最激进的推测模式。如果随后检测到违规,违规恢复机制(第 38.0 章)会冲刷Load及其后续指令,同时更新Store Set预测器以防止下次重蹈覆辙。
LFST的时效性。一个微妙的时序问题是:Load在分发阶段查询LFST得到的Store Queue编号可能已经“过时”——如果在Load分发之后、发射之前,同一Store Set中有新的Store被分发,LFST会被更新为更新的Store Queue编号,但Load携带的仍是旧编号。这种情况下,Load可能等待的是一条已经完成的旧Store(此时Load会立即被唤醒,不产生额外延迟),而忽略了真正应该等待的新Store。
这个问题在大多数情况下是良性的——如果新Store与Load确实冲突,正常的Store Queue CAM搜索机制会在Load执行后检测到冲突并触发转发或违规。但在极端情况下(新Store的地址计算非常慢),Load可能已经从Cache读取了过期数据、经过违规恢复后才获得正确值——这增加了一次违规恢复的开销。更保守的实现可以在Load发射时重新查询LFST(“晚查询”策略),但这会增加发射阶段的关键路径延迟。
性能分析 3 — Store Set在数据库工作负载上的IPC分析
以TPC-C基准测试(模拟OLTP数据库事务处理)为例,分析Store Set预测器对数据库工作负载的性能影响。
步骤1:工作负载特征。TPC-C的核心操作是New Order事务,涉及B+树索引查找、行锁获取、日志写入和索引更新。存储器指令占比约38%(22%Load + 16%Store)。B+树查找的指针追踪深度平均为34层。
步骤2:违规模式。在6发射、256项ROB的处理器上,盲推测的违规率为每3K5K条指令1次——显著高于SPEC CPU的每10K条指令1次。原因是B+树节点中的子指针Store和后续的指针解引用Load频繁冲突。每次违规恢复代价约35个周期。盲推测的IPC约为3.6(频繁违规恢复严重影响性能)。
步骤3:Store Set的效果。Store Set预测器将90%的重复违规消除,残余违规率降至每30K50K条指令1次。但过保守率较高(约12%),因为B+树的不同层级可能被合并到同一个Store Set中。需要等待的Load平均额外等待610个周期。IPC约为4.0。
步骤4:与保守消歧对比。保守消歧在TPC-C上的IPC约为2.8(因为大量Load需要等待多条先序Store)。Store Set相对于保守消歧的IPC提升为43%(),甚至高于SPEC CPU上的37.5%——这是因为数据库工作负载中先序Store数量更多,Store Set的细粒度等待优势更加显著。
步骤5:优化空间。TPC-C上Store Set的12%过保守率主要来自B+树不同层级的Store Set合并。如果采用更精细的aging机制(每5K条指令老化一次而非全清除),过保守率可以降至7%,IPC进一步提升到4.15。但aging机制增加了约20%的SSIT面积(每条目额外23位年龄计数器)。
Store Set完整生命周期:从建立到淘汰
为了将Store Set预测器的所有细节整合为一个连贯的整体,下面按时间线描述一个Store Set关联的完整生命周期。这个描述综合了前面各节讨论的建立、查询、合并、aging和拆分机制。
阶段1:冷启动推测(T=0T1)。程序开始执行。SSIT和LFST全部为空(有效位=0)。所有Load均可自由推测执行。处理器以盲推测模式运行,享受最低的Load有效延迟(4周期,仅L1命中延迟)。
阶段2:首次违规与Store Set建立(T=T1)。Load (PC=0x4028)推测执行后,先序Store (PC=0x4010)的地址计算完成,违规检测器发现两者地址冲突。触发违规恢复(冲刷及后续约50100条指令,耗时35周期)。同时更新预测器:分配SSID=17,写入SSIT[0x4028] = {Valid=1, SSID=17}和SSIT[0x4010] = {Valid=1, SSID=17}。这是不可避免的“首次违规代价”。
阶段3:预测命中(T=T1+T2)。Store 被再次取指和分发时,SSIT查找命中,LFST[17]被更新为当前的Store Queue编号SQ#23。随后Load 被分发时,SSIT查找返回SSID=17,LFST[17]返回SQ#23。在IQ中携带“等待SQ#23地址就绪”的标记。当SQ#23地址就绪后,被唤醒:如果地址匹配(冲突),执行STLF转发;如果地址不匹配(不冲突),正常从Cache读取。无论哪种情况,不发生违规。
阶段4:合并事件(T=T3,可选)。如果另一个Load (PC=0x5044)被发现与Store (PC=0x5020)冲突,且已经属于另一个Store Set(SSID=23),但之前也与冲突过(的SSID=17),则需要合并SSID=17和SSID=23。简化实现:将SSIT[0x5020]的SSID从23改为17。合并后,SSID=17的Store Set变大——不仅包含和,还包含和。
阶段5:老化与回收(T=T2+)。每隔条提交指令(如10K条),所有SSIT条目的年龄计数器递增。如果Store Set关联在若干个老化周期内未被查询或更新,其年龄计数器达到最大值,条目被标记为无效。SSID被释放回空闲池,可供后续新的Store Set关联使用。
阶段6:全清除(T=,如果使用全清除策略)。每隔条提交指令(如500K条),SSIT和LFST的所有条目被批量清零。所有Store Set关联被一次性丢弃,预测器回到冷启动状态。清除后的若干千条指令内,可能出现若干次首次违规——但这些违规会迅速重建必要的Store Set关联。清除的目的是防止长期积累的过大Store Set导致持续的过保守行为。
Store Set查找的完整决策流程
将Store Set预测器的查找过程分解为一个精确的决策流程图,有助于理解每个硬件判断节点对应的逻辑门和比较器。图图 36.5展示了Load分发时的完整决策流程。
决策流程中的关键分支点对应以下硬件开销:
SSIT Valid判断:1位比较器,延迟可忽略。大约95%的Load在此处被过滤(直接推测执行)。
LFST Valid判断:1位比较器。约50%走到此处的Load在此被过滤(Store Set存在但当前无活跃Store)。
SQ#地址就绪判断:需要查询Store Queue中对应条目的“地址有效”位。这个查询可以在Load进入IQ时完成,如果地址已就绪则Load不需要等待。
地址匹配判断:完整的地址比较(4549位),这是整个消歧流程中最昂贵的比较操作,发生在Load实际执行时。
Store Set预测器的定量性能模型
为建立对Store Set预测器性能影响的精确直觉,可以构建一个简单的解析性能模型。设以下参数:
:盲推测下的违规频率(每条指令1次违规,)。
:每次违规的恢复代价(周期数)。
:Store Set预测命中时,Load额外等待的平均周期数。
:Store Set预测器的覆盖率(能够预防的重复违规比例)。
:过保守率(预测冲突但实际不冲突的比例)。
:需要Store Set保护的Load比例(在所有Load中,SSIT命中的比例)。
盲推测的CPI损失(无预测器):
例如,,周期,则,即每条指令平均多花0.0035周期在违规恢复上。
Store Set预测器的CPI损失:
第一项是残余违规的恢复代价(预测器未能覆盖的违规),第二项是预测器导致的等待代价。其中包含了正确预测和过保守预测两部分。
将两者对比,Store Set的净收益为:
代入典型值:,,,(0.5%的Load需要等待),周期:
这个结果看似Store Set预测器的等待代价超过了收益,但这是因为模型过于简化——假设每次SSIT命中都产生等待,实际上约50%的SSIT命中时Store地址已经就绪()。修正后:
对于违规频率更高的工作负载(如数据库,):
这揭示了一个重要的设计洞察:Store Set预测器的收益对违规频率高度敏感。在违规极其稀少的科学计算工作负载中(),预测器的等待代价远大于节省的违规恢复代价——此时最优策略是完全禁用Store Set。而在违规频繁的数据库工作负载中,预测器的净收益为正且显著。
注意:上述简化模型没有考虑违规恢复对流水线吞吐率的连锁影响。一次违规恢复不仅浪费了个周期,还丢弃了ROB中50100条已执行但未提交的指令的工作量。考虑这种连锁影响后,的有效值可能高达80150周期(包括重新取指、解码和执行被冲刷的指令),使得Store Set预测器的净收益更加显著。
地址部分匹配与假依赖
Store Set预测器的另一个微妙问题是SSIT的索引别名(aliasing)与Store Queue中的地址部分匹配之间的交互。
SSIT以PC的低位索引(如PC[13:2]),这意味着不同的Load/Store PC可能映射到同一个SSIT条目。当两条不相关的Load映射到同一SSIT条目时,它们会共享同一个SSID——这导致一条Load的Store Set信息被另一条Load的冲突历史“污染”。例如:
// Load L1 (PC=0x1028) 与 Store S1 (PC=0x2040) 反复冲突
// Load L2 (PC=0x5028) 从不冲突,但PC[13:2] == 0x1028的PC[13:2]
// 结果:L2被错误地分配了与S1相同的SSID,导致L2不必要地等待S1别名导致的假依赖是纯粹的性能损失——等待了一条它根本不需要等待的Store,白白增加了有效Load延迟。4096项的SSIT在SPEC CPU 2006基准中的别名率约为1%5%,但在指令footprint更大的服务器工作负载(如数据库、JVM)中可能达到8%12%。
缓解别名的方案包括:
增加SSIT大小。从4096项增加到8192或16384项,将别名率降低约一半。代价是SSIT面积翻倍(从4.5 KB增加到9 KB或18 KB)。
添加PC tag。在每个SSIT条目中存储PC的高位若干位作为tag,仅当索引匹配且tag匹配时才认为命中。这将SSIT从直接映射变为带tag的半关联结构,大幅降低别名率。代价是每条目增加812位存储和一个tag比较器。
置信度计数器。在SSIT条目中添加2位置信度计数器:每次预测成功(预测冲突且实际冲突)时递增,每次预测失败(预测冲突但实际不冲突)时递减。当计数器为0时,认为该关联已经不可信,Load可以忽略Store Set约束直接推测执行。这种方案的开销最小(每条目仅增加2位),且能自适应地处理别名和过时关联。
Store Set预测器与分支预测器的类比
Store Set预测器的设计方法论与第 14.0 章第 15.0 章中讨论的分支预测器有深刻的相似性。下表系统地列出了两者的对应关系,有助于建立跨章节的统一理解。
| 设计维度 | 分支预测器 | Store Set预测器 |
|---|---|---|
| 预测对象 | 分支是否跳转(1位决策) | Load是否与特定Store冲突 |
| 索引方式 | 分支PC(可能哈希全局历史) | Load/Store PC |
| 训练信号 | 分支解析后的实际方向 | 违规检测后的冲突Load-Store对 |
| 训练延迟 | 分支解析延迟(1520周期) | 违规检测延迟(2040周期) |
| 误预测代价 | 分支误预测恢复(1520周期) | 违规恢复(3040周期) |
| 冷启动问题 | 首次分支不可预测 | 首次冲突不可预防 |
| 别名问题 | PHT别名(不同分支共享条目) | SSIT别名(不同Load共享SSID) |
| 上下文敏感性 | 全局历史提供路径上下文 | 无上下文(仅基于PC) |
| 过保守/过激进 | 过激进误预测冲刷 | 过保守不必要等待IPC损失 |
| 面积预算 | 3264 KB(TAGE-SC-L) | 6 KB(SSIT+LFST) |
Store Set预测器与分支预测器的系统性类比
这个类比揭示了一个关键差异:分支预测器投入了大量面积(3264 KB)来获取极高的预测精度(96%),因为分支预测对IPC的影响是全局性的(每57条指令就有一条分支)。相比之下,Store Set预测器只用6 KB就达到了93%95%的覆盖率,这是因为需要预测的“事件”(Load-Store冲突)远比分支稀少(每10K100K条指令才发生一次冲突),且大多数情况下盲推测就是正确的。这解释了为什么商业处理器愿意为分支预测器投入巨大的面积预算,而对Store Set预测器的面积增加则更为谨慎——边际收益率截然不同。
Store Set在多线程环境中的挑战
在支持SMT(同时多线程,参见第 40.0 章)的处理器中,Store Set预测器面临额外的挑战。多个硬件线程共享同一个物理前端(包括SSIT和LFST),但不同线程的Load-Store冲突模式通常完全不同。
SSIT共享与线程间干扰。如果两个线程共享一个SSIT,线程A中某条Load的Store Set关联可能被线程B中恰好PC低位相同的Load/Store覆盖——这是线程间的破坏性别名。解决方案包括:
SSIT分区。将SSIT的条目按线程ID(Thread ID)静态分区——Thread 0使用SSIT的前半部分,Thread 1使用后半部分。分区消除了线程间别名,但每个线程可用的SSIT容量减半。
SSIT标签扩展。在SSIT条目的tag中包含线程ID位。查找时同时匹配PC低位和线程ID,消除跨线程别名。每条目增加12位(对于24个硬件线程)。
独立SSIT。为每个硬件线程维护完全独立的SSIT实例。面积开销是共享SSIT的(为线程数),但完全消除了线程间干扰。在2-way SMT中,面积开销为2SSIT 9 KB,通常是可以接受的。
LFST的线程隔离。LFST中存储的Store Queue编号必须按线程隔离——Thread 0的Load不应该等待Thread 1的Store。最简单的实现是在LFST条目中添加线程ID字段,Load查询LFST时只匹配相同线程ID的条目。
跨线程违规。在SMT中,是否存在跨线程的“违规”?严格来说,两个线程在架构层面通过内存模型(TSO或RVWMO)定义了可见性关系,而非通过程序序的Load-Store关系。因此,Store Set预测器只需要在同一线程内跟踪Load-Store冲突,不需要关注跨线程的冲突——跨线程的数据一致性由缓存一致性协议(第 37.0 章)保证。
SSIT/LFST的功耗优化
Store Set预测器虽然面积较小(6 KB),但它在每条Load和Store的分发阶段都会被查询——在6发射处理器中,这意味着每周期可能有34次SSIT读取和12次LFST读取。频繁的SRAM读取产生的动态功耗不可忽视。
功耗优化策略包括:
条件性查询。在许多程序中,95%以上的Load从未发生违规,它们的SSIT条目要么为空要么从未被建立。可以在SSIT之前添加一个小型的Bloom Filter(256位),记录哪些PC[13:2]范围内有活跃的Store Set。只有当Bloom Filter报告“可能有关联”时,才实际读取SSIT。在典型工作负载中,95%的查询会被Bloom Filter过滤掉(报告“无关联”),节省95%的SSIT读取功耗。Bloom Filter的假阳性率约为1%3%,会导致少量不必要的SSIT读取,但不影响正确性。
时钟门控。在SSIT和LFST的SRAM阵列上应用精细的时钟门控——只有在确实需要读取或写入时才激活时钟。这可以将静态功耗(泄漏电流)降低约30%50%。
LFST的延迟查询。LFST的查询可以推迟到SSIT命中后才进行(串行查询),而非SSIT和LFST并行查询。串行查询增加了1周期的查询延迟,但当SSIT未命中时(大多数情况),LFST完全不被访问。这种策略在面积和功耗受限的嵌入式处理器中尤为有价值。
硬件描述 2 — Store Set预测器的完整硬件参数(面向实现的规格)
以下参数面向6发射、256项ROB、64项Store Queue的高性能处理器设计:
SSIT:4096项,直接映射,每项9位(1位Valid + 8位SSID)。总存储:4.5 KB。读端口:6个(支持6发射宽度的Load/Store分发)。写端口:2个(支持违规反馈更新 + 定期清除/aging)。读延迟:1周期。工艺节点估算面积:7nm下约0.01 mm。
LFST:256项,直接映射(SSID索引),每项8位(1位Valid + 6位SQ# + 1位已分发标志)。总存储:256 B。读端口:4个(支持最多4条Load同时查询)。写端口:4个(支持最多4条Store同时更新)。读延迟:1周期。
Bloom Filter(可选):256位位向量,以PC[9:2]索引。用于功耗优化的条件性查询。面积:32 B,可忽略。
Aging计数器(可选):2位/SSIT条目,总计8192位 = 1 KB。每10K条提交指令递增一次。
总面积预算:基本配置(SSIT+LFST) 5 KB;完整配置(含Bloom Filter + Aging) 6.5 KB。
功耗预算:在3 GHz频率下,估算动态功耗约1525 mW(含所有读写端口活动),约占核心总功耗的0.1%0.2%。
其他内存依赖预测方案
Store Set预测器是内存依赖预测(Memory Dependence Prediction)的经典方案,但不是唯一的方案。本节讨论更广泛的内存依赖预测器设计空间。
简单计数器预测器
最简单的方案是为每条Load维护一个饱和计数器(saturating counter),类似分支预测中的2位计数器:
当Load发生违规时,计数器递增(更倾向于"等待")。
当Load没有发生违规时,计数器递减(更倾向于"推测")。
当计数器超过阈值时,该Load在所有先序Store地址就绪前不推测执行。
这种方案的优点是硬件简单——只需一个以Load PC索引的2位计数器表。一个4096项的计数器表仅需位 = 1 KB。缺点是它只能决定"等待所有Store"或"不等待",无法精确指出应该等待哪个Store。这意味着一旦预测为"等待",Load需要等待所有先序Store的地址就绪,导致不必要的延迟。
基于距离的预测器
另一种方案记录冲突Store与Load之间的指令距离(即在ROB中的年龄差)。当Load被分发时,如果预测器指示该Load可能与距离为的先序Store冲突,Load只需等待ROB中年龄差为的Store的地址就绪,而不需等待所有先序Store。这种方案的优点是比计数器预测器更精确(不需要等待所有Store),但它假设冲突总是发生在相同距离的Store-Load对之间——当循环展开或代码模式变化时,这个假设可能不成立。
Alpha 21264的Store Wait Map
Alpha 21264采用的消歧策略是一种简化的Store Set预测器。处理器维护一个称为Wait Table(也称为Store Wait Map)的1024项位向量表,以Load PC的低10位索引。当一条Load发生违规时,对应的Wait Table项被设置为1,后续该Load将等待所有先序Store地址就绪后再执行。Wait Table定期被全部清零(每隔100K条指令),以防止过于保守导致性能下降。
与完整的Store Set预测器相比,Alpha 21264的方案有以下差异:
无法指定等待哪个Store:一旦Load被标记为"需要等待",它必须等待所有先序Store,而Store Set预测器只要求等待特定的Store。
面积更小:只需1024位 = 128 B,远小于Store Set预测器的6 KB。
无合并问题:没有Store Set合并带来的过保守问题。
精度较低:由于PC的低10位索引会产生别名(不同的Load PC映射到同一条目),可能导致无关Load被错误地标记为"需要等待"。
这种"粗粒度"的预测器虽然简单,但在Alpha 21264上取得了良好的效果——违规恢复的频率降低了约90%,而额外的等待延迟仅增加了约2%的CPI。其成功的关键在于:在实际程序中,反复发生违规的Load只占极少数(1%),而这些Load通常总是与同一条Store冲突。
预测器的训练与反馈
所有内存依赖预测器都依赖于反馈训练——通过违规检测的结果来更新预测器状态。训练的时效性(training latency)是一个重要因素:
违规通常在Store地址计算完成后才被检测到,此时可能距离Load被分发已经过去了许多周期。
在违规被检测到并更新预测器之前,同一条Load(如果处于循环中)可能已经被多次分发和错误推测执行。
为了加速训练收敛,一些设计在违规恢复后的重新执行中,立即对该Load应用保守策略(不依赖预测器查找结果),避免在预测器更新完成前再次违规。这可以通过在ROB中设置一个"最近违规"标志来实现——在重新执行时检查此标志,强制Load等待。
预测器的精度度量
类似于分支预测器,内存依赖预测器的质量可以用以下指标衡量:
违规残余率(Residual Violation Rate):使用预测器后仍然发生的违规频率。越低越好。
过保守率(Over-Conservatism Rate):预测器指示Load需要等待,但实际上Load与对应Store不冲突的比例。过保守导致不必要的延迟。
覆盖率(Coverage):预测器能够捕获的重复违规Load占所有违规Load的比例。
理想的预测器应该同时具有低违规残余率和低过保守率——只对真正会冲突的Load-Store对施加等待约束,其他Load自由推测。
各预测器方案的量化对比
表表 36.4对比了各种内存依赖预测器的关键参数。数据基于SPEC CPU 2006基准测试的模拟结果。
| 方案 | 面积 | 覆盖率 | 过保守率 | 精度 |
|---|---|---|---|---|
| 无预测器(盲推测) | 0 | 0% | 0% | – |
| 计数器预测器 | 1 KB | 85% | 15% | 粗粒度 |
| Wait Table(Alpha 21264) | 128 B | 90% | 10% | 粗粒度 |
| Store Set(原始方案) | 6 KB | 95% | 5% | 细粒度 |
| Store Set + 定期清除 | 6 KB | 93% | 3% | 细粒度 |
内存依赖预测器的方案对比
从表中可以看出,Store Set预测器在覆盖率和过保守率上都优于更简单的方案。其主要优势在于细粒度——能够指定Load应该等待哪个Store,而不是让Load等待所有先序Store。这在有多条先序Store的场景中差异显著:假设一条Load有10条先序Store,其中只有1条与之冲突。Wait Table方案让Load等待全部10条Store的地址就绪(等待最慢的那条),而Store Set只需要等待1条特定Store的地址就绪,节省的延迟可能达到510个周期。
高级预测器方案
在学术文献中,已有多种高级内存依赖预测器被提出:
基于Bloom Filter的预测器:使用Bloom Filter存储历史冲突的Store PC集合,Load在分发时查询Bloom Filter判断是否需要等待。优点是面积小、查找快;缺点是存在假阳性。
基于路径历史的预测器:类似于TAGE分支预测器的思路,使用Load的PC和前面若干条分支的路径历史作为联合索引来查找预测表。这种方案可以区分同一Load PC在不同调用路径下的不同冲突模式,精度更高。但面积开销也更大(需要存储路径历史信息)。
基于依赖距离的预测器:记录Load与冲突Store之间的ROB距离,Load只需等待距离为的先序Store。这种方案的精度介于计数器预测器和Store Set之间。
Oracle式预测器(仅限模拟研究):假设完美预测——只对真正冲突的Load-Store对施加等待,其他Load完全自由推测。在SPEC CPU中,Oracle预测器相比Store Set预测器可以额外提升约0.5%2%的IPC,说明Store Set已经接近最优,改进空间有限。
但在商业处理器中,由于面积和时序的限制,大多数设计仍然采用相对简单的方案——Store Set预测器或其变体已经能够捕获绝大多数的重复违规模式,更复杂的预测器带来的边际收益不足以证明额外的硬件开销。
案例研究 2 — Intel Core微架构的内存消歧
Intel从Merom(Core 2, 2006年)开始引入了Memory Disambiguation预测器,称为Store Address Prediction。具体实现细节未公开,但根据专利和性能分析推断:
处理器使用一个类似Store Set的两级查找结构。
预测器区分"安全推测"和"需要等待"两种情况。
从Skylake(2015年)开始,预测精度进一步提高,能够在大多数情况下准确预测Load是否需要等待特定Store。
违规恢复采用部分冲刷(pipeline nuking不总是从违规Load开始,而是可以只重放违规Load及其依赖指令),减少恢复代价。
Store Buffer
Store Buffer(SB)是连接乱序执行引擎与存储器子系统的关键缓冲结构。在乱序处理器中,Store指令在执行阶段将数据写入Store Buffer而非直接写入Cache,只有当Store指令提交(commit/retire)后,Store Buffer中的数据才会被写入L1 Data Cache。这种设计有两个根本原因:
支持精确异常:如果Store直接在执行时写入Cache,那么在分支预测失败或异常发生时,已经写入Cache的数据无法撤销(Cache没有"回滚"机制)。Store Buffer充当了一个缓冲区,只有当指令确认不会被撤销后才真正修改Cache。
支持Store-to-Load转发:当一条Load需要读取一条尚未提交的Store的数据时,Store Buffer可以直接将数据转发给Load,而不需要等待Store提交并写入Cache后再读取。
Store Buffer的条目结构与位宽分析
Store Buffer通常实现为一个循环队列(circular queue),每项包含以下字段:
| 字段 | 位宽 | 说明 |
|---|---|---|
| Valid | 1位 | 该项是否有效(已分配) |
| Address Valid | 1位 | 地址是否已计算完成 |
| Data Valid | 1位 | 数据是否已就绪 |
| Committed | 1位 | Store是否已提交(可以写入Cache) |
| Physical Address | 4852位 | Store的物理地址(由TLB翻译得到) |
| Virtual Address | 4857位 | Store的虚拟地址(用于快速STLF匹配) |
| Data | 64512位 | Store的数据(取决于最大Store宽度) |
| Size | 24位 | 访存大小(1/2/4/8/16/32字节) |
| Byte Enable | 864位 | 字节使能掩码(每个可写字节一位) |
| ROB Index | 79位 | 对应的ROB项编号(256512项ROB) |
| Memory Type | 23位 | 内存类型(WB/WT/UC/WC等) |
Store Buffer每项的字段与详细位宽分析
逐字段分析各字段的设计考量:
Physical Address(4852位):物理地址是STLF地址匹配的最终判断依据。在当前的x86-64架构中,物理地址宽度为4652位(取决于处理器的物理地址位数,如Intel的5级页表支持52位物理地址)。RISC-V的Sv48页表支持44位物理地址。在STLF匹配中,通常不需要比较最低3位(8字节对齐的块内偏移),因此实际比较位宽为位或位。
Data(64512位):数据字段的宽度取决于处理器支持的最大Store宽度。标量处理器通常支持最大8字节(64位)Store;支持SIMD的处理器需要更宽的数据字段——AVX-512的VMOVDQU64指令可以Store 64字节,此时数据字段需要512位。但大多数设计将超宽Store拆分为多个Store Buffer条目,每个条目仍然只存储816字节。
Byte Enable(864位):字节使能掩码标记数据字段中哪些字节是有效的。例如,一条sw(写4字节)到地址0x1004的Store,在一个8字节宽的Store Buffer条目中,字节使能为0xF0(高4字节有效)。字节使能掩码是实现部分转发的关键——它告诉STLF逻辑哪些字节可以从Store Buffer转发,哪些字节需要从Cache读取。
每个Store Buffer条目的总位宽约为位(以典型的128位数据宽度、16字节使能计算)。对于一个64项的Store Buffer,总存储量约为位2.1 KB。这还不包括CAM比较逻辑的面积——CAM部分(用于STLF匹配)的面积通常与存储部分相当甚至更大。
Store Buffer的典型大小为32128项。随着处理器乱序窗口的不断增大(ROB从200项增长到500+项),Store Buffer的大小也在持续增长——更大的ROB意味着更多的飞行中Store指令需要被缓冲。表表 36.6列出了一些实际处理器的Store Buffer规模,可以清楚地看到这一增长趋势。
| 处理器 | Store Buffer项数 | 年份 |
|---|---|---|
| Alpha 21264 | 32 | 1998 |
| Intel Pentium 4 (Netburst) | 24 | 2000 |
| Intel Core 2 (Merom) | 20 | 2006 |
| Intel Sandy Bridge | 36 | 2011 |
| Intel Skylake | 56 | 2015 |
| Intel Golden Cove | 72 | 2021 |
| AMD Zen 3 | 64 | 2020 |
| AMD Zen 4 | 64 | 2022 |
| Apple M1 (Firestorm) | 108 | 2020 |
实际处理器的Store Buffer规模
Store Buffer在微架构中的位置
Store Buffer在微架构中的位置如图图 36.6所示:
Store指令的执行分为两个阶段:
地址计算阶段:AGU计算出Store的有效地址,写入Store Buffer的地址字段,设置"Address Valid"标志。
数据写入阶段:当Store的源操作数(要写入的数据)就绪后,数据被写入Store Buffer的数据字段,设置"Data Valid"标志。注意,地址和数据的写入可以乱序发生——数据可能先于地址就绪。
当Store指令被ROB提交(retire)后,Store Buffer项的"Committed"标志被设置,该Store的数据可以被写入L1 Cache。写入Cache的过程称为Store Buffer drain(排空),通常按先进先出(FIFO)顺序进行——最老的已提交Store最先写入Cache。
Store指令的生命周期
一条Store指令从分发到最终写入Cache的完整生命周期涉及多个流水线阶段,每个阶段都与Store Buffer的不同字段交互:
分配(Dispatch阶段):Store被分配一个Store Buffer条目。设置Valid = 1,Address Valid = 0,Data Valid = 0,Committed = 0。写入ROB Index。此时SB条目只有控制信息,无地址和数据。
地址计算(Execute阶段,AGU流水线):基址寄存器就绪后,AGU计算有效地址。物理地址通过TLB翻译获得。设置Address Valid = 1,写入Physical Address和Virtual Address字段。此时触发违规检测——将Store地址广播到Load Queue搜索冲突。
数据写入(Execute阶段,数据流水线):Store的源数据操作数就绪后,数据被写入Store Buffer的Data字段。设置Data Valid = 1。注意,步骤2和步骤3可以以任意顺序发生——数据可能先于地址就绪(例如Store的基址寄存器来自前面的长延迟Load,而数据是一个立即数)。
提交(Retire阶段):ROB按程序序提交指令。当Store到达ROB头部且没有更老的异常或分支预测失败时,设置Committed = 1。此时Store不可能再被撤销。
排空(Post-Retire阶段):已提交且Address Valid和Data Valid都为1的Store Buffer条目,按FIFO顺序出队,将数据写入L1 Data Cache。写入完成后释放Store Buffer条目(设置Valid = 0)。
步骤4和步骤5之间存在一个解耦——提交是一个瞬时操作(设置标志位),而排空需要等待L1 Cache的写端口可用。这种解耦允许提交不被Cache写入的延迟阻塞——ROB可以继续提交后续指令,即使之前的Store尚未排空。这对性能至关重要:如果提交必须等待Store排空到Cache,则Store密集的代码段会严重阻塞ROB提交,导致ROB填满和流水线停顿。
Store的地址与数据解耦
现代处理器将Store的地址计算和数据写入解耦为两个独立的操作,它们可以在不同的时间完成。这种解耦设计有重要的性能意义:
早期地址计算的优势:即使Store的数据尚未就绪,只要地址先计算完成,就可以立即用于违规检测和STLF匹配。这使得后续Load可以更早地知道是否与该Store冲突,减少了不必要的推测执行和违规。
数据延迟不影响消歧:如果Store的数据来自一条长延迟的Load(Cache未命中),数据可能需要几十到几百个周期才能就绪。但只要地址先就绪,消歧过程可以正常进行。
Split-Store微架构:一些处理器(如Intel Core系列)将Store指令在微操作层面拆分为两条独立的微操作——Store Address(STA,计算地址并写入SQ)和Store Data(STD,写入数据到SQ)。STA和STD可以在不同的执行端口独立执行,甚至可以乱序完成。这种拆分使得地址和数据的解耦更加彻底。
Split-Store微架构的优势在于:STA微操作可以在AGU端口执行(与Load共享),而STD微操作可以在专用的Store Data端口执行(不占用AGU)。这增加了Store指令的执行并行度。例如,在Intel Skylake中,有3个AGU端口(可执行STA和Load)和1个STD端口,每周期最多可以执行3个地址计算和1个数据写入。
但Split-Store也增加了调度复杂度——STA和STD是同一条Store指令的两个部分,它们需要被分配到同一个SQ条目。如果STA先完成,SQ条目的Address Valid被设置但Data Valid仍为0;反之亦然。只有当两者都完成后,Store才算完全执行完毕。调度器需要独立地跟踪STA和STD的操作数就绪状态,分别在条件满足时发射它们。
Store Buffer的排空策略
Store Buffer的排空策略影响着Cache写端口的利用效率和处理器的Store吞吐量。常见的排空策略包括:
严格FIFO排空:总是排空最老的已提交Store。简单可靠,但如果最老的Store遇到Cache未命中(需要从L2取回Cache行),整个排空流水线停顿。
非阻塞排空:当最老的Store遇到Cache未命中时,跳过它,先排空后面的Cache命中Store。这需要Store Buffer支持非头部出队,增加了控制逻辑复杂度。但它可以显著提高排空吞吐量——在Cache未命中率为5%的工作负载中,非阻塞排空可以将有效排空速率提高约10%15%。
优先级排空:当Store Buffer接近满时(例如使用率超过75%),提高排空优先级——增加分配给排空的Cache写端口时间片,甚至暂时牺牲Load的Cache读取带宽来为排空腾出端口。
Store Buffer的端口设计
Store Buffer的写端口竞争是一个需要仔细考虑的问题。Store Buffer至少需要以下几个端口:
分配端口(Allocate Port):在指令分发阶段为新的Store分配Store Buffer项,写入ROB索引等控制信息。如果处理器支持每周期分发条指令,则需要至少个分配端口(在最坏情况下,所有分发的指令都是Store)。实际上,由于每周期的Store指令数很少超过23条,分配端口通常为24个。
地址写入端口(Address Write Port):AGU计算完成后将地址写入对应的Store Buffer项。端口数与AGU的数量匹配,通常为12个。
数据写入端口(Data Write Port):当Store的源数据就绪后,将数据写入Store Buffer项。端口数通常为12个。
CAM读端口(CAM Read Port):Load执行时进行Store-to-Load转发查找。端口数与Load执行单元数匹配,通常为12个。
排空端口(Drain Port):将已提交的Store数据写入L1 Cache。通常为1个。
多端口的Store Buffer在面积和功耗上代价显著。对于一个SRAM结构,每增加一个端口,面积约增加40%60%(需要额外的字线和位线)。一个64项、4个写端口、2个CAM读端口的Store Buffer,其面积可能达到(7nm工艺),功耗约 mW(在 GHz频率下)。
设计提示
Store Buffer的排空速率(drain rate)是一个重要的性能参数。如果排空速率低于Store指令的生成速率,Store Buffer最终会被填满,导致流水线后端阻塞。典型设计的排空速率为每周期12个Store,与L1 Cache的写端口数匹配。当Store Buffer接近满时,处理器可以暂停分发新的Store指令,或者优先分配Cache写端口给Store Buffer排空。
一个关键的设计约束是:排空一个Store到L1 Cache需要(1)Cache的写端口空闲,(2)Cache行在L1中存在(写命中)或已经被分配(写未命中需要先读取Cache行)。如果排空的Store遇到L1 Cache未命中,则排空操作需要等待Cache行从L2填入,可能阻塞整个排空流水线。为缓解此问题,一些设计允许Store Buffer乱序排空——跳过Cache未命中的Store,先排空后续Cache命中的Store。
Store-to-Load转发
Store-to-Load转发(Store-to-Load Forwarding, STLF)是Store Buffer最重要的功能之一。当一条Load要访问的地址与Store Buffer中某条未提交的Store的地址重叠时,Store Buffer可以直接将Store的数据转发给Load,而不需要等到Store提交并写入Cache。
CAM匹配逻辑的硬件实现
STLF的实现基于Store Buffer的CAM(Content-Addressable Memory,内容可寻址存储器)结构。CAM的每个条目包含一个比较器,将存储的地址与搜索键(Load的地址)进行比较。当Load的地址被送到Store Buffer时,Store Buffer对所有有效项的地址字段进行并行比较(CAM查找),识别出地址匹配的Store项。
每个Store Buffer条目中的CAM比较逻辑由以下部分组成:
地址比较器:比较物理地址的PA[47:3](忽略8字节块内的字节偏移),共45位。每位使用一个异或非(XNOR)门产生匹配信号,然后通过一个45输入的与门将所有位的匹配信号合并。45位异或非门需要个晶体管,与门树需要约44个晶体管。每个条目的比较器总共约134个晶体管。
有效性检查:比较结果还需要与该条目的Valid和Address Valid标志进行与操作,确保只有有效且地址已就绪的条目才能产生匹配信号。这需要额外2个与门。
年龄过滤:匹配信号还需要与年龄比较结果进行与操作,确保只有比Load更老的Store才能参与转发。年龄比较逻辑需要一个减法器和比较器(详见36.3.5 节)。
对于一个64项的Store Buffer,CAM匹配逻辑的总门数约为门。如果支持2个并行的Load(即2个CAM搜索端口),门数翻倍至18000门。
优先级编码——选择最年轻的匹配Store
当多个Store Buffer条目的地址都与Load匹配时(同一地址被多次Store),需要选择其中最年轻的先序Store(即在Load之前、但尽可能接近Load的Store)。这是因为最近的Store包含该地址的最新值。
优先级选择电路将CAM匹配结果(一个位的匹配向量,为Store Buffer项数)转换为被选中条目的索引。实现方式有两种:
优先级编码器(Priority Encoder):将匹配向量按年龄顺序(从年轻到老)编码,输出最年轻的匹配项索引。对于64项Store Buffer,这是一个64-to-6优先级编码器,延迟约为级门,约0.51ns@3GHz。
年龄矩阵(Age Matrix):使用一个的位矩阵记录每对Store Buffer条目之间的年龄关系,然后对每个匹配项检查是否是所有匹配项中最年轻的。年龄矩阵的面积为,对于64项Store Buffer需要位,面积开销较大但查找延迟可能更短。
在循环队列的Store Buffer中,一种更高效的方式是利用队列的天然顺序:匹配向量中,从Load对应的SQ Tail Snapshot位置向前(向队列头方向)搜索第一个匹配位,即为最年轻的先序Store。这可以通过一个前缀优先级编码器实现,其电路结构与循环队列的指针逻辑紧密集成。
STLF的数据选通逻辑
当优先级编码器选中了一个Store Buffer条目后,需要从该条目中读出数据。但读出的数据可能需要进一步处理才能作为Load的结果:
数据有效性检查:如果选中条目的Data Valid = 0(地址已匹配但数据尚未就绪),Load不能获得转发数据。此时Load被标记为"等待Store数据"状态——当对应Store的数据最终写入Store Buffer时,Load会被唤醒并重新执行STLF读取。这种"STLF on data ready"的机制需要在Store Buffer的数据写入端口上添加唤醒逻辑。
数据宽度匹配:如果Store是4字节(
sw),Load是4字节(lw),且地址完全匹配,则直接转发Store的4字节数据。但如果Store是8字节(sd),Load是4字节(lw),则需要根据Load地址的低3位从Store的8字节中选取正确的4字节。这需要一个8字节到4字节的多路选择器,选择信号为。符号扩展:如果Load是
lb(读1字节并符号扩展到64位)或lh(读2字节并符号扩展),转发的数据还需要经过符号扩展逻辑。符号扩展逻辑是Load数据路径的标准部分,与STLF共用即可。
STLF失败的处理
STLF可能在以下情况下"失败"——即Load的地址与Store Buffer中某个条目匹配,但无法完成转发:
Store数据未就绪:匹配的Store条目地址有效但数据无效。Load必须等待。
部分覆盖且不支持合并转发:匹配的Store条目只覆盖Load所需字节的一部分,且硬件不支持从Store Buffer和Cache合并数据。Load必须等待Store提交排空到Cache后重新执行。
跨Cache行转发:Store和Load的地址范围跨越了Cache行边界,转发逻辑无法处理。Load等待Store排空。
多Store覆盖:Load需要的字节来自多个不同的Store Buffer条目。大多数处理器不支持多Store合并转发。
STLF失败的处理方式通常是让Load进入回放(replay)状态——Load被暂时挂起,等待条件满足后重新尝试。对于数据未就绪的情况,当Store的数据写入Store Buffer后,Store Buffer通过"唤醒"信号通知发射队列,Load被重新调度执行。对于其他失败情况,Load通常需要等待相关Store提交并排空到Cache后,从Cache重新读取数据。
STLF失败的性能代价因情况而异。数据未就绪的情况通常只增加几个周期的延迟(等待Store的数据源操作数就绪)。而需要等待Store排空到Cache的情况,延迟可能高达1030个周期——这是Store从执行到提交再到排空的典型延迟。
STLF的时序路径
STLF的时序路径是Store Buffer设计中最关键的部分。从Load的地址到达Store Buffer开始,经过以下几个阶段:
CAM比较(约1个周期):将Load地址的低位与Store Buffer中所有有效项的地址进行并行比较。比较器通常使用异或门阵列加或归约结构,位宽为1220位。
年龄过滤(与CAM并行):根据Load的年龄标记和每个Store Buffer项的ROB索引,过滤出比Load更老的Store。
优先级编码(约0.51个周期):在所有地址匹配且年龄合法的Store中,选择最年轻的一个。
数据读出(约0.51个周期):从选中的Store Buffer项中读出数据字段。
数据对齐(约0.5个周期):将Store的数据按Load的地址和大小进行字节对齐和选择。
整个过程必须在一定的时钟周期数内完成。典型的STLF延迟为35个时钟周期——这比直接从L1 Cache读取(通常45周期)要快,或至少不慢。
在实际设计中,STLF的查找通常与L1 Cache的读取并行进行。当Load的地址被送到AGU后,地址同时被发送到Store Buffer的CAM和L1 Cache的Tag数组。在几个周期后,Store Buffer和Cache分别返回结果。如果Store Buffer命中,使用Store Buffer的数据;否则使用Cache的数据。这种并行查找消除了"先查Store Buffer、再查Cache"的串行延迟。
但并行查找带来了一个设计取舍:如果Store Buffer命中但Cache也命中,硬件需要一个多路选择器在两个数据源之间做选择。这个选择器位于Load数据的关键路径上,增加了约半个门延迟。对于追求极限频率的设计,这半个门延迟可能迫使Load的总延迟增加一个完整的流水级。
性能分析 4 — Store-to-Load转发的频率与影响
在SPEC CPU 2017基准测试中,Store-to-Load转发的频率因工作负载而异:
整数基准(如
gcc、mcf):约5%10%的Load通过STLF获取数据。浮点基准(如
lbm、wrf):约2%5%的Load通过STLF获取数据。栈操作密集型程序:由于函数调用/返回频繁地在同一栈帧地址上Store后Load,STLF比例可高达20%30%。
如果没有STLF,这些Load必须等待Store提交并写入Cache后才能执行。Store从执行到提交的延迟通常为1030个周期(取决于ROB的排空进度),这意味着STLF可以节省大量的等待时间,对IPC的贡献显著。
STLF的延迟本身也是一个重要的性能因素。在Intel的处理器上,成功的STLF延迟通常为45个周期(与L1 Cache命中延迟相当或略快)。但在AMD Zen系列上,STLF延迟可能为34个周期——这比L1 Cache命中更快,因为Store Buffer的容量小于Cache,查找更快。STLF的延迟差异在依赖链密集的代码(如编译器、解释器中的栈操作密集代码)中会显著影响性能。
STLF与Cache并行查找的时序分析
在实际设计中,Load的地址同时被发送到Store Buffer和L1 Cache进行并行查找。下面详细分析这个并行路径的时序:
周期1(AGU):Load的基址寄存器和偏移量被送入地址生成单元。AGU输出有效虚拟地址。同时,的低12位(页内偏移)被发送到Store Buffer的CAM比较器阵列,以及L1 Cache的Tag数组和Data数组(L1 Cache通常使用VIPT方式,用虚拟地址索引、物理地址标签)。
周期2(CAM匹配 + Cache Tag比较 + TLB查找):三个操作并行进行——(1) Store Buffer的CAM将低12位与所有条目的地址低12位比较,产生初步匹配向量。(2) L1 Cache的Tag数组用索引对应的Set,读出该Set中所有Way的Tag。(3) TLB查找对应的物理页号。
周期3(优先级选择 + Tag匹配 + PA确认):(1) Store Buffer的优先级编码器从匹配向量中选出最年轻的先序Store,读出其数据。同时用TLB输出的对初步匹配进行精确确认——排除低12位匹配但物理页不同的假阳性。(2) L1 Cache用与Tag数组的输出进行标签比较,确定是否Cache命中以及命中的Way。
周期4(数据选择 + 对齐):(1) 如果Store Buffer精确匹配且Data Valid,选择Store Buffer的数据作为Load结果。(2) 如果Store Buffer未匹配但Cache命中,选择Cache的数据。(3) 如果都未命中,标记Load为Cache Miss,进入L2访问流程。数据经过对齐和符号扩展后写入目的物理寄存器。
因此,STLF的总延迟为4个周期(从AGU输入到数据写入寄存器),与L1 Cache的命中延迟相同。关键路径上的两个竞争路径是:
Store Buffer路径:AGU CAM比较(12位XNOR + AND归约) 年龄过滤 优先级编码 数据MUX 对齐。
Cache路径:AGU SRAM索引 Tag比较 + Way选择 Data MUX 对齐。
两条路径的延迟需要平衡——如果Store Buffer路径明显慢于Cache路径,则STLF会增加Load的总延迟,即使大多数Load从Cache获取数据。设计者通常需要仔细优化Store Buffer的CAM比较逻辑的时序,使其不超过Cache路径的延迟。
硬件描述 3 — STLF路径的门延迟分析
以一个64项Store Buffer、12位CAM比较的设计为例,分析Store Buffer路径的关键门延迟:
CAM比较:12个XNOR门(1级)+ 12输入AND归约(级AND门)= 5级门延迟。假设每级门延迟约25 ps(7nm工艺),则CAM比较约125 ps。
有效性与年龄合并:将CAM比较结果与Valid、Address Valid、年龄过滤结果进行AND——3个AND门 = 1级门延迟25 ps。
优先级编码:64位匹配向量的优先级编码 = 级门延迟150 ps。
数据MUX:用编码索引从64个条目中选择数据 = 64:1 MUX = 级2:1 MUX150 ps。
Source选择:在Store Buffer数据和Cache数据之间选择 = 1级MUX25 ps。
总延迟约125 + 25 + 150 + 150 + 25 = 475 ps0.5 ns。在3 GHz频率下(周期333 ps),这需要约1.5个周期——与L1 Cache SRAM的访问延迟(通常23个周期)处于同一量级。通过仔细的电路优化(如使用动态逻辑的CAM、分段优先级编码器),可以将Store Buffer路径压缩到1个周期内完成。
部分转发的处理
当Store和Load的地址完全匹配且大小相同时(例如Store写8字节、Load读8字节到同一地址),STLF是直接的。但在许多情况下,Store和Load的大小或地址对齐方式不同,导致部分转发(Partial Forwarding)的问题。
部分转发的场景分类
Store窄、Load宽:例如
sw(写4字节)后跟ld(读8字节),Load需要的8字节中,低4字节来自Store Buffer,高4字节来自Cache。硬件需要合并来自两个源的数据。Store宽、Load窄:例如
sd(写8字节)后跟lw(读4字节),Load只需要Store数据的一部分。硬件需要从Store的8字节数据中提取正确的4字节。地址不对齐:Store写地址0x1002开始的4字节,Load读地址0x1000开始的4字节。两者有2字节重叠、各有2字节不重叠。
多Store合并:Load需要的数据来自多个不同的Store,每个Store覆盖Load地址范围的一部分。
具体场景分析
下面通过三个具体场景详细分析部分转发的复杂性:
场景1:Store SW 0x1000, 4字节 Load LD 0x1000, 8字节。
Store写入地址0x1000的4字节(字节[3:0]),Load读取地址0x1000的8字节(字节[7:0])。Store的字节使能掩码为0x0F(低4字节有效),Load的字节使能掩码为0xFF(全8字节有效)。
转发逻辑需要:
字节[3:0]:从Store Buffer读取(Store的数据低4字节)。
字节[7:4]:从L1 Cache读取地址0x1004的4字节。
合并为8字节结果:{Cache[7:4], SB[3:0]}。
这需要一个8字节宽的2:1多路选择器,每个字节独立选择数据来源(Store Buffer或Cache)。选择控制信号由Store的字节使能掩码产生:使能为1的字节来自Store Buffer,使能为0的字节来自Cache。
场景2:Store SW 0x1002, 4字节 Load LD 0x1000, 8字节。
这个场景更为复杂。Store写入地址0x1002开始的4字节(覆盖8字节块内的字节[5:2]),Load读取地址0x1000的8字节。Store的字节使能掩码为0x3C(字节[5:2]有效)。
转发逻辑需要:
字节[1:0]:从L1 Cache读取。
字节[5:2]:从Store Buffer读取。但Store Buffer中数据的存储格式可能是:Store的4字节数据存储在数据字段的低4字节位置(自然对齐),需要右移2字节才能对齐到Load期望的位置。
字节[7:6]:从L1 Cache读取。
合并为8字节结果:{Cache[7:6], SB_shifted[5:2], Cache[1:0]}。
这需要一个桶形移位器(barrel shifter)来对齐Store Buffer的数据,以及一个8字节宽的多路选择器来合并数据。桶形移位器需要按字节粒度移位07个位置,硬件开销为个2:1 MUX(级,每级64位宽),约400个晶体管。
场景3:两个Store覆盖Load的不同部分。
sw a0, 0(s0) # Store1: M[s0+0] = a0 (4字节)
sw a1, 4(s0) # Store2: M[s0+4] = a1 (4字节)
ld a2, 0(s0) # Load: a2 = M[s0+0] (8字节)Load读取的8字节中,低4字节应来自Store1,高4字节应来自Store2。这是多Store合并转发——Load的数据需要从Store Buffer中的两个不同条目收集并合并。
多Store合并转发在硬件中极为复杂。对于一个8字节的Load,需要为每个字节独立地在所有匹配的Store Buffer条目中选择数据来源。设Store Buffer有项,每个字节需要一个选1的多路选择器(个Store Buffer条目 + 1个Cache),共8个这样的选择器。对于,每个选择器需要级MUX,每级约64位宽的逻辑,总门数约门——面积和延迟都不可忽视。
为什么多Store合并转发如此困难?关键在于字节级优先级选择的复杂度。对于每个Load的输出字节(),需要找到Store Buffer中所有覆盖该字节的Store,然后选择其中最年轻的一个作为该字节的数据来源。如果没有任何Store覆盖字节,则该字节来自Cache。
形式化地,对于每个字节:
这需要为每个字节位置维护一套独立的"最年轻匹配"优先级编码器。8个字节 64项Store Buffer = 8套独立的64位优先级编码器。每套编码器的输入是64位的"该字节位置被覆盖"向量(),输出是被选中的Store Buffer条目索引。
总硬件开销:8个64位优先级编码器(200门1600门)+ 8个64:1数据MUX(400门3200门)+ 8个2:1最终选择MUX(SB vs Cache,100门)5000门。加上控制逻辑,总计约60008000门。
这个开销是单Store完全覆盖转发(1500门)的45倍。而且关键路径延迟也更长——字节级优先级编码器的输入依赖于字节使能匹配的结果,增加了一级逻辑延迟。在时序已经非常紧张的STLF路径上,这额外的延迟可能迫使设计者增加一个流水线级,将Load的总延迟从4周期增加到5周期——对所有Load都产生影响,而多Store合并转发的实际发生频率极低(0.1%的Load)。
这就解释了为什么大多数商业处理器选择不支持多Store合并转发——为极少数情况付出所有Load延迟增加的代价是不划算的。
部分转发的硬件实现
处理部分转发的硬件逻辑包括:
字节使能匹配:对于Store Buffer中每个匹配的Store,计算其字节使能掩码(Byte Enable Mask)与Load的字节使能掩码的交集,确定哪些字节可以从Store Buffer转发。例如,一个写入地址0x1000的
sw(4字节)的字节使能掩码为0000_1111(低4字节有效),一个读取地址0x1000的ld(8字节)的字节使能掩码为1111_1111。两者的交集为0000_1111,表示低4字节可以从Store Buffer转发,高4字节需要从Cache读取。完全覆盖检查:硬件需要判断Store Buffer中的匹配Store是否完全覆盖了Load请求的所有字节。判断条件为:,即Load的每个有效字节在Store中都是有效的。如果完全覆盖,Load的数据完全来自Store Buffer,不需要访问Cache;如果不完全覆盖,Load需要同时访问Cache和Store Buffer并合并数据。
数据对齐与移位:Store的数据在Store Buffer中的存储位置由Store的地址决定。如果Store Buffer按8字节对齐存储(即Store的低3位地址决定数据在数据字段中的字节位置),那么一个
sw写到地址0x1002会将4字节数据存储在数据字段的字节[5:2]位置。当Load从0x1000读取8字节时,Store Buffer的字节[5:2]直接对应Load结果的字节[5:2],无需移位。这种地址对齐存储方式简化了转发逻辑,但增加了Store Buffer数据字段的写入复杂度。字节级多路选择:最终的数据合并需要一个字节级的多路选择器阵列。对于一个8字节的Load结果,每个字节独立选择数据来源。选择控制信号来自字节使能匹配的结果。
设计权衡 2 — 部分转发的硬件复杂度与设计选择
由于部分转发的硬件复杂度极高,许多处理器选择不支持某些形式的部分转发:
Intel Core系列:支持Store完全覆盖Load的转发(Store Load且地址对齐),以及Store完全包含Load字节范围的转发。不支持需要合并多个Store的转发。当出现不支持的部分转发情况时,Load必须等待相关Store提交并写入Cache后再执行,产生约1020周期的额外延迟。Intel的文档将此情况称为"Store Forwarding Stall"。
AMD Zen系列:支持的转发场景与Intel类似,但在某些跨越自然对齐边界的情况下会失败。例如,一个4字节Store到地址0x1001后跟一个4字节Load从地址0x1001——虽然是完全覆盖,但由于地址不是4字节对齐的,Zen 1/Zen 2会产生转发失败。Zen 3及之后改进了此限制。
ARM Cortex-A77及后续:支持较为灵活的部分转发,包括Store窄Load宽的情况(需要合并Cache数据),但多Store合并仍不支持。
不支持的部分转发虽然不影响功能正确性(Load最终会从Cache获取正确数据),但会导致性能惩罚。编译器和程序员应该尽量避免同一地址的不同宽度访问模式。这也是为什么许多编译器优化pass会将小宽度的Store/Load合并为宽访问,或保证访问的地址对齐。
Store Forwarding Stall的详细机制
当STLF无法完成转发时,产生Store Forwarding Stall——Load不能立即获得数据,必须等待相关Store的数据通过其他路径变得可用。等待的具体过程如下:
Load的STLF查找发现地址匹配的Store,但检测到转发条件不满足(部分覆盖、地址不对齐等)。
Load被标记为"Store Forwarding Blocked"状态,不从Store Buffer或Cache读取数据。
Load进入等待模式——等待匹配的Store提交并排空到L1 Cache。
当Store排空到Cache后(Store Buffer Drain),Store的数据已经写入L1 Cache。
Load被唤醒,重新执行——这次直接从L1 Cache读取完整的数据(因为Store的数据已经在Cache中)。
从Store执行完成到排空到Cache的延迟包含以下部分:
Store在ROB中等待提交:取决于ROB中比Store更老的指令的提交进度,可能0100+周期。
Store从提交到排空的延迟:取决于Store Buffer排空队列的长度和Cache写端口的可用性,通常15周期。
Load重新执行的延迟:重新进入发射队列、发射、AGU、Cache访问,约45周期。
在最好情况下(Store已经提交且即将排空),Store Forwarding Stall的额外延迟约510周期。在最坏情况下(Store在ROB中部,需要等待大量更老的指令先提交),延迟可高达50100周期。
STLF成功的条件总结
总结各主要处理器上STLF成功的条件,供处理器设计者和性能优化工程师参考:
| 条件 | 说明 |
|---|---|
| Store完全覆盖Load | Store的字节范围完全包含Load的字节范围。这是最基本的成功条件。 |
| Store地址自然对齐 | Store的起始地址是其大小的整数倍(如4字节Store的地址是4的倍数)。非对齐Store可能导致转发失败。 |
| 不跨越Cache行边界 | Store和Load都不跨越64字节的Cache行边界。 |
| Load不跨越自然对齐边界 | Load的地址范围不跨越其大小的自然对齐边界。 |
| Store数据已就绪 | Store Buffer中匹配条目的Data Valid = 1。否则Load需要等待。 |
| 单Store转发 | Load的数据完全来自一个Store Buffer条目(不需要合并多个Store)。 |
各处理器STLF成功的条件
违反以上任何条件的STLF尝试,在大多数处理器上会导致Store Forwarding Stall。性能敏感的代码应确保Store-Load对满足这些条件——这通常可以通过保证数据结构的自然对齐和使用一致的访问宽度来实现。
硬件描述 4 — 编译器优化与Store Forwarding
编译器可以通过以下优化来减少Store Forwarding Stall:
访问宽度一致化:如果同一变量的Store和Load使用不同的宽度(例如字节写入后字读取),编译器可以统一为相同的宽度。
对齐保证:使用
__attribute__((aligned(N)))或类似的对齐指示,确保频繁访问的数据结构按自然边界对齐。Store-Load消除:如果编译器能够证明一条Load总是读取一条Store刚写入的值(且中间无其他写入),可以直接用Store的源操作数替代Load的结果,完全消除STLF查找。
Store合并:将多个小宽度Store合并为一个宽Store,使后续Load可以完全覆盖转发。
跨Cache行的转发
当一个Store或Load的访存地址范围跨越了Cache行边界(Cache Line Boundary)时,问题变得更加复杂。Cache行是Cache操作的最小粒度,典型大小为64字节。一个8字节的ld如果起始地址为0x103C,则访问的地址范围为0x103C0x1043,跨越了0x103F/0x1040的Cache行边界。
跨Cache行的访存通常被拆分为两个微操作(micro-ops):
第一个微操作访问低地址部分(同一Cache行内的字节)。
第二个微操作访问高地址部分(下一Cache行的字节)。
两个微操作的结果在Load单元的输出端合并。
跨Cache行的Store-to-Load转发更为棘手:Store可能只跨越了一个Cache行边界(拆为两个微操作),而Load可能跨越了不同的Cache行边界(也拆为两个微操作),两者的拆分方式可能不同。在这种情况下,转发逻辑需要处理Store的两个部分与Load的两个部分之间的各种组合,硬件复杂度急剧增加。
多数处理器对跨Cache行的转发采取保守策略:当检测到跨Cache行的Store-to-Load转发需求时,直接放弃转发,让Load等待Store提交后从Cache读取。这种保守策略在性能上的代价通常很小,因为跨Cache行的访存在经过良好优化的程序中非常少见(编译器和程序员通常保证数据结构的自然对齐)。
跨Cache行的Load还涉及原子性(atomicity)问题。在多核处理器中,如果一条Load读取的数据跨越两个Cache行,则两次读取之间可能被其他核的写入打断,导致Load读到"半新半旧"的数据。对于需要原子性的操作(如C11的atomic_load),跨Cache行访问通常需要额外的同步机制(如锁总线或使用特殊的Cache协议操作),代价极高。因此,原子操作的数据通常被要求自然对齐,不跨越Cache行边界。
硬件描述 5 — 跨Cache行Load的微操作拆分
以x86为例,一个跨Cache行的MOV指令在微操作层面被拆分为以下步骤:
地址生成单元检测到地址加上访存大小跨越了64字节的Cache行边界(即)。
生成两个微操作:访问地址到Cache行末尾的字节,访问下一Cache行起始到的字节。
和分别访问L1 Cache(可能在连续两个周期完成,也可能因资源冲突而更慢)。
一个合并缓冲(merge buffer)将两个部分的数据拼接成完整的Load结果。
合并后的数据写入目的寄存器。
跨Cache行Load的总延迟通常为正常Load延迟的2倍以上,在Intel的处理器上表现为约1112个周期(相比正常的45周期)。
设计提示
跨Cache行的Load在性能上代价显著:不仅需要两次Cache访问(延迟加倍),还可能导致两次TLB查找、两次Cache Bank冲突检查。一些处理器(如Intel Sandy Bridge及之后的设计)对跨Cache行Load进行硬件优化——当跨越的两个Cache行都在L1 Cache中命中时,两次访问可以在背靠背的两个周期内完成;但如果任何一个Cache行未命中,则额外延迟更加显著。因此,编译器应尽量保证数据的自然对齐,避免跨Cache行访问。RISC-V的RV64I基本指令集要求Load/Store的地址按自然对齐(lw要求4字节对齐,ld要求8字节对齐),非对齐访问会触发异常。但Zam扩展允许硬件支持非对齐访问,此时跨Cache行的处理就变得必要。
Store合并(Store Coalescing)
Store合并(Store Coalescing / Store Combining)是一种优化技术,将Store Buffer中多个写入相同或相邻地址的Store合并为一个写入操作,减少对L1 Cache写端口的占用和对存储器层次结构的写入压力。Store合并在以下场景中特别重要:
Cache写端口有限:L1 Data Cache通常只有12个写端口,而Store Buffer的排空速率不能超过写端口数。Store合并将多次写入合并为一次,有效提高了每个写端口周期传递的数据量。
减少写分配开销:在写分配策略下,写未命中需要先从L2读取整个Cache行,然后修改对应字节。如果多个Store写入同一Cache行,合并后只需要一次写分配操作,节省了多次L2读取。
降低总线带宽消耗:对于写直达Cache或WC内存类型,每个未合并的Store都会产生一次总线写事务。合并可以将事务数减少数十倍。
Store合并有两个层次:
Store Buffer内的合并
当Store Buffer中有多个已提交的Store写入同一Cache行时,可以将它们合并为一次写入操作。例如,四个连续的sb(写1字节)指令写入同一个4字节对齐的地址范围,可以合并为一个4字节的写入。这种合并减少了Store Buffer排空时占用Cache写端口的次数。
合并的实现通常是在Store Buffer的排空逻辑中加入一个合并检测器——当头部的Store准备排空时,检查紧跟其后的Store是否写入同一Cache行。如果是,则将两者的数据合并到同一个写事务中:使用字节使能掩码将两个Store的有效字节组合到一个完整的写事务中。这个检测过程可以链式地检查多个连续Store,直到遇到写入不同Cache行的Store为止。
合并的硬件实现需要一个合并缓冲(coalescing buffer),大小为一个Cache行(64字节),配备一个64位的字节使能掩码。排空逻辑将连续的同Cache行Store的数据写入合并缓冲的相应字节位置,更新字节使能掩码。当遇到不同Cache行的Store时,将合并缓冲的内容作为一个写事务发送到L1 Cache,然后开始新一轮合并。
合并的收益在以下场景中特别显著:
结构体初始化:将结构体的多个字段逐一赋值时,如果字段位于同一Cache行内,多次Store可以合并。
循环展开后的Store:编译器循环展开后,连续的Store指令往往写入相邻地址。
字符串操作:
memset、strcpy等函数逐字节或逐字写入连续地址。
写合并缓冲(Write Combining Buffer, WCB)
对于写入非缓存(Uncacheable)或写合并(Write Combining)类型的地址空间(如GPU的帧缓冲区),处理器使用专门的写合并缓冲(Write Combining Buffer, WCB)。WCB的每项可以缓存一个Cache行大小的数据,多个Store可以写入同一WCB项的不同字节位置。只有当WCB项被填满、或被驱逐时,才将整行数据一次性写入存储器。
WCB对流式写入(Streaming Writes)模式特别有效——例如memset或memcpy等函数对连续内存区域的顺序写入。没有WCB时,每个Store都会产生一次总线事务;有WCB后,64个字节Store可以合并为一次Cache行写入事务,减少了总线带宽占用和写入延迟。
硬件描述 6 — Intel的Store Buffer与写合并
Intel Core微架构中的Store Buffer排空流程:
已提交的Store按FIFO顺序从Store Buffer头部出队。
如果出队的Store地址与正在排空的Cache行相同(即连续的Store写同一Cache行),则合并写入。
合并后的写入作为一个事务发送到L1 Cache。
对于Write Combining内存类型(PAT设置),使用专用的WCB(通常410项),不经过L1 Cache直接写入更低层次。
当Store Buffer满或接近满时,后端会暂停Load/Store的分发,直到足够多的Store被排空。
Store合并与Cache写策略
Store合并与Cache的写策略(Write Policy)密切相关。在写回(Write-Back)Cache中,Store将数据写入Cache行并标记为脏(dirty),脏行在被替换时才写回下一级。在写直达(Write-Through)Cache中,Store同时写入Cache和下一级存储器。Store合并对写直达Cache特别重要——没有合并的话,每个Store都会产生一次对下一级存储器的写事务,总线带宽很快饱和。
在写回Cache中,Store Buffer排空到Cache时,如果目标Cache行不在L1中(即写未命中),处理器有两种选择:
写分配(Write Allocate):先将目标Cache行从下一级调入L1,然后在L1中修改对应字节。这保证了Cache行的完整性,但引入了额外的读取延迟。
非写分配(No-Write Allocate / Write Around):直接将Store的数据写入下一级,不调入L1。这避免了污染L1 Cache,但后续对同一地址的Load将会在L1中未命中。
现代处理器的L1 Data Cache几乎都采用写回+写分配策略。Store Buffer的合并优化在此策略下仍然有意义——多个Store对同一Cache行的写入可以合并为一次写分配操作,减少了L2 Cache的读取次数。
值得一提的是,某些处理器(如Intel Haswell及以后)支持非临时Store(Non-Temporal Store)指令(如x86的MOVNTI),这些指令绕过所有级别的Cache直接写入内存,并使用Write Combining Buffer进行合并。非临时Store适用于大块数据的写入(如memcpy的目标缓冲区),因为写入的数据不会被短期内再次读取,缓存它们只会污染Cache并浪费容量。RISC-V目前没有标准的非临时Store指令,但可以通过PMA(Physical Memory Attributes)将特定地址区域配置为Write Combining类型来实现类似效果。
Store合并的正确性约束
Store合并并非总是安全的——某些情况下合并可能违反内存序保证。以下是Store合并的正确性约束:
相同内存类型:只有写入相同内存类型(如都是WB或都是WC)的Store才能合并。不同内存类型的Store可能有不同的排序和可见性要求,合并可能违反这些要求。
不跨越FENCE:如果两个Store之间有FENCE指令,则不能合并——FENCE要求它之前的Store在它之后的Store之前变得对其他核可见。合并会破坏这个顺序保证。
不跨越设备内存边界:对设备内存(MMIO)的Store通常不能合并,因为设备可能对Store的数量和顺序敏感。例如,两次写入同一MMIO寄存器可能有不同的效果(如触发两次中断),合并为一次写入会丢失一次触发。
原子性保证:如果一个Store是原子操作的一部分(如AMO指令的写入阶段),不能与其他Store合并——否则可能破坏原子操作的原子性。
在硬件中实现这些约束的方法是:Store Buffer条目中记录每个Store的内存类型(Memory Type字段,23位)和合并禁止标志(No-Combine标志,1位)。排空逻辑在合并之前检查这些字段——只有内存类型相同且都没有设置No-Combine标志的连续Store才能合并。
Store合并的量化收益
Store合并的收益因工作负载而异。在以下场景中收益特别显著:
memset/memcpy:连续的字节或字Store写入相邻地址。4个连续的
sb可以合并为1个4字节写入,减少75%的Cache写端口占用。更大的合并(如16个字节合并为1个16字节写入)在支持宽Write Combining的处理器上效果更好。结构体赋值:将结构体的多个字段逐一赋值时,如果字段大小不均(如混合1字节、2字节、4字节字段),产生多个小Store。合并可以将它们组合为更少的宽Store。
编译器生成的溢出代码:当寄存器压力过大时,编译器将寄存器值溢出到栈上——产生多条Store写入栈帧的不同位置。如果多个溢出Store写入同一Cache行,可以合并。
量化数据:在SPEC CPU 2017整数基准中,Store合并可以减少约10%25%的L1 Cache写事务数。在字符串处理密集的程序(如perlbench)中,减少比例可高达30%40%。
Store合并的硬件实现细节
Store合并逻辑的实现需要以下硬件组件:
Cache行地址比较器:比较当前排空Store与下一个待排空Store的Cache行地址(PA[47:6],共42位)。如果两者的Cache行地址相同,可以合并。这个比较器可以复用STLF的CAM比较逻辑,但比较的是Cache行地址而非字节地址。
合并数据缓冲(Merge Buffer):一个64字节宽的缓冲区(一个Cache行大小),配备一个64位的脏字节掩码(dirty byte mask)。合并过程中,每个参与合并的Store将其数据写入缓冲区的对应字节位置,并设置对应的脏字节掩码位。
合并链终止检测:当下一个待排空Store的Cache行地址不同于当前合并缓冲中的地址时,合并链终止。此时将合并缓冲的数据作为一个写事务发送到L1 Cache(只写入脏字节掩码为1的字节),然后清空合并缓冲开始新的合并。
合并数量限制:为防止合并逻辑无限延迟排空(如果连续大量Store写入同一Cache行),通常设置一个最大合并数量(如最多合并816条Store)。超过限制后强制排空。
合并缓冲的面积开销为位(512位数据 + 64位脏掩码)72 B。相比64项Store Buffer的2 KB面积,合并缓冲的额外面积仅3%——是一个非常划算的优化。
合并排空的时序优势在于:合并后的写事务只占用L1 Cache写端口一个周期(写入整个Cache行的脏字节),而未合并的条Store需要占用个周期。在Store密集的代码段中,这可以将Store Buffer排空速率提高数倍,有效防止Store Buffer满阻塞。
Store Buffer的可见性与一致性
Store Buffer引入了一个重要的可见性问题:Store Buffer中的数据只对本核可见,对其他核不可见——其他核看到的是L1 Cache中的旧值,而不是Store Buffer中的新值。这对多核处理器的内存一致性有深刻影响。
Store Buffer与TSO
在TSO(Total Store Ordering)内存模型中,Store Buffer的存在是Store-Load重排的根本原因。考虑以下经典的多核场景(Dekker算法的简化版):
// 核0 // 核1
X = 1; // Store Y = 1; // Store
r0 = Y; // Load r1 = X; // Load在顺序一致性(Sequential Consistency, SC)模型下,和不可能同时成立。但在TSO模型下,这是合法的结果——因为每个核的Store可能仍在Store Buffer中(尚未写入Cache/主存),而Load从Cache读到了旧值0。
这种行为的本质是Store Buffer引入的延迟可见性——一个核的Store对其他核的可见性被Store Buffer延迟了。TSO模型允许这种延迟,只要同一核内的Load-Load和Store-Store顺序得到保持。
FENCE指令对Store Buffer的影响
当处理器执行一条FENCE指令时(如RISC-V的fence w,r),它强制Store Buffer排空——所有已提交的Store必须写入L1 Cache后,后续的Load才能执行。这保证了FENCE之前的Store对其他核可见后,FENCE之后的Load才能观察到其他核的最新写入。
FENCE的硬件实现通常是在发射队列中将FENCE指令标记为一个屏障——在FENCE之前的所有Store排空之前,FENCE之后的所有Load不能发射。FENCE的性能代价取决于Store Buffer中待排空的Store数量——如果Store Buffer中有条待排空Store,FENCE的延迟约为周期(假设每周期排空12条Store)。在一个64项Store Buffer中,最坏情况的FENCE延迟约为3060周期。
Store Buffer的前向可见性
一个有趣的设计问题是:Store Buffer中的数据是否应该对本核的Load可见?答案是必须可见——这就是STLF的本质。如果本核的Load不能看到Store Buffer中未排空的Store数据,则每条Load都需要等待所有先序Store排空到Cache后才能执行,性能将严重下降。
从一致性的角度看,本核Load看到Store Buffer中的数据是TSO模型的固有部分——TSO保证每个核可以立即看到自己的Store(不需要等待Store写入Cache),但不保证其他核也能立即看到。这种"本核可见、他核不可见"的不对称性正是由Store Buffer实现的。
设计权衡 3 — Store Buffer大小与一致性延迟
Store Buffer越大,可以容纳越多的飞行中Store,支持更深的乱序窗口和更高的IPC。但更大的Store Buffer也意味着Store到达L1 Cache的延迟更长(排空队列更长),从而增加了Store对其他核可见的延迟。这在多核共享数据的场景中可能影响性能——一个核写入的数据需要更长的时间才能被其他核看到。
在实际设计中,Store Buffer的大小主要由单核IPC的需求决定(通常为64128项),多核一致性延迟是一个次要考量。这是因为大多数多核通信通过同步原语(如lock/unlock、fence)进行,这些原语会强制Store Buffer排空,消除了可见性延迟。
Store Buffer与硬件预取的交互
硬件预取器(Hardware Prefetcher)会提前将数据调入Cache,以减少Load的Cache未命中率。但预取器和Store Buffer之间存在一些需要注意的交互:
Store排空触发的写分配预取
当Store Buffer排空一条Store到L1 Cache时,如果目标Cache行不在L1中(写未命中),需要从L2 Cache调入该行(写分配策略)。这个调入过程本质上是一次"被动预取"——它可以触发L2级的流式预取器开始工作。
如果程序正在执行一个顺序写入模式(例如memset),连续的Store Buffer排空会产生一系列的L1写未命中,每次触发从L2调入一个Cache行。L2的流式预取器可以检测到这种模式,开始提前预取更远的Cache行到L2中,减少后续Store排空的L2未命中延迟。
预取与Store Buffer的一致性
硬件预取器可能将一个Cache行提前调入L1 Cache。如果此时Store Buffer中有一条Store写入该Cache行的某些字节,则L1 Cache中的数据和Store Buffer中的数据存在不一致——Cache中是被预取的旧数据,Store Buffer中是即将写入的新数据。
这种不一致不影响正确性,因为STLF机制保证了本核的Load会从Store Buffer(而非Cache)获取最新数据。但它可能影响功耗效率——预取带来的Cache行可能很快就被Store Buffer排空的Store覆盖,预取的带宽被浪费了。
一些处理器通过Store Buffer过滤预取来避免这种浪费:如果预取器准备预取的Cache行地址与Store Buffer中某个已提交Store的地址匹配,则取消该预取请求——因为该Cache行将很快被Store Buffer排空覆盖,预取它没有意义。这种过滤需要将预取地址与Store Buffer进行CAM搜索(复用STLF的CAM端口),增加了少量的搜索开销。
Store Buffer的微架构总结
Store Buffer是乱序处理器中最复杂的结构之一,集存储、搜索、转发、排空和一致性管理于一体。以下是Store Buffer设计的关键要点总结:
Store Buffer必须支持乱序写入(地址和数据可以任意顺序写入)和顺序排空(按程序序排空到Cache)。
Store Buffer的CAM结构是STLF的基础,其时序是Load数据路径的关键瓶颈。
部分转发的支持程度是面积-性能的权衡——完全覆盖转发是最低要求,多Store合并转发的硬件代价通常不值得。
Store合并可以显著减少Cache写端口的占用,特别是对流式写入模式。
Store Buffer引入了多核可见性延迟,但TSO模型允许这种延迟,FENCE指令可以在需要时强制排空。
Store Buffer的大小需要与ROB大小匹配——太小会导致Store Buffer满阻塞流水线,太大会增加面积和STLF延迟。
Load/Store队列
Load/Store队列(Load/Store Queue, LSQ)是乱序处理器中管理所有飞行中(in-flight)存储器指令的核心结构。它记录了每条存储器指令的状态信息,用于地址消歧、数据转发、违规检测和按序提交。LSQ的设计直接影响处理器的存储器指令吞吐量和IPC。
Load Queue的结构与字段分析
Load Queue(LQ)记录所有已分发但尚未提交的Load指令的信息。LQ的每项包含:
| 字段 | 位宽 | 说明 |
|---|---|---|
| Valid | 1位 | 该项是否有效 |
| Executed | 1位 | Load是否已执行(已读取数据) |
| Address Valid | 1位 | Load地址是否已计算完成 |
| Physical Address | 4852位 | Load的物理地址 |
| Virtual Address | 12位 | 虚拟地址低12位(页内偏移,用于快速匹配) |
| Size | 23位 | 访存大小 |
| Data | 64128位 | Load读取的数据(用于违规检测时重新验证) |
| ROB Index | 79位 | 对应的ROB项编号 |
| SQ Tail Snapshot | 67位 | 分发时Store Queue的尾指针快照 |
| Speculative | 1位 | 是否为推测执行的Load |
| Need Replay | 1位 | 是否需要重放(Cache未命中等原因) |
Load Queue每项的字段与详细分析
逐字段分析关键字段的设计考量:
Data字段:Load Queue中存储Load读取的数据,这看似冗余(数据已经写入目的物理寄存器),但它在违规检测的一种优化方案中至关重要。当Store地址计算完成后触发违规检测,如果发现地址匹配的已执行Load,可以先比较数据——将Load之前读到的数据与Store即将写入的数据进行比较。如果数据相同(Store写入的值恰好等于Load读到的旧值),则即使地址冲突,Load的结果仍然正确,无需触发违规恢复。这种值匹配优化可以减少约10%20%的无谓违规冲刷。但它需要Load Queue中存储完整的Load数据,增加了Load Queue的面积。
SQ Tail Snapshot:这个字段记录了Load被分发时Store Queue的尾指针位置。它的作用是快速判断Store Queue中的某条Store是否是该Load的先序Store(详见36.3.5 节)。没有这个快照,年龄判断需要使用ROB索引进行复杂的循环比较。
Load Queue的每项总位宽约为位。对于一个128项的Load Queue,总存储约位 3.3 KB。加上CAM比较逻辑(用于违规检测),总面积约为纯存储的23倍。
Load Queue的主要功能有:
违规检测:当一条先序Store的地址计算完成后,Store的地址被广播到Load Queue的所有项,检查是否有已执行的Load与之地址冲突(详见36.4 节)。这是Load Queue存在的首要原因——如果没有推测式消歧,就不需要违规检测,Load Queue的CAM结构也不需要。
Load回放:当Load因Cache未命中或其他原因需要重新执行时,Load Queue提供必要的状态信息(地址、大小等),使回放逻辑能够重新生成Load请求。
有序提交:Load Queue按照分配顺序(即程序序)回收项,确保Load按序提交。虽然Load的执行可以乱序,但提交必须按序——这是精确异常的要求。
一致性检查(仅TSO模型):当收到其他核的Snoop invalidation时,搜索Load Queue中是否有已执行的Load读取了被invalidated的Cache行。
Load-Store依赖管理:记录每条Load的SQ Tail Snapshot,用于确定哪些Store是该Load的先序Store。这个信息同时用于STLF(确定搜索范围)和违规检测(确定检测范围)。
可以看到,Load Queue的存在完全是为了支持推测式消歧——在保守式消歧中,Load Queue可以大幅简化甚至完全省略(因为不需要违规检测和回放)。这也解释了为什么有序处理器(如RISC-V Rocket)不需要Load Queue。
Load Queue的CAM结构与功耗分析
Load Queue的CAM结构是其面积和功耗的主要贡献者。每项需要一个地址比较器(用于违规检测),以及年龄比较逻辑。对于一个128项的Load Queue,违规检测时需要128个并行比较器同时工作。
每个比较器比较约20位地址(物理地址低20位,或页内偏移12位加上部分页号),需要约个晶体管。128个比较器总共约K个晶体管,仅用于匹配逻辑。加上年龄过滤和优先级编码逻辑,Load Queue的CAM部分总门数约门。
功耗方面,CAM的动态功耗与搜索频率成正比。在一个每周期可能有2条Store完成地址计算的处理器中,Load Queue的CAM每周期最多搜索2次,每次搜索128项。对于一个128项、20位比较宽度的CAM,每次搜索的动态功耗约 mW@3 GHz,两次搜索的总功耗约 mW。
一些设计通过分组(banking)来降低功耗——将Load Queue分为48个bank,根据地址的低位将Store地址只发送到匹配的bank进行比较,减少同时活动的比较器数量。例如,使用地址的低3位作为bank选择,每次搜索只激活个比较器,功耗降低约。但bank化增加了设计复杂度——如果一条Store的地址范围跨越多个bank,需要同时搜索多个bank。
性能分析 5 — Load Queue大小对IPC的影响
Load Queue的大小直接限制了处理器的存储器级并行(Memory-Level Parallelism, MLP)。在Cache未命中频繁的工作负载中,多个Load可能同时等待L2/L3 Cache或内存的响应,每个未完成的Load都占据一个Load Queue项。如果Load Queue满了,处理器无法分发新的Load指令,MLP受限。
定量分析:假设一条Load的L1 Cache未命中延迟为12周期(L2命中),L2未命中延迟为50周期(L3命中),L3未命中延迟为200周期(内存访问)。在一个L1 Cache未命中率为5%的工作负载中:
每100条Load中有5条需要12200周期才能完成。
在一个6发射处理器中,约2条Load/周期被分发,这5条未完成Load占据Load Queue约30120周期。
如果Load Queue只有32项,可能在高Cache未命中率下成为瓶颈。
将Load Queue从48项增加到128项,在
mcf(高Cache未命中率)上可提升IPC约8%15%。
Load执行流水线
Load指令在乱序处理器中的执行过程涉及多个流水线阶段和硬件结构的协同工作。理解Load的完整执行流水线对于分析性能瓶颈和设计优化至关重要。
Load的流水线阶段
一条Load指令从发射到结果写回,经过以下流水线阶段:
发射(Issue,第0周期):调度器选中Load指令,将其从发射队列送入Load执行管道。Load的基址寄存器值从物理寄存器堆或旁路网络读取。
地址生成(AGU,第1周期):地址生成单元计算有效虚拟地址。加法器宽度为64位(RV64),延迟约0.3 ns@7nm。同时,的低12位被发送到L1 Cache进行索引(VIPT Cache可以在TLB翻译之前开始索引)。
AGU的设计看似简单(只是一个加法器),但实际中有一些需要注意的优化。首先,RISC-V的Load偏移量为12位有符号立即数(),符号扩展到64位后与基址相加。加法器的关键路径是进位链(carry chain),对于64位加法器,使用条件求和加法器(Conditional Sum Adder)可以将延迟降低到1214级门(相比串行进位加法器的64级门)。
其次,AGU的输出需要同时驱动多个目的地——L1 Cache索引逻辑、TLB查找逻辑、Store Queue CAM搜索逻辑。这些目的地的物理位置可能分散在芯片的不同区域,需要仔细的布线规划和缓冲器插入来保证信号完整性和时序。
一些设计在AGU阶段还进行地址有效性初步检查——判断计算出的地址是否在合法的虚拟地址范围内(对于Sv48页表,有效虚拟地址的高16位必须都等于第47位)。这个检查可以在AGU加法器的最高位完成后立即进行,与低位地址发送到Cache的过程并行。
TLB查找与Cache索引(第2周期):L1 TLB并行查找物理页号PPN。同时,L1 Data Cache用的索引位读取对应Set的所有Way的Tag和Data SRAM。如果使用VIPT方式且Cache大小页面大小路数,则Cache索引不依赖于PPN,两者可以完全并行。
VIPT(Virtually Indexed, Physically Tagged)方式是现代L1 Data Cache的标准设计。其关键约束是:Cache的Set数 Cache行大小 页面大小。对于4KB页面和64字节Cache行,这意味着Set数。对于一个32KB、8路组相联的Cache,Set数 = ,恰好满足约束。如果Cache更大(如64KB),则需要更多的路数(16路)来保持Set数不变,或者使用虚拟别名处理机制。
Tag比较与Store Queue搜索(第3周期):TLB输出PPN后,将PPN与L1 Cache读出的各Way Tag进行比较,确定命中的Way。同时,物理地址被发送到Store Queue的CAM进行STLF搜索。两个操作并行进行。
数据选择与对齐(第4周期):根据Tag比较的结果选择命中Way的数据。如果Store Queue命中(STLF成功),使用Store Queue的数据代替Cache数据。数据经过字节选择和对齐(根据地址低位和Load大小),然后进行符号扩展(对于
lb、lh等有符号Load)。结果写回(第45周期):最终数据写入目的物理寄存器文件,同时通过旁路网络转发给已被唤醒的消费者指令。Load在Load Queue中被标记为Executed = 1。
因此,Load的总延迟(从发射到结果可用)为45个周期——这是L1 Cache命中和STLF成功时的最佳情况延迟。如果L1未命中,延迟增加到12+周期(L2命中)或更长。
Load执行中的异常检查
在Load的执行过程中,多种异常情况需要被检查:
TLB未命中:如果Load的虚拟地址在L1 TLB和L2 TLB中都未命中,需要触发Page Table Walk。Load被暂停,等待页表遍历完成后重新执行。Page Table Walk的延迟可能高达100500周期。
Page Fault:如果Load访问的虚拟页面尚未映射到物理页面(页表项的有效位为0),或者访问权限不足(例如用户态访问内核页面),触发Page Fault异常。Load在ROB中被标记为"异常",在提交时触发异常处理。
地址不对齐:如果Load的地址不满足自然对齐要求(如
lw的地址不是4的倍数),在某些架构上触发Address Misaligned异常。RISC-V的基本ISA要求自然对齐,但Zam扩展允许非对齐访问。PMA/PMP检查:RISC-V的物理内存属性(PMA)和物理内存保护(PMP)单元检查Load是否有权访问目标物理地址。违规的访问触发Access Fault异常。
这些异常检查可以与正常的数据路径并行进行——异常信号在数据路径完成的同一周期或下一周期产生。如果检测到异常,Load的结果被标记为无效,Load在ROB中记录异常类型,等待提交时由异常处理逻辑处理。
Load端口与带宽
处理器通常有13个Load执行端口,每个端口可以每周期执行一条Load指令。Load端口的数量决定了Load的吞吐量上限——在一个2端口Load设计中,每周期最多执行2条Load。
每个Load端口需要独立的:
AGU(地址生成单元)。
L1 TLB的读端口。
L1 Data Cache的读端口。
Store Queue的CAM搜索端口。
结果旁路网络的写端口。
增加Load端口对面积和功耗的影响显著——L1 Cache的每增加一个读端口,面积增加约30%50%。这就是为什么大多数处理器限制在23个Load端口。Intel Golden Cove和Apple Firestorm拥有3个Load端口,是目前最宽的设计之一。
硬件描述 7 — Load端口与Cache Bank的关系
为支持多个Load端口而不使Cache面积急剧增加,一些设计使用Cache Banking——将L1 Data Cache分为多个bank,不同的Load可以同时访问不同的bank。例如,一个32KB、8路组相联的L1 Data Cache可以被分为4个bank(每bank 8KB),每个bank有独立的读端口。只要两条并行Load访问的地址映射到不同的bank,它们就可以在同一周期完成。
如果两条Load映射到同一bank(Bank冲突),其中一条需要被推迟到下一周期——这就是前面提到的Bank冲突导致Load回放的情况。Bank冲突的概率取决于bank数量和访问模式。对于4个bank的设计,随机访问模式下的冲突概率约25%30%(两条Load中至少有一对冲突的概率)。
Store Queue的结构
图图 36.9展示了Load Queue和Store Queue在乱序引擎中的整体组织结构。
Store Queue与Store Buffer的区别
Store Queue(SQ)在功能上与前面讨论的Store Buffer有大量重叠。在许多微架构文献中,"Store Queue"和"Store Buffer"有时被混用,但严格来说它们有区别:
Store Queue:管理所有已分发但尚未提交的Store指令,包括推测执行的Store。Store Queue的功能包括地址消歧、数据转发和年龄序维护。Store Queue中的条目可能被冲刷(如果分支预测失败或违规恢复)。
Store Buffer:特指已提交但尚未写入Cache的Store的缓冲区。Store Buffer中的Store已经确认不会被撤销,等待写入Cache。Store Buffer中的条目不会被冲刷。
在一些微架构实现中,Store Queue和Store Buffer合并为一个统一的结构,每项通过"Committed"标志位区分是否已提交。在另一些实现中,Store Queue和Store Buffer是物理上分离的两个结构——Store在提交时从Store Queue"毕业"(graduate)到Store Buffer。
分离设计的优势在于:
Store Buffer可以独立地以高速率排空到Cache,而不受Store Queue中推测Store的影响。
Store Queue的大小可以针对"推测Store的消歧需求"来优化,而Store Buffer的大小针对"已提交Store的排空带宽"来优化——两者的设计约束不同,分离处理可以各自达到更优的配置。
STLF的CAM搜索可以限定在更小的范围内——只搜索Store Queue中的推测Store即可(已提交Store的数据已经写入或即将写入Cache,从Cache读取即可),降低了动态功耗。
冲刷操作只影响Store Queue,不需要清除Store Buffer中已提交的Store——这些Store的数据必须被保留并最终写入Cache。
合并设计的优势在于:
只需一个物理结构,面积更小。
STLF可以在一个统一的搜索中同时匹配推测Store和已提交Store,实现更简单。
不需要Store从SQ到SB的数据搬移逻辑。
Store Queue的每项字段与表表 36.5中列出的Store Buffer字段基本相同,增加一个"Speculative"标志位表示该Store是否处于推测状态。
分离设计中的Store毕业过程
在分离设计(SQ + SB分开)中,Store从Store Queue"毕业"(graduate)到Store Buffer的过程如下:
ROB按程序序提交Store指令。
提交逻辑向Store Queue发送"提交"信号,标识SQ头部的Store可以毕业。
Store Queue头部的已提交Store的数据被复制到Store Buffer的一个空闲条目中。
Store Queue头部条目被释放(回收),SQ头指针递增。
Store Buffer的新条目被标记为"可排空"(ready to drain)。
这个复制过程需要从SQ到SB的数据路径——一个130位宽的数据通道(地址 + 数据 + 控制位)。如果每周期最多毕业2条Store(与ROB的提交宽度匹配),则需要2个从SQ到SB的数据端口。
分离设计的一个关键优势是解耦提交和排空。在合并设计中,SQ/SB是同一个结构,已提交Store必须等到排空后才能释放条目。如果排空速度较慢(如遇到L1 Cache未命中),SQ/SB可能填满,导致无法分发新的Store。在分离设计中,Store提交后从SQ毕业到SB,SQ条目立即释放——即使SB排空较慢,SQ的空间已经释放给新的Store使用。只有当SB也满了时,才会阻塞SQ的毕业,进而阻塞ROB的提交。
这种解耦效果在Store密集的工作负载中特别有价值——SQ的回收速率由ROB提交速率决定(通常很快),而不是由Cache写端口的带宽决定(可能较慢)。
STLF在分离设计中的搜索范围
在分离设计中,Load进行STLF时需要同时搜索Store Queue(推测Store)和Store Buffer(已提交Store)。这是因为Load可能需要的数据来自一条已经提交但尚未排空到Cache的Store——如果只搜索SQ而不搜索SB,可能漏掉这条Store,Load从Cache读到过期值。
两个结构的搜索可以并行进行(SQ搜索和SB搜索同时开始),最终通过优先级逻辑选择最年轻的匹配:
如果SQ和SB都有匹配,选择SQ中的匹配(因为SQ中的Store比SB中的更年轻——SB中的Store已经提交,一定比SQ中任何推测Store更老)。
如果只有SB有匹配,使用SB的数据。
如果都没有匹配,从Cache读取。
但并行搜索两个结构增加了功耗——每条Load的STLF都需要激活两套CAM。一种优化是先搜索SQ、再搜索SB(串行搜索)——如果SQ中找到匹配,则不需要搜索SB。但串行搜索增加了STLF的延迟(两次CAM搜索的串行延迟)。在实际设计中,大多数选择并行搜索(接受功耗代价)以保持低延迟。
统一队列与分离队列
Load Queue和Store Queue可以采用统一(Unified)或分离(Split)两种组织方式:
统一队列(Unified LSQ)
将Load和Store混合存储在一个统一的队列中,按程序序排列。统一队列的优点是:
Load和Store之间的年龄比较天然由队列位置给出,无需额外的年龄比较逻辑。
队列项数可以在Load和Store之间动态共享——如果程序中Load较多、Store较少,更多的队列项可以分配给Load。
实现相对简单,只需一个循环队列。
统一队列的缺点是:
Store-to-Load转发需要在统一队列中搜索特定类型的项(Store),增加了匹配逻辑的复杂度。每个条目的CAM比较器除了地址匹配外,还需要检查"类型 = Store"条件。
违规检测时,需要搜索所有Load项并跳过Store项,降低了搜索效率。
每项需要一个额外的"类型"字段(1位)区分Load和Store。
如果队列很大,CAM搜索的功耗较高——每次搜索需要激活所有条目的比较器,但其中约一半(Load或Store,取决于搜索方向)的结果将被丢弃。
分离队列(Split LSQ)
将Load Queue和Store Queue作为两个独立的循环队列实现。分离队列的优点是:
每个队列只包含单一类型的指令,搜索和匹配逻辑更高效。STLF只需搜索Store Queue(不搜索Load Queue),违规检测只需搜索Load Queue(不搜索Store Queue)。
Store Queue可以专门优化为Store Buffer的功能(地址匹配和数据转发),Load Queue可以专门优化为违规检测。
两个队列可以独立地分配和回收项,互不干扰。
功耗更低——每次CAM搜索只激活相关队列的比较器。
分离队列的缺点是:
两个队列之间需要额外的年龄比较逻辑(如何判断一条Load比一条Store更老或更年轻)。
队列项数无法在Load和Store之间动态共享——如果Load Queue满了但Store Queue有空项,新的Load仍然无法分发。这可能导致资源碎片化——在Load密集型代码段中Load Queue提前填满,而Store Queue利用率很低。
需要维护两个队列的分配/回收指针和状态机。
面积对比分析
从面积角度对比两种方案。假设需要支持128条飞行中存储器指令(约60%是Load、40%是Store):
统一队列方案:一个128项的统一LSQ。每项需要存储Load和Store的所有字段的并集——地址(50位)、数据(128位,Load数据 + Store数据共用)、字节使能(16位)、控制位(15位)、ROB索引(8位)、类型位(1位)218位/项。CAM部分需要支持两种搜索(STLF和违规检测),搜索时需要同时检查类型位,每项增加4门。总存储:位3.4 KB。CAM面积:0.06 mm。
分离队列方案:Load Queue 80项 + Store Queue 48项(总计128项,但按60:40比例分配)。LQ每项213位(见表表 36.8),SQ每项263位(见表表 36.5)。LQ总存储:位2.1 KB。SQ总存储:位1.5 KB。总计3.6 KB。LQ的CAM(违规检测用,20位比较)面积0.03 mm。SQ的CAM(STLF用,45位比较)面积0.03 mm。总CAM面积0.06 mm。额外需要年龄比较逻辑:0.005 mm。
两种方案的总面积基本相当。但分离方案的优势在于功耗——STLF搜索只激活48项SQ的CAM(而非128项统一队列),违规检测只激活80项LQ的CAM,功耗分别降低约和。
分离方案的缺点在于资源碎片化。在最坏情况下(所有飞行中指令都是Load),统一队列可以容纳128条Load,而分离队列只能容纳80条——差异为60%。但在实际工作负载中,Load:Store的比例相对稳定(约60:40),碎片化的实际影响通常2%的IPC损失。
案例研究 3 — 实际处理器的LSQ组织
Alpha 21264:分离队列。Load Queue 32项,Store Queue 32项。
Intel Skylake:分离队列。Load Buffer 72项,Store Buffer 56项。
Intel Golden Cove:分离队列。Load Buffer 192项,Store Buffer 72项。注意Load Buffer远大于Store Buffer,因为Load指令数量通常多于Store,且Cache未命中的Load需要长期占据队列项。
AMD Zen 4:分离队列。Load Queue 88项,Store Queue 64项。
Apple M1 Firestorm:分离队列。Load Queue 约130项,Store Queue 约108项。
ARM Cortex-A77:分离队列。Load Queue 68项,Store Queue 44项。
现代高性能处理器几乎无一例外地采用分离队列设计。统一队列主要出现在早期或低功耗设计中。
设计权衡 4 — LSQ的大小与流水线阻塞
LSQ的大小直接影响流水线的阻塞概率(stall probability)。当Load Queue或Store Queue满时,分发阶段必须暂停,直到有项被释放。阻塞的频率取决于:
存储器指令的比例:SPEC CPU中约30%40%的指令是存储器指令。
存储器指令的延迟:Cache未命中的Load可能长期占据Load Queue项。
Store提交的速率:Store Queue项只有在Store提交后才能释放,而提交速率受限于ROB的顺序提交约束。
在设计LSQ大小时,一个经验法则是:Load Queue的项数应至少为ROB大小的30%50%,Store Queue应至少为ROB的15%25%。例如,一个256项ROB的处理器,Load Queue建议为80128项,Store Queue建议为4064项。
这个经验法则的来源是SPEC CPU基准测试中的统计:约35%的指令是存储器指令,其中60%是Load(21%的总指令)、40%是Store(14%的总指令)。在一个256项ROB中,平均有条Load和条Store在飞行。考虑到需要一定的余量来应对突发的高密度存储器指令段,Load Queue和Store Queue的大小分别设为80128项和4064项是合理的。
年龄序的维护
在分离队列设计中,一个关键问题是如何高效地判断两条存储器指令之间的年龄关系(age ordering)——即哪条指令在程序序中更靠前(更老)。年龄关系在以下操作中至关重要:
Store-to-Load转发:当Load在Store Queue中找到多个地址匹配的Store时,必须选择最年轻的先序Store(即在Load之前、但尽可能接近Load的Store)。
违规检测:当Store的地址计算完成后,必须检查是否有比该Store更年轻的Load已经提前执行且地址冲突。
提交顺序:Store必须按程序序提交到Store Buffer。
年龄比较的常见实现方式有:
ROB索引比较
每个Load Queue和Store Queue项都记录了对应的ROB索引。由于ROB按程序序分配,ROB索引可以直接用于年龄比较。但ROB是循环队列,直接比较索引号可能出错(例如索引255比索引0更老,但)。正确的比较需要考虑循环回绕:
其中是ROB的大小,是当前ROB头指针(最老的未提交指令)。
这个比较需要两个减法器(各位宽)和一个比较器,总共约个晶体管(对于)。但ROB头指针是全局信号,需要广播到所有需要年龄比较的位置(Store Buffer的每个条目、Load Queue的每个条目),这增加了布线复杂度和信号延迟。
分发序列号
另一种方式是为每条指令分配一个单调递增的序列号(sequence number)。序列号在每次分发时递增,不会回绕(或在回绕时使用足够宽的计数器)。年龄比较直接通过序列号的大小关系完成。这种方式更简单——只需一个比较器,无需ROB头指针。但序列号的位宽需要足够大(通常1012位)以避免回绕。12位序列号可以区分4096条指令的年龄顺序,对于256项ROB来说已经足够。
SQ尾指针快照
在分离队列设计中,一种常用的技术是:当Load被分发到Load Queue时,记录此时Store Queue的尾指针位置(SQ Tail Snapshot)。这个快照标记了分发Load时Store Queue中最年轻的Store——所有在此指针之前(按循环队列顺序)的Store都比该Load更老。这样在后续进行Store-to-Load转发或违规检测时,可以快速判断一个Store是否是该Load的先序Store。
具体地,设Store Queue的大小为,Load分发时记录的SQ尾指针快照为。对于Store Queue中的某项,如果:
则比该Load更老(是先序Store)。这个比较只需要一个简单的减法器和比较器(位宽),比全局序列号比较更高效。
反之,在Store地址计算完成后进行违规检测时,也需要判断Load Queue中的某条Load是否比当前Store更年轻。类似地,每条Store在分发时记录当时Load Queue的尾指针位置(LQ Tail Snapshot),后续用于快速判断哪些Load需要被检查。
设计提示
年龄比较逻辑的延迟直接影响Store-to-Load转发的时序路径。在时序紧张的设计中,可以将年龄比较与地址比较并行进行——先并行获得"地址匹配"和"年龄合法"两个结果,再用一个与门将两者合并选择最终的转发源。这种并行化以少量额外面积换取了更短的关键路径延迟。
具体地,在Store Buffer的每个条目中,地址比较器和年龄比较器同时开始工作。两者的延迟分别约为0.5ns(12位异或 + 12输入与门)和0.3ns(7位减法 + 比较)。由于年龄比较更快,它的结果会先就绪,不在关键路径上。最终的"匹配有效"信号 = 地址匹配 AND 年龄合法 AND Valid AND Address_Valid,需要一个4输入与门(0.05ns),总延迟约0.55ns。
完整的Load执行流程示例
为了将本章讨论的各个机制融会贯通,下面通过一个完整的示例展示一条Load指令从取指到提交的全部交互过程。假设处理器具有以下配置:6发射、256项ROB、128项LQ、64项SQ、4096项SSIT、256项LFST、L1 Cache 4周期延迟。
指令:ld a3, 0(s2),PC = 0x4028。
周期(取指/译码):前端取到该Load指令。译码阶段识别它为Load指令。同时查询Store Set预测器:用PC[13:2] = 0x00A索引SSIT,读出SSIT[0x00A] = {Valid=1, SSID=5}。用SSID=5索引LFST,读出LFST[5] = {Valid=1, SQ#=23}。这意味着该Load需要等待Store Queue中的第23项。
周期(分发):Load被分发到发射队列。同时分配LQ条目(LQ#67)和ROB条目(ROB#180)。在LQ#67中记录:ROB Index = 180,SQ Tail Snapshot = 当前SQ尾指针25。Load在发射队列中被标记为"依赖于SQ#23"——它的"内存依赖"源操作数来自SQ#23的地址就绪信号。
周期(等待):Load在发射队列中等待两个条件:(1) 基址寄存器s2就绪(操作数依赖),(2) SQ#23的地址计算完成(内存依赖,来自Store Set预测器)。假设s2在就绪,但SQ#23的地址在才就绪。
周期(发射):两个条件都满足。调度器选中该Load,从发射队列送入Load执行管道。s2的值从物理寄存器堆读出。
周期(AGU + Cache索引 + SQ搜索开始):AGU计算虚拟地址。的低12位送入L1 Cache进行Set索引。同时送入Store Queue的CAM开始STLF搜索。TLB查找也并行开始。
周期(Tag比较 + SQ CAM匹配):TLB输出PPN = 0x1234,物理地址PA = 0x1234_8000。Cache读出对应Set的Tag并与PPN比较——Cache命中Way 2,数据为0xOLD。同时SQ CAM搜索完成:SQ#23的地址为0x8000,匹配!SQ#23的Data Valid = 1,数据为0xNEW。优先级编码器确认SQ#23是最年轻的匹配先序Store。
周期(数据选择 + 对齐 + 写回):由于SQ命中,选择SQ#23的数据0xNEW(而非Cache的0xOLD)。数据经过对齐和符号扩展后写入目的物理寄存器a3。同时通过旁路网络转发给消费者指令。LQ#67标记为Executed = 1,记录地址0x8000和数据0xNEW。
周期(提交):当ROB#180到达ROB头部时,Load被提交。LQ#67被释放(Valid = 0),LQ头指针递增。
整个过程中涉及了以下硬件结构的协同工作:
Store Set预测器(SSIT + LFST):在取指阶段识别Load的内存依赖。
发射队列:管理操作数依赖和内存依赖的等待。
Load Queue:分配条目、记录执行状态、提交时释放。
AGU:计算有效地址。
TLB:虚拟地址到物理地址翻译。
L1 Cache:并行访问Tag和Data SRAM。
Store Queue CAM:STLF搜索。
数据选择MUX:在SQ数据和Cache数据之间选择。
旁路网络:将结果转发给消费者指令。
ROB:按序提交和异常处理。
LSQ的分配与回收
在分离队列设计中,Load和Store的分配(allocation)需要在指令分发阶段完成。分配逻辑需要判断当前分发的指令是Load还是Store(或两者都不是),并将其分配到对应队列的尾部。如果目标队列已满,则分发阶段必须暂停。一个重要的优化是允许Load Queue和Store Queue的分配与ROB分配并行进行——如果任何一个结构(ROB、LQ或SQ)满了,整个分发阶段都需要暂停。
在一个6发射处理器中,每周期最多分发6条指令,其中可能有06条Load和06条Store。分配逻辑需要在一个周期内:
确定6条指令中哪些是Load、哪些是Store。
检查Load Queue和Store Queue是否有足够的空闲项。
为每条Load/Store分配一个队列项(写入队列尾指针,更新尾指针)。
将分配的队列索引记录到ROB对应条目中,以便后续通过ROB索引找到对应的LQ/SQ项。
多条Load/Store的并行分配需要多入口队列尾指针更新——如果本周期有条Load需要分配,则LQ尾指针一次性递增。这需要一个简单的加法器和路队列项写入端口。
在指令提交(commit/retire)阶段,Load Queue和Store Queue按照分配顺序回收项。Load在提交时直接释放Load Queue项(因为Load的结果已经写入寄存器文件)。Store在提交时设置"Committed"标志,但不立即释放Store Queue项——Store的数据仍然需要保留在Store Queue / Store Buffer中,直到排空到Cache后才能释放。这意味着Store Queue项的回收速率不仅受限于提交速率,还受限于Cache写端口的带宽。
分配端口的竞争与仲裁
在宽发射处理器中,每周期可能需要同时分配多条Load和Store到LSQ。分配端口的数量限制了每周期可以分配的存储器指令数量。例如,在一个6发射处理器中:
最坏情况:6条指令全部是Load——需要6个LQ分配端口。但提供6个写端口的LQ面积极大。
实际设计:通常限制每周期最多分配23条Load和23条Store。如果某周期有4条Load需要分配但只有3个LQ分配端口,则第4条Load(及其后续指令)被延迟到下一周期分发。
经验数据:在SPEC CPU中,每周期分发的Load指令数平均约1.2条,超过2条的概率15%,超过3条的概率3%。因此3个分配端口通常足够。
分配端口的仲裁逻辑需要在一个周期内完成以下操作:(1) 扫描分发窗口中的6条指令,识别其中的Load和Store;(2) 按程序序为前3条(或2条)Load分配LQ项,为前3条Store分配SQ项;(3) 如果分配端口不够(Load或Store超过端口数),阻塞后续指令。
这个仲裁逻辑需要一个简单的前缀计数器——扫描6条指令的类型位,计算Load和Store的累计数量。当累计Load数超过LQ端口限制或累计Store数超过SQ端口限制时,从该指令开始截断分发。前缀计数器的延迟为级门(,约3级门),约75 ps@7nm,不在分发阶段的关键路径上。
LSQ的可扩展性挑战
随着处理器乱序窗口不断增大(ROB从64项增长到500+项),LSQ的大小也需要同步增长。但LSQ的CAM结构面积和功耗与条目数的平方成正比——条目数翻倍时,CAM面积约增长4倍(因为每个条目都需要与所有其他条目进行匹配)。这使得大型LSQ的设计面临严峻的可扩展性挑战。
CAM面积的平方增长问题
对于一个项的CAM,每项有位比较宽度:
存储面积 (线性增长)
比较器面积 (线性增长)
匹配线(match line)面积 (线性增长,但匹配线延迟)
搜索线(search line)面积 (线性增长,但驱动能力)
优先级编码器面积 (超线性增长)
虽然每个组成部分的面积只是线性或增长,但CAM的时序与成正比——匹配线的延迟随条目数线性增长,因为更多的比较器需要驱动更长的匹配线。对于一个128项CAM,匹配线延迟约0.3 ns@7nm;256项则约0.5 ns。在3 GHz频率(周期0.33 ns)下,128项CAM勉强可以在一个周期内完成搜索,但256项CAM需要两个周期——这直接增加了STLF和违规检测的延迟。
分段LSQ
为解决CAM的可扩展性问题,一种方案是将LSQ分为多个较小的段(segment)。每个段独立维护一个小型CAM,段之间通过简单的逻辑连接。
按年龄分段:将LSQ分为"年轻段"和"老段"。新分发的指令首先进入年轻段,提交后迁移到老段。STLF搜索先搜索年轻段(因为最年轻的匹配Store通常在年轻段中),如果年轻段没有匹配则搜索老段。这种方案将每次搜索的CAM大小减半,但增加了两段搜索的串行延迟。
按地址分段(Bank化):将LSQ按地址低位分为多个bank(如4个bank,以地址[4:3]选择bank)。STLF搜索只需搜索一个bank(根据Load的地址确定),功耗降低为。但如果Load地址的低位与Store地址的低位不同但高位相同(跨bank冲突),可能导致搜索遗漏——这需要额外的跨bank检测逻辑来保证正确性。
Bloom Filter辅助:为整个LSQ维护一个Bloom Filter摘要。搜索时先查Bloom Filter,如果报告"不存在"则跳过CAM搜索(节省功耗);如果报告"可能存在"则进行完整的CAM搜索。Bloom Filter的假阳性率需要足够低(5%),否则大多数搜索仍需要完整CAM搜索,Bloom Filter的功耗反而成为额外开销。
案例研究 4 — Apple M1/M2的大型LSQ
Apple M1的Firestorm核拥有业界最大的LSQ之一——Load Queue约130项、Store Queue约108项、ROB约630项。为支持如此大的LSQ,Apple可能使用了以下技术(根据专利和微架构分析推测):
分段Store Queue:将Store Queue分为推测段和已提交段,每段独立维护CAM。
Bank化Load Queue:按地址低位分为多个bank,降低每次违规检测的功耗。
优化的匹配线设计:使用动态逻辑(如Domino逻辑)代替静态逻辑,加速CAM匹配延迟。
乐观调度与回放:假设STLF总是在一个周期内完成,如果实际需要更多周期(如因为队列过大导致搜索延迟增加),通过回放机制处理。
Apple的设计哲学是宁可增加面积(M1的Firestorm核面积远大于同代的ARM Cortex-A78),也要最大化单核IPC。大型LSQ是实现超深乱序窗口和高MLP的关键之一。
LSQ的冲刷与回滚
当发生分支预测失败或Load违规时,LSQ需要进行回滚操作——释放所有比恢复点更年轻的Load Queue和Store Queue条目。回滚的实现方式取决于处理器是否使用检查点机制:
基于检查点的回滚:如果处理器在每个分支指令处保存了LQ/SQ尾指针的快照,则回滚只需将尾指针恢复为对应检查点的值。恢复点之后的所有条目被自然地标记为无效(因为它们的索引超出了新的尾指针范围)。这种方式非常快(1个周期),但需要为每个检查点存储额外的LQ/SQ尾指针值。
基于ROB的逐步回滚:从ROB尾部向恢复点方向逐条指令回收。每遇到一条Load或Store指令,释放其对应的LQ/SQ条目,并递减相应的尾指针。这种方式不需要额外的快照存储,但回滚速度取决于需要回收的指令数——在最坏情况下可能需要数十个周期。
大多数现代处理器使用基于检查点的回滚方式,因为LSQ尾指针的快照只需位位(假设LQ 128项、SQ 64项),每个检查点的存储开销很小。
Load回放机制
Load指令在执行过程中可能因多种原因需要回放(replay)——即重新进入发射队列并再次执行。回放机制是Load执行管道中处理各种异常情况的统一方式。触发Load回放的常见原因包括:
L1 Cache未命中:Load的地址在L1 Cache中未命中。在使用乐观调度(optimistic scheduling)的处理器中,调度器假设所有Load都会L1命中,提前唤醒依赖指令。当发现Cache未命中时,需要回放Load及其已被唤醒的依赖指令。
Bank冲突:Load访问的Cache Bank与同一周期的另一个Load或Store排空操作冲突。Load被推迟到下一个无冲突的周期。
TLB未命中:Load的虚拟地址在L1 TLB中未命中,需要查找L2 TLB或进行Page Table Walk。Load被推迟到TLB翻译完成后重新执行。
Store Buffer数据未就绪:STLF匹配成功但匹配的Store数据尚未就绪。Load等待Store数据就绪后重新读取。
STLF部分覆盖失败:STLF匹配但无法完成转发(部分覆盖场景且不支持合并)。Load等待Store排空后重新执行。
地址消歧结果变化:初始STLF查找未匹配(Load从Cache读取),但后续有一条更老的Store的地址计算完成且与Load地址匹配。Load需要用Store的数据重新执行。
回放机制的硬件实现通常采用一个回放队列(replay queue)——被回放的Load从执行管道退出后进入回放队列,等待回放条件满足后重新送入发射队列。一些设计将回放队列与发射队列合并——Load在发射队列中保持其条目不释放,直到确认执行成功后才释放。如果需要回放,Load直接在发射队列中被重新标记为"等待"状态。
乐观调度与回放的级联效应
在使用乐观调度(Optimistic Scheduling)的处理器中,调度器假设所有Load都会L1 Cache命中,提前唤醒Load的消费者指令(依赖于Load结果的指令)。消费者指令在Load的结果实际到达之前个周期就被发射(是Load从发射到结果写回的延迟,通常4周期)。如果Load确实L1命中,消费者指令在Load结果写回的同一周期就能从旁路网络获取数据,实现零等待。
但如果Load L1未命中,消费者指令已经被错误地发射——它们在Load结果预期写回的周期从旁路网络读取数据,但此时数据不可用。消费者指令的执行结果是错误的,需要被级联回放——不仅Load本身需要回放,所有已被唤醒的消费者(以及消费者的消费者)都需要回放。
级联回放的代价可能很高。考虑一个依赖链:
ld a1, 0(s0) # Load: L1未命中
add a2, a1, a3 # 消费者1: 依赖于a1
sll a4, a2, 2 # 消费者2: 依赖于a2
ld a5, 0(a4) # 消费者3: 依赖于a4(间接Load)如果第一条ld发生L1未命中:
add在ld发射后4周期被唤醒并发射,但ld结果不可用——add需要回放。sll在add发射后1周期被唤醒并发射——sll也需要回放。第二条
ld在sll发射后1周期被唤醒并发射——同样需要回放。总共4条指令需要回放。
在极端情况下,一次L1未命中可能导致数十条指令的级联回放。为减少级联回放的代价,一些设计使用保守唤醒策略——不假设所有Load都L1命中,而是等待Load实际命中后再唤醒消费者。这消除了级联回放,但增加了所有Load的有效延迟约12个周期(等待Cache命中确认)。
Intel采用了一种折中方案:延迟回放。当检测到L1未命中时,不立即回放消费者,而是等待L2 Cache的响应。如果L2命中(延迟12周期),则在L2数据返回时统一回放所有消费者。这样消费者只需要回放一次(而不是在L1未命中时回放一次、L2响应后再执行一次),减少了回放的总开销。
性能分析 6 — Load回放的频率与影响
在典型的工作负载中,Load回放的频率统计如下:
L1 Cache未命中导致的回放:约3%10%的Load(取决于工作集大小和Cache大小)。
Bank冲突导致的回放:约1%3%的Load(取决于访问模式和Cache Bank数)。
TLB未命中导致的回放:约0.1%1%的Load。
STLF相关回放:约0.5%2%的Load。
回放的代价是Load的有效延迟增加。一次回放通常增加35个周期的延迟(回放队列等待 + 重新发射 + 重新执行)。但更严重的代价是级联回放——如果处理器使用乐观调度,已被唤醒的依赖指令也需要被取消和重新调度,形成一连串的回放。Intel的处理器中,一次L1 Cache未命中可能导致510条依赖指令被级联回放。
Load违规检测
推测式消歧的前提是"先推测后验证"——当Load推测执行后,硬件必须在Store地址就绪时检测是否存在Load违规(Load Order Violation)。违规意味着Load在先序Store之前执行,且两者的地址存在冲突——Load从Cache读取了过期的值,而不是Store要写入的新值。
违规检测的触发条件
违规检测的基本原理是:当一条Store的地址计算完成后,将Store的地址广播到Load Queue的所有项,搜索满足以下三个条件的Load:
Load的状态为"已执行"(Executed = 1)——未执行的Load没有违规问题,因为它们尚未从Cache读取数据。
Load比该Store更年轻(在程序序中位于Store之后)——如果Load比Store更老,那是Store需要等待Load,不存在违规。
Load的地址与Store的地址存在重叠——只有地址冲突才构成违规。
如果找到这样的Load,则发生了违规——该Load在Store之前执行,可能读到了错误的值。
违规检测的CAM搜索过程
违规检测的搜索过程可以用以下电路逻辑描述。对于Load Queue中的每个条目,并行计算:
其中:
:通过SQ尾指针快照或ROB索引比较判断Load是否比Store更年轻。
:比较Load和Store的物理地址低位是否匹配(通常比较PA[19:3]或PA[11:3],具体取决于设计选择的位宽)。
所有条目的信号形成一个位的"违规向量"。如果向量中有任何一位为1,则触发违规恢复。
选择最老的违规Load
当违规向量中有多个位为1时(多条Load同时违规),需要选择其中最老的违规Load。这是因为冲刷操作是从违规点开始冲刷所有后续指令——如果只冲刷最年轻的违规Load,中间的指令可能也依赖于错误的Load结果,同样需要重新执行。从最老的违规Load开始冲刷可以一次性修复所有问题。
选择最老的违规Load需要一个优先级编码器——按年龄从老到年轻扫描违规向量,输出第一个为1的位的索引。对于一个128项的Load Queue,这是一个128-to-7优先级编码器。
违规检测的伪代码如算法算法 36.1所示。
违规检测的完整时序示例
下面通过一个完整的时序示例展示违规检测的工作过程。假设处理器有4条飞行中指令:
I1: add s0, a0, a1 # 计算Store的基址(需要等待a0)
I2: sd a2, 0(s0) # Store: M[s0] = a2(地址依赖于I1)
I3: ld a3, 0(s1) # Load: a3 = M[s1](s1已就绪)
I4: add a4, a3, a5 # 使用Load结果(依赖于I3)假设s0和s1指向同一地址0x2000。
周期:I1I4已分发到发射队列。I2的地址依赖于I1的结果s0,尚未就绪。I3的基址s1已就绪。
周期:I3被推测发射执行。AGU计算出Load地址 = 0x2000。同时查询Store Queue中的先序Store(I2),发现I2的地址尚未就绪(Address Valid = 0)。推测式消歧假设I3与I2不冲突。
周期:I3访问L1 Cache,读取地址0x2000的值(旧值0xOLD)。Load完成,结果写入a3。I3在Load Queue中被标记为Executed = 1,记录地址 = 0x2000。
周期:I4被唤醒(a3已就绪),开始执行add计算。
周期:I1执行完成,s0 = 0x2000。I2被唤醒。
周期:I2的AGU计算地址 = s0 + 0 = 0x2000。地址写入Store Queue条目,设置Address Valid = 1。同时,地址0x2000被广播到Load Queue进行违规检测。
周期(违规检测周期):Load Queue搜索发现:LQ中I3的条目满足所有违规条件——Valid=1, Executed=1, 比I2更年轻, 地址0x2000匹配。违规检测命中。
周期:触发违规恢复。冲刷I3(违规Load)及其后续指令I4。将前端取指PC重定向到I3的地址。更新Store Set预测器:将I3和I2的PC关联到同一SSID。
周期:前端重新取指I3和I4。
周期:I3再次被分发。此时Store Set预测器指示I3需要等待I2。查LFST得到I2的SQ编号。I3被标记为依赖于I2。
周期:I2的地址已经就绪(在第一次执行中已计算完成,现在I2的数据也已就绪)。I3被唤醒,检查I2的地址与自己的地址匹配——执行Store-to-Load转发,I3获得I2的正确数据a2。
周期:I3从Store Buffer获取正确数据,I4再次执行。流水线恢复正常。
总违规恢复代价:从(检测到违规)到(正确结果可用)= 11个周期。其中5周期用于冲刷和前端重启,6周期用于重新执行I3和I4。
违规检测的设计考量
检测的及时性
理想情况下,违规应在Store地址计算完成后的下一个周期就被检测到,以尽快触发恢复。延迟检测虽然不影响正确性,但会浪费更多的流水线资源——违规发生后到检测到之前的每个周期,处理器都在执行无用的指令。
在实际设计中,违规检测通常需要12个周期:第一个周期进行CAM搜索和年龄过滤,第二个周期进行优先级编码和恢复信号生成。一些设计将违规检测与Store的地址写入Store Queue在同一周期内并行完成——当Store的地址被写入SQ条目时,同一地址同时被广播到Load Queue进行搜索。
多Store同时完成的处理
在宽发射处理器中,可能有多条Store在同一周期完成地址计算。如果每条Store都需要搜索整个Load Queue进行违规检测,则需要多个CAM搜索端口——每增加一个端口,Load Queue的面积和功耗大约增加30%50%。
解决方案包括:
串行化:将多个Store的违规检测在不同周期进行。简单但增加检测延迟。
多端口CAM:Load Queue支持多个并行搜索端口。面积代价高但延迟低。
Bloom Filter预筛选:使用Bloom Filter(布隆过滤器)对Load Queue的地址进行摘要——先通过Bloom Filter快速筛选出可能冲突的Load项,然后只对这些项进行精确的CAM比较。Bloom Filter可能产生假阳性(增加不必要的精确比较),但不会产生假阴性(不会漏掉真正的冲突)。一个位的Bloom Filter,使用个哈希函数,对个元素的假阳性率约为。例如,对于条已执行Load,位、,假阳性率约为8%。
Bank化Load Queue:将Load Queue分为多个bank,不同Store的搜索可以发送到不同bank并行进行(如果它们的地址映射到不同bank)。
假违规的影响
类似于地址消歧中的假阳性,如果违规检测只比较地址的低位(例如低12位),可能产生假违规(false violation)——两个Load和Store实际上访问不同的地址,但低位碰巧相同。假违规不影响正确性(只是触发不必要的冲刷),但会降低性能。
假违规率的计算:如果比较低12位地址,假违规的概率约为。在一个Load Queue有64条已执行Load的场景中,每条Store完成地址计算时的假违规概率约为。如果每5条指令产生一条Store(约20%的指令是Store),则每333条Store中有5条触发假违规。以30周期的恢复代价计算,假违规的CPI开销约为,约3%的IPC损失——这已经不可忽略。
因此,比较位宽的选择需要平衡面积/功耗与假违规率。比较20位地址(假阳性率)可以将假违规降低到几乎为零,但需要更大的CAM比较器。
基于值的违规消除
一种优化方案是在违规检测时进行数据值比较——即使地址匹配(Load与先序Store访问同一地址),如果Store写入的值恰好等于Load已经从Cache读到的值,则Load的结果仍然是正确的,无需触发违规恢复。
这种优化称为值匹配违规消除(Value-Based Violation Avoidance)。其硬件实现需要:
Load Queue中存储Load读取的数据值(Data字段)。
当违规检测发现地址匹配后,进一步比较Store要写入的数据与Load Queue中记录的数据。
如果数据相同,取消违规信号——无需冲刷。
如果数据不同,正常触发违规恢复。
这种优化的收益取决于"地址冲突但值相同"的频率。在实际程序中,以下情况会产生此类场景:
冗余Store:编译器未能消除的冗余Store指令——Store写入的值与该地址的当前值相同(例如条件赋值中无论哪个分支都赋相同的值)。
初始化后的读取:一段内存被初始化为特定值(如0),Store再次写入相同的值,后续Load读取也得到相同的值。
循环不变量:循环中反复Store同一个不变的值到同一地址。
值匹配优化的硬件代价是Load Queue的每个条目增加一个数据字段(64128位),以及一组数据比较器。对于128项的Load Queue,增加64位数据字段意味着额外位1 KB的存储,以及128个64位比较器。这些额外面积通常被认为是值得的——一些学术研究表明,值匹配优化可以消除约10%20%的违规冲刷。
违规检测与STLF的协同
违规检测和STLF使用了相似但方向相反的CAM搜索:
STLF:Load的地址搜索Store Queue(SQ),查找地址匹配的先序Store。搜索方向:从Load向更老的Store搜索。
违规检测:Store的地址搜索Load Queue(LQ),查找地址匹配的后序已执行Load。搜索方向:从Store向更年轻的Load搜索。
两者的硬件可以部分共享——如果采用统一LSQ(Load和Store在同一个队列中),则只需要一套CAM比较器阵列,STLF和违规检测是同一个搜索操作的两个方面。但在分离队列设计中,STLF的CAM位于Store Queue中,违规检测的CAM位于Load Queue中,无法共享。
在时序上,STLF发生在Load执行阶段(Load需要数据来继续执行),是性能关键路径;而违规检测发生在Store地址计算完成后,不在Load的关键路径上。因此设计者通常将更多的优化投入到STLF的时序上,而对违规检测的延迟容忍度更高。
性能分析 7 — 违规检测的频率与代价
在SPEC CPU基准测试中,Memory Order Violation的频率统计如下:
激进推测(所有Load总是推测执行,无预测器):每100010000条指令发生一次违规。
带Store Set预测器:违规频率降低到每10万100万条指令一次。
每次违规的恢复代价约为2040个时钟周期(类似分支预测失败的代价)。
以每5000条指令一次违规、每次代价30周期计算,违规恢复的CPI额外开销为,即0.6%的CPI增加。这远小于保守式消歧15%25%的IPC损失,因此推测式消歧是净收益。
使用Store Set预测器后,违规频率降低到几乎可以忽略,CPI额外开销。
流水线冲刷与恢复机制
当检测到Load违规后,处理器需要冲刷流水线(Pipeline Flush / Pipeline Squash)并恢复到正确的状态。冲刷的范围和方式取决于微架构的实现。
完全冲刷(Full Pipeline Flush)
最简单的方案是冲刷所有比违规Load更年轻的指令——包括违规Load本身和所有后续指令。冲刷后,处理器从违规Load的PC重新开始取指和执行。这种方式实现简单,但代价最高——所有比违规Load更年轻的指令的工作都被浪费了。在一个拥有256项ROB的处理器中,如果违规Load处于ROB的中部,则约128条指令的执行结果将被丢弃。
完全冲刷的流程如下:
标记违规Load为"需要重新执行"。
释放ROB中所有比违规Load更年轻的指令项。
恢复寄存器重命名表到违规Load分发时的快照(如果有快照机制)或通过ROB逆向恢复。
释放发射队列中所有相关指令。
恢复Load Queue和Store Queue的指针到违规Load的位置。
将违规Load的PC送入前端,重新开始取指。
更新内存依赖预测器——将违规的Load-Store对记录到Store Set预测器中。
完全冲刷的时序分析
完全冲刷的恢复延迟可以分解为以下几个组成部分:
违规检测延迟(12周期):从Store地址广播到违规信号产生。
恢复信号传播(1周期):违规信号从Load Queue传播到ROB、重命名表和前端。
状态恢复(13周期):恢复寄存器重命名表。如果使用检查点机制(checkpoint),恢复只需1个周期(加载快照);如果使用ROB逆向恢复(walking back the ROB),可能需要多个周期。
前端重启(35周期):将新的PC发送到I-Cache,等待指令取回。前端流水线需要经过取指、预译码、译码等阶段才能产生新的有效指令。
后端填充(510周期):新指令从分发到执行再到产生有效结果,需要经过分发、发射、执行等阶段。
总恢复延迟 = 1 + 1 + 1 + 4 + 7 1420周期(使用检查点的快速恢复),或2040周期(使用ROB逆向恢复的慢速恢复)。
在恢复延迟期间,流水线不产生任何有效的指令提交。如果处理器的理想IPC为4(每周期提交4条指令),则一次违规恢复浪费的指令等效于条指令的执行时间。
选择性重放(Selective Replay)
更精细的方案是只重新执行违规Load及其依赖链上的指令,而不冲刷整个流水线。这种方案的代价远低于完全冲刷,但硬件实现极为复杂——需要追踪指令之间的依赖关系,并选择性地使部分指令的结果无效。
选择性重放需要以下硬件支持:
依赖追踪矩阵:记录每条飞行中指令依赖于哪些其他指令。当违规Load被标记为无效时,所有直接或间接依赖于它的指令也需要被标记为无效并重新执行。
选择性无效化:能够只使ROB中特定指令的结果无效,而不影响其他指令。这需要每条指令有独立的"有效/无效"标志,而不是简单地截断ROB尾部。
重放调度:被无效化的指令需要重新进入发射队列并重新调度执行,而不影响其他正常执行的指令。
这些硬件的面积和复杂度与ROB大小的平方成正比(依赖追踪矩阵为),对于256项ROB来说开销极大。
违规恢复与分支预测失败恢复的复用
违规恢复与分支预测失败恢复在机制上高度相似——两者都需要从某个点开始冲刷流水线,恢复寄存器映射表,并从正确的PC重新开始取指。因此,许多处理器将两者的恢复逻辑统一实现:
两种恢复都使用ROB中的检查点(checkpoint)或快照来恢复寄存器重命名表。
两种恢复都需要清空发射队列中从违规/错误预测点之后的所有指令。
两种恢复都需要释放Load Queue和Store Queue中对应的项。
两种恢复都需要重定向前端的取指PC。
区别在于:分支预测失败时,恢复到的PC是分支的正确目标地址;而Load违规恢复时,恢复到的PC是违规Load自身的地址(Load需要重新执行)。此外,Load违规恢复可能还需要同时更新内存依赖预测器的状态,以减少同一违规的再次发生。
值得注意的是,在同一周期内可能同时检测到Memory Order Violation和分支预测失败。当两者同时发生时,处理器选择恢复到更老的事件点——如果违规Load比错误预测的分支更老,则执行违规恢复;反之则执行分支恢复(因为分支恢复会自然地冲刷掉违规Load及其后续指令,无需额外处理)。
在Intel的性能计数器中,Memory Order Machine Clear对应的事件为MACHINE_CLEARS.MEMORY_ORDERING。通过perf或VTune等工具监控此事件,可以诊断程序中是否存在频繁的内存序违规,并指导编译器或程序员优化数据访问模式。
设计权衡 5 — 冲刷范围与恢复代价
完全冲刷(从违规点冲刷所有后续指令):
恢复代价:约2040周期,具体取决于流水线深度和前端重启延迟。
硬件复杂度:较低,可复用分支错误预测的恢复逻辑。
适用场景:违规频率极低(每万条指令一次)时,简单的完全冲刷足够。
选择性重放(只重新执行违规Load及其依赖链):
恢复代价:约515周期,只影响违规Load的依赖链。
硬件复杂度:极高,需要完整的依赖追踪和选择性无效化机制。面积开销约为ROB面积的30%50%。
适用场景:违规频率较高的工作负载可以获益,但在大多数情况下,硬件复杂度不值得。
大多数商业处理器选择完全冲刷,因为违规频率足够低,选择性重放的额外硬件复杂度不划算。
Spectre v4(推测存储旁路)
Spectre v4(Speculative Store Bypass, SSB)是2018年发现的一种微架构侧信道攻击,它利用了推测式Memory Disambiguation的安全隐患。Spectre v4也被称为Spectre Variant 4或CVE-2018-3639。
Spectre v4的发现者是Jann Horn(Google Project Zero)以及微软安全团队。与其他Spectre变体一起,Spectre v4促使整个处理器行业重新审视推测执行的安全边界,并推动了一系列硬件和软件层面的缓解措施。
攻击原理
Spectre v4的攻击利用了推测式消歧的核心机制。攻击场景如下:
攻击者安排一条Store指令写入某个已知地址,写入一个"安全值"。
紧跟一条Load指令从同一地址读取数据。
由于推测式消歧,如果Store的地址尚未计算完成,Load可能推测性地绕过Store,从Cache中读取地址的旧值(可能是敏感数据)。
Load读取的值被后续的推测执行指令用于计算数组索引等操作,通过Cache侧信道泄露出去。
虽然最终违规检测会发现Store和Load的地址冲突并触发冲刷,但此时Cache侧信道效应已经产生——敏感数据的影子已经留在了Cache的时序行为中。
用伪代码表示:
// 攻击者控制的代码
*ptr = safe_value; // Store: 写入安全值
index = *ptr; // Load: 推测性地绕过Store,读到旧值(敏感数据)
temp = array[index * 64]; // 用敏感数据作为索引访问数组(侧信道泄露)攻击的关键在于步骤3:当Store的地址尚未从AGU输出时,推测式消歧器判断"不知道这条Store会写到哪里,先假设与Load不冲突"。Load于是从Cache读取地址的当前值——这可能是之前由其他代码写入的敏感数据(如内核密钥、其他进程的数据)。Store的意图是用safe_value覆盖这个敏感数据,但由于推测执行,Load在Store完成之前就读到了敏感值。
与Spectre v1的区别
Spectre v1利用分支预测的推测执行来访问越界内存,而Spectre v4利用存储器消歧的推测执行来绕过Store并读取过期值。两者都利用了推测执行期间的Cache侧信道效应来泄露信息。
Spectre v4在某种意义上比v1更难防御,因为它不依赖于分支——即使程序中没有条件分支,只要有Store后跟Load到同一地址的模式,就可能被利用。在JIT编译的场景中(如JavaScript引擎),攻击者可以构造任意的Store-Load序列,使得攻击更加灵活。
攻击的前提条件
Spectre v4攻击需要以下条件同时满足:
处理器使用推测式存储器消歧(几乎所有现代高性能处理器都满足)。
攻击者能够控制Store的地址和Load的地址,使它们指向同一内存位置。
Store的地址计算延迟足够长,使得Load能够在Store地址就绪前推测执行。
攻击者能够通过Cache侧信道(如Flush+Reload、Prime+Probe)观察推测Load所访问的数据值。
目标地址中存储着攻击者希望泄露的敏感数据(如内核数据、其他进程的数据)。
硬件缓解措施
处理器厂商提供了多种硬件级别的缓解措施:
禁用推测存储旁路(Speculative Store Bypass Disable, SSBD):通过设置一个控制寄存器位(如x86的
IA32_SPEC_CTRL寄存器的SSBD位),可以禁用推测式Store绕过。启用SSBD后,所有Load必须等到所有先序Store的地址就绪后才能执行——实质上退化为保守式消歧。这提供了安全性但牺牲了性能(约2%8%的IPC下降)。选择性禁用:更精细的方案是只对跨安全域边界(如内核代码、沙箱代码)的存储器指令禁用推测旁路,而对应用程序内部的代码保持推测执行。这需要操作系统在上下文切换时设置/清除SSBD位。
微码更新:一些处理器通过微码更新引入新的屏障指令(如x86的专用
LFENCE用法),允许软件在关键位置插入屏障来阻止推测存储旁路。
RISC-V中的Spectre v4防御
RISC-V架构尚未有专门的Spectre v4缓解指令,但可以通过以下方式实现防御:
FENCE指令:在Store和Load之间插入FENCE指令,强制Store在Load之前完成。但FENCE的粒度太粗——它会暂停所有存储器指令的重排,不仅仅是目标Store-Load对。
Zicbom扩展中的屏障:RISC-V的Cache管理扩展可能提供更精细的屏障机制。
微架构特定控制:处理器实现者可以提供自定义的CSR位来控制推测存储旁路,类似于x86的SSBD。
案例研究 5 — Spectre v4的影响与缓解
Spectre v4的实际影响因处理器和工作负载而异:
性能影响:全面启用SSBD(禁用推测存储旁路)的性能代价约为2%8%,取决于工作负载中Load-Store对的密度和依赖关系。在数据库和Web服务器等Load密集型工作负载中影响较大。
选择性缓解:Linux内核从4.17版本开始支持per-process的SSBD控制。只有明确请求的进程(如运行不受信任的JavaScript代码的浏览器沙箱)才启用SSBD。
硬件改进:从Intel Ice Lake(2019年)和AMD Zen 3(2020年)开始,处理器在硬件中实现了更精细的推测控制——可以在不完全禁用推测存储旁路的情况下阻止跨安全域的泄露。具体机制未公开,但推测涉及在安全域切换时刷新Store Buffer状态。
Spectre v4是一个典型的"性能与安全的权衡"案例。推测式消歧对IPC的贡献无可替代,但它引入的安全漏洞需要通过软硬件协同的方式进行缓解。处理器设计者需要在微架构层面考虑安全边界,而不仅仅是性能优化。
存储器消歧与内存序模型
推测式消歧与处理器的内存序模型(Memory Ordering Model)密切相关。在x86的TSO(Total Store Ordering)模型下,Load-Load和Store-Store的顺序必须保持,但Store可以在后续Load之后可见(即允许Store-Load重排序)。这意味着x86处理器可以推测性地将Load提前到Store之前执行(利用推测式消歧),但不能将Load提前到其他Load之前执行(除非地址不同)。
在ARM和RISC-V的弱序模型(Weak Ordering)下,限制更少——处理器可以自由地重排存储器指令(除非有显式的fence指令),这给了硬件更大的优化空间。但弱序模型也意味着程序员需要更仔细地使用同步原语。
从微架构的角度来看,推测式消歧在TSO模型下的实现需要额外的约束:Load不能"绕过"更老的Load(即Load-Load顺序必须保持)。这通常通过以下方式实现:
Load按照发射顺序访问Cache——即使两条Load同时就绪,也按年龄序依次发射。
或者,允许Load乱序发射,但在提交时验证Load-Load顺序的正确性——如果一条更老的Load在更年轻的Load之后读取了不同的值(因为中间有其他核的写入),则触发违规。
Intel的处理器采用后一种方式(称为Memory Order Buffer机制),在允许Load乱序执行的同时保证TSO语义。这进一步增加了Load Queue违规检测的复杂度——不仅需要检查Load-Store冲突,还需要检查Load-Load的一致性。
Load-Load违规的检测
在TSO内存模型下,Load-Load违规的检测需要额外的机制。当一条Load (较老)在另一条Load (较年轻)之后执行时,如果在执行后、执行前这段时间内,另一个核写入了和读取的地址,则和可能读到不一致的值——读到旧值,读到新值。这违反了TSO的Load-Load顺序保证。
检测Load-Load违规的方法是:当执行完成后,检查从执行到执行期间是否有其他核的写入(invalidation)到达了或读取的Cache行。如果有,则触发Load-Load违规恢复。
这需要Load Queue中额外记录每条Load读取的Cache行的一致性状态,并在收到其他核的invalidation请求时搜索Load Queue中是否有已执行但未提交的Load读取了被invalidated的Cache行。这个"snoop-triggered violation check"是Load Queue功耗和面积的又一个重要贡献者。
Snoop触发的Load Queue搜索
在多核处理器中,当其他核修改了一个Cache行(发送invalidation请求),本核需要检查Load Queue中是否有未提交的Load读取了该Cache行。这个搜索过程的详细机制如下:
其他核的Store操作通过一致性协议(如MESI/MOESI)发送invalidation请求到本核的L1 Cache控制器。
L1 Cache控制器在处理invalidation的同时,将被invalidated的Cache行地址发送到Load Queue的CAM进行搜索。
Load Queue搜索所有满足以下条件的条目:(a) Valid = 1,(b) Executed = 1(已执行),(c) 地址匹配(比较物理地址的Cache行地址部分,即PA[47:6])。
如果找到匹配的条目,说明该Load读取的数据可能已经过时(被其他核修改)。触发Snoop违规。
Snoop违规的恢复方式与普通Load-Store违规相同——从最老的违规Load开始冲刷流水线。
Snoop触发的Load Queue搜索增加了Load Queue的端口需求——除了正常的Store地址搜索端口外,还需要额外的Snoop搜索端口。Snoop请求的频率取决于工作负载的共享数据量和核间通信模式。在高度共享的工作负载(如数据库事务处理)中,Snoop频率可能每1000条指令发生一次;在计算密集的单线程工作负载中,Snoop频率则低得多。
弱序模型下的简化
在RISC-V和ARM的弱序内存模型下,硬件不需要保证Load-Load的顺序(除非有显式的fence指令),因此:
不需要Snoop触发的Load Queue搜索——即使其他核修改了本核已执行Load读取的Cache行,由于弱序模型允许Load读到"旧值",这不构成违规。
Load Queue的面积和功耗可以减小——不需要额外的Snoop搜索端口和Cache行地址比较器。
Load可以完全乱序发射和执行,无需年龄序约束。
这是弱序模型在硬件实现上的一个重要优势——Load Queue的设计可以更简单、更高效。但程序员需要在需要顺序保证的地方显式插入fence指令,将一致性保证的责任从硬件转移到软件。
在RISC-V中,fence r,r指令强制先序Load在后序Load之前完成(Load-Load顺序),fence r,w指令强制先序Load在后序Store之前完成(Load-Store顺序)。当处理器遇到这些fence指令时,需要暂时恢复到类似TSO的行为——等待所有先序Load完成后才允许后序操作执行。
FENCE指令的微架构实现
FENCE指令在乱序处理器中的实现需要仔细处理,因为它强制施加了顺序约束。RISC-V的fence指令具有精细的语义——可以分别指定哪些类型的操作(读/写、输入/输出)需要排序。不同的FENCE类型需要不同的硬件处理:
fence w,r(Store-Load顺序)**:这是TSO模型中唯一被允许重排的操作对。硬件实现需要确保FENCE之前的所有Store排空到L1 Cache后,FENCE之后的Load才能执行。具体实现:(1) FENCE指令被分发到Store Queue,占据一个SQ条目。(2) 在FENCE提交时,等待所有先序Store排空到Cache。(3) 排空完成后,FENCE提交,后续Load可以正常执行。fence r,r(Load-Load顺序)**:需要确保FENCE之前的所有Load在FENCE之后的Load执行之前完成。硬件实现:(1) FENCE指令被分发到Load Queue。(2) FENCE之后的Load不能发射,直到FENCE之前的所有Load都已执行完成。(3) 这可以通过在Load Queue中设置一个"屏障"标记来实现——FENCE之后的Load在分发时记录FENCE的位置,发射前检查FENCE之前的所有LQ条目是否都已Executed。fence w,w(Store-Store顺序):在大多数实现中,Store-Store顺序已经由Store Buffer的FIFO排空保证,因此fence w,w**通常是NOP——不需要额外的硬件操作。fence r,w(Load-Store顺序)**:需要确保FENCE之前的所有Load在FENCE之后的Store提交之前完成。硬件实现较为简单——在ROB的提交逻辑中检查:如果正在提交的Store之前有一条FENCE r,w指令,则确保该FENCE之前的所有Load都已在Load Queue中标记为Executed。fence iorw,iorw(完全屏障)**:这是最强的FENCE类型,相当于x86的MFENCE。需要排空Store Buffer中的所有Store,并等待所有飞行中的Load完成。完全屏障的延迟最长,可能达到3060周期(取决于Store Buffer中待排空的Store数量)。
FENCE指令的性能代价取决于屏障的强度和此时Store Buffer/Load Queue中的飞行中指令数量。在性能敏感的代码中,应使用最弱满足需求的FENCE类型。例如,在RISC-V中实现一个release-store只需要fence rw,w(而不是完全屏障),代价远低于fence iorw,iorw。
性能分析 8 — FENCE指令的性能代价
不同FENCE类型的典型延迟(假设64项Store Buffer,半满状态):
fence w,w:01周期(NOP或接近NOP)。fence r,r:515周期(等待先序Load完成)。fence w,r:1540周期(等待Store Buffer排空)。fence iorw,iorw:2060周期(排空Store Buffer + 等待Load完成)。
在SPEC CPU单线程基准测试中,FENCE指令的频率很低(通常每万条指令一次),因此其性能影响可忽略。但在多线程同步密集的工作负载(如数据库锁管理器、消息队列)中,FENCE频率可能每1001000条指令出现一次,此时FENCE的延迟会显著影响性能。
原子指令与LSQ的交互
RISC-V的原子指令(AMO指令和LR/SC指令)对LSQ有特殊的处理需求。
LR/SC(Load Reserved / Store Conditional):
lr.w指令执行一个Load,同时在硬件中设置一个保留集(reservation set)——记录被读取的Cache行地址和保留有效标志。后续的
sc.w指令检查保留集是否仍然有效(即该Cache行在LR和SC之间没有被其他核修改或被本核的中断打断)。如果有效,SC成功执行Store并返回0;否则SC失败并返回非零值。LR指令在Load Queue中需要额外的标记,表明它是一条Load Reserved。当Load Queue收到Snoop invalidation命中LR读取的Cache行时,除了正常的违规检测外,还需要清除保留标志——这确保后续的SC将会失败。
SC指令的Store只有在保留有效时才会被实际执行和提交。SC在Store Queue中需要条件提交逻辑——提交时检查保留标志,如果保留已被清除,则SC被视为失败,不执行Store操作,只将失败码写入目的寄存器。
AMO(Atomic Memory Operation)指令:
AMO指令(如
amoadd.w)在微操作层面被拆分为:Load(读取旧值) ALU操作(计算新值) Store(写入新值)。这三个微操作必须原子执行——在Load和Store之间,不允许其他核修改同一Cache行。原子性通常通过Cache行锁定实现——AMO的Load微操作在读取Cache行后,将该Cache行锁定为Exclusive状态,阻止其他核在ALU操作和Store完成之前获取该行。
AMO指令在LSQ中占据一个Load Queue条目和一个Store Queue条目。两个条目通过ROB索引关联,确保它们作为一个原子单元被处理。
存储器指令加速的设计方法论
存储器指令加速涉及多个互相关联的设计决策。设计者需要在以下维度上进行权衡:
推测的激进程度:从完全保守(所有Load等待所有先序Store)到完全激进(所有Load立即推测执行),中间有大量的折中方案。更激进的推测带来更高的IPC但更频繁的违规;更保守的策略减少违规但增加Load延迟。Store Set预测器提供了一种动态调整激进程度的方式——对历史上不冲突的Load激进推测,对历史上冲突的Load保守处理。
LSQ的大小:更大的LSQ支持更深的乱序窗口和更高的MLP,但面积和功耗更高,CAM搜索延迟更长。设计者需要根据目标工作负载的Cache未命中率和存储器指令密度来确定最优的LSQ大小。
STLF的复杂度:支持更多的转发场景(部分覆盖、多Store合并)可以减少Store Forwarding Stall,但增加了转发逻辑的面积和延迟。大多数处理器选择只支持完全覆盖的转发,将部分覆盖场景交给"等待Store排空"的回退路径处理。
违规检测的精度:更宽的地址比较(更多位数)减少假违规但增加CAM面积。更窄的地址比较减少面积但增加假违规的IPC代价。设计者需要根据程序的地址分布模式来确定最优的比较位宽。
内存序模型的影响:TSO模型需要额外的Load-Load顺序保证机制(Snoop触发的LQ搜索),增加了面积和功耗。弱序模型简化了硬件但增加了软件复杂度。RISC-V采用弱序模型,为硬件设计提供了更大的优化空间。
安全性约束:Spectre v4等攻击要求在安全关键场景下禁用或限制推测式消歧。设计者需要提供可配置的安全控制机制(如SSBD位),并将性能损失限制在可接受范围内。
RISC-V处理器的LSQ设计考量
RISC-V的ISA特性对LSQ设计有一些特定的影响:
弱序模型的简化优势
RISC-V的RVWMO(RISC-V Weak Memory Ordering)模型比x86的TSO模型弱得多——默认情况下不保证任何操作之间的顺序(除了对同一地址的数据依赖)。这为硬件提供了显著的简化优势:
不需要Load-Load顺序保证:在x86中,Load Queue需要处理Snoop触发的Load-Load违规检测。在RVWMO中,这个机制完全不需要,可以移除Load Queue中的Snoop搜索端口和相关的CAM比较逻辑。这减少了Load Queue约15%20%的面积和20%30%的动态功耗。
Load可以完全乱序执行:不需要保持Load之间的年龄序。两条Load同时就绪时,调度器可以按任意顺序发射它们,增加了调度灵活性和Load端口利用率。
Store-Load重排无需特殊处理:TSO模型只允许Store-Load重排,其他重排被禁止。RVWMO允许所有类型的重排(除同地址依赖),因此Store Buffer的设计更简单——不需要额外的机制来保证Store-Store或Load-Store顺序。
但弱序模型也带来了额外的FENCE处理需求——程序员需要在需要顺序保证的地方显式插入FENCE指令,硬件需要正确实现FENCE的语义。好消息是FENCE指令的频率很低(通常每万条指令一次),对性能影响可忽略。
自然对齐要求
RISC-V的基本ISA(不含Zam扩展)要求所有Load/Store的地址按自然对齐:lw/sw要求4字节对齐,ld/sd要求8字节对齐。非对齐访问触发Address Misaligned异常。
自然对齐保证对LSQ设计有以下好处:
不需要处理跨Cache行访问:自然对齐的访问保证不会跨越Cache行边界(假设Cache行8字节,几乎总是如此)。因此Load单元不需要跨Cache行的拆分/合并逻辑。
简化部分转发:自然对齐保证了Store和Load的地址总是自然对齐的边界上。例如,一个4字节Store的地址总是4的倍数,一个8字节Load的地址总是8的倍数。这限制了部分转发可能出现的场景,简化了字节使能匹配和数据对齐逻辑。
简化地址比较:对于自然对齐的访问,地址比较可以忽略最低的位,进一步减少比较器位宽。
如果处理器实现了Zam扩展(支持非对齐访问),则需要添加上述所有复杂的处理逻辑。这是一个面积-兼容性权衡——支持非对齐访问增加了约10%20%的Load单元面积(主要是跨Cache行拆分/合并逻辑),但使得处理器可以运行未对齐数据的遗留代码。
向量存储器指令的影响
RISC-V的V扩展(向量扩展)引入了向量Load/Store指令(如vle32.v、vse32.v),这些指令可以一次访问多个连续或非连续的内存位置。向量Load/Store对LSQ设计的影响包括:
宽访问:一条向量Load可能访问字节的数据(如VLEN=256时为32字节)。这个宽度可能超过Store Buffer的数据字段宽度,需要将向量Load拆分为多个微操作,每个微操作作为独立的Load Queue条目。
Stride/Scatter访问:向量的strided和indexed访问模式(如
vlse32.v、vluxei32.v)访问不连续的地址。每个元素的地址不同,需要独立地进行STLF搜索和违规检测。这大幅增加了CAM搜索的次数和功耗。Segment Load/Store:向量的segment访问模式一次加载多个结构体字段,涉及多个不同的地址。每个字段需要独立的LSQ条目和STLF查找。
LQ/SQ资源消耗:一条向量Load/Store微操作可能占用多个LQ/SQ条目(每个元素或每个Cache行一个条目),快速消耗LSQ资源。对于大VLEN的实现,LSQ大小可能需要额外增大。
这些挑战使得向量存储器指令的微架构实现比标量存储器指令复杂得多。一些设计选择将向量Load/Store与标量Load/Store分离到不同的执行管道中,使用独立的LSQ结构,避免向量操作对标量性能的影响。
设计提示
存储器指令加速的设计是一个多目标优化问题——IPC、面积、功耗、时序和安全性需要同时满足。以下是一些经验性的设计指导原则:
LQ大小 ROB大小 0.3(确保MLP不受LQ限制)。
SQ大小 ROB大小 0.15(确保Store排空不成为瓶颈)。
STLF延迟 L1 Cache命中延迟(确保STLF不增加Load延迟)。
违规检测延迟 2个周期(减少违规检测到恢复之间的浪费)。
Store Set预测器大小 LQ + SQ总面积的10%(预测器不应过大)。
FENCE延迟在弱序模型中由显式fence指令承担,但应提供最细粒度的fence类型以减少不必要的序列化开销。
面积与功耗预算
存储器指令加速的各个硬件结构占用了乱序执行引擎中显著的面积和功耗预算。本节对各结构的面积和功耗进行汇总分析,帮助设计者理解存储器子系统在整个处理器中的开销占比。
各结构的面积估算
表表 36.9列出了一个典型高性能乱序处理器(6发射,256项ROB,128项LQ,64项SQ)中存储器相关结构的面积估算(基于7nm工艺)。
| 结构 | 存储位数 | 面积(mm) | 备注 |
|---|---|---|---|
| Store Queue(64项) | 17K | 0.03 | 含CAM比较器 |
| Load Queue(128项) | 27K | 0.05 | 含违规检测CAM |
| Store Set预测器 | 40K | 0.02 | SSIT + LFST |
| STLF转发逻辑 | – | 0.01 | 优先级编码+数据MUX |
| Store合并缓冲 | 0.5K | 0.01 | 1个Cache行大小 |
| WCB(10项) | 5K | 0.01 | Write Combining |
| 总计 | 90K | 0.12 |
存储器指令相关结构的面积预算(7nm工艺估算)
作为对比,同一处理器中的L1 Data Cache(32KB,8路组相联)面积约0.150.20 mm,ROB(256项)面积约0.050.08 mm。因此,存储器指令相关结构的总面积约占L1 D-Cache面积的60%80%,占整个执行引擎面积的15%20%。
面积的主要贡献者是Load Queue和Store Queue的CAM部分。CAM结构的面积通常是等容量SRAM的23倍,因为每个条目除了存储单元外还需要比较器逻辑。减少CAM面积的方法包括:(1) 减少CAM比较的位宽(用更少位数的地址比较);(2) 使用bank化结构减少同时活动的比较器数量;(3) 使用混合CAM/SRAM结构——将频繁搜索的字段(地址低位)放在CAM中,不频繁搜索的字段(数据、控制位)放在SRAM中。
功耗分析
存储器指令相关结构的功耗由静态功耗和动态功耗两部分组成。
静态功耗与晶体管数量和工艺节点的漏电流成正比。在7nm工艺中,每个SRAM位元的静态功耗约0.10.3 nW。对于总共90K位的存储器指令结构,静态功耗约1030 W——相对于整个核心的510 W功耗来说微不足道。
动态功耗是主要关注点,取决于搜索频率和每次搜索的活动因子:
STLF搜索:每条Load执行时搜索Store Queue一次。假设每周期2条Load,则SQ的CAM每周期搜索2次。64项SQ、45位比较,每次搜索的动态功耗约35 mW@3 GHz。
违规检测搜索:每条Store地址计算完成时搜索Load Queue一次。假设每周期1条Store完成地址计算,则LQ的CAM每周期搜索1次。128项LQ、20位比较,每次搜索的动态功耗约48 mW@3 GHz。
Snoop搜索(仅TSO模型):每次收到invalidation请求搜索Load Queue一次。频率取决于核间通信,约每1001000周期一次。功耗与违规检测搜索相当,但因频率低,平均功耗贡献很小。
Store Set预测器访问:每条Load/Store在分发时访问SSIT和LFST各一次。频率约每周期23次。SSIT为SRAM结构(非CAM),功耗约12 mW@3 GHz。
存储器指令结构的总动态功耗约1530 mW@3 GHz,约占核心总功耗的0.3%0.5%。虽然占比不高,但在追求极致能效的移动处理器中,每一点功耗节省都很重要。因此,功耗优化技术(如bank化CAM、Clock Gating、按需搜索)在实际设计中被广泛采用。
功耗优化技术
以下是减少存储器指令结构功耗的主要技术:
Clock Gating
当Store Queue中的某些条目无效(Valid = 0)时,这些条目的CAM比较器不需要工作。通过对每个条目的比较器进行时钟门控(Clock Gating),可以在条目无效时关闭其比较器的时钟,消除不必要的翻转功耗。
在实际中,Store Queue的使用率平均约50%70%(取决于工作负载的Store密度)。因此Clock Gating可以减少约30%50%的CAM动态功耗。
Clock Gating的实现需要为每组比较器添加一个门控时钟单元(ICG, Integrated Clock Gate),面积开销约5%10%的比较器面积。这是一个非常划算的功耗-面积权衡。
Bank化搜索
将LSQ按地址低位分为多个bank,每次搜索只激活一个bank(由搜索地址的低位决定)。例如,将64项Store Queue分为4个bank(每bank 16项),按地址PA[4:3]选择bank。每次STLF搜索只激活16项的CAM比较,功耗降低为原来的。
Bank化搜索的正确性要求:同一Cache行内的不同字节必须映射到同一bank。由于Cache行大小为64字节,使用PA[4:3]作为bank选择可以保证同一Cache行的所有字节映射到同一bank(,不跨bank)。但如果使用PA[5:4]作为bank选择,则一个64字节Cache行可能跨越两个bank——当Load和Store恰好访问同一Cache行的不同half时,需要搜索两个bank,增加了控制逻辑的复杂度。
预过滤搜索
在CAM搜索之前,先用一个简单的预过滤器快速判断搜索是否有可能命中。预过滤器可以是一个小型Bloom Filter(64256位),记录当前Store Queue中所有有效条目的地址摘要。如果Bloom Filter报告"不存在",则跳过整个CAM搜索——直接从Cache读取数据即可。
在实际工作负载中,约85%95%的Load不会匹配Store Queue中的任何条目。因此,Bloom Filter可以消除85%95%的CAM搜索,功耗节省显著。Bloom Filter本身的功耗(一次哈希计算 + 一次位数组查找)远小于完整的CAM搜索。
分时复用搜索端口
如果Load和Store的地址计算不在同一周期完成(通常如此),可以在不同周期复用同一个CAM搜索端口。例如,STLF搜索和违规检测搜索可以共用Load Queue的CAM端口——在Load执行的周期用于STLF(搜索SQ),在Store地址完成的周期用于违规检测(搜索LQ)。这种分时复用减少了CAM的物理端口数,降低了面积和功耗。
各代处理器的存储器子系统演进
表表 36.10总结了过去25年间主要处理器的存储器指令加速结构的演进。
| 处理器 | ROB | LQ | SQ | 消歧 | STLF |
|---|---|---|---|---|---|
| Alpha 21264 (1998) | 80 | 32 | 32 | Wait Table | 3周期 |
| P4 Netburst (2000) | 126 | 48 | 24 | 保守式 | 45周期 |
| Core 2 (2006) | 96 | 32 | 20 | 预测器 | 45周期 |
| Sandy Bridge (2011) | 168 | 64 | 36 | 预测器 | 4周期 |
| Skylake (2015) | 224 | 72 | 56 | 预测器 | 45周期 |
| Golden Cove (2021) | 512 | 192 | 72 | 预测器 | 5周期 |
| Zen 3 (2020) | 256 | 72 | 64 | 预测器 | 34周期 |
| Zen 4 (2022) | 320 | 88 | 64 | 预测器 | 34周期 |
| A78 (2020) | 160 | 68 | 44 | 预测器 | 4周期 |
| Firestorm (2020) | 630 | 130 | 108 | 预测器 | 34周期 |
处理器存储器子系统演进
从表中可以观察到几个明显趋势:
LSQ大小持续增长:从1998年的32项增长到2021年的192项(LQ),增长了6倍。这与ROB大小的增长(80512,增长6.4倍)基本同步,验证了"LQ ROB 30%40%"的经验法则。
LQ/SQ比例逐渐偏向LQ:早期的Alpha 21264是1:1(LQ 32: SQ 32),现代的Golden Cove是2.7:1(LQ 192: SQ 72)。这反映了设计者对MLP(受LQ限制)和Store排空带宽(受SQ限制)之间的权衡认知——LQ是更关键的瓶颈。
消歧策略从保守走向预测:P4 Netburst使用保守式消歧,导致Load延迟较高。后续所有设计都采用了某种形式的推测式消歧配合预测器。
STLF延迟基本稳定:在35个周期范围内,与L1 Cache命中延迟匹配。AMD Zen系列通过更小的SQ实现了34周期的更快STLF。
Apple的激进设计:Firestorm核的LQ和SQ远大于同代其他设计,反映了Apple追求极致单核性能的设计哲学。
存储器指令加速的技术总结
表表 36.11从多个维度总结了本章讨论的各项技术。
| 技术 | 解决的问题 | 硬件代价 | 性能收益 |
|---|---|---|---|
| 推测式消歧 | Load等待先序Store | Load Queue + 违规检测CAM | IPC +15%30% |
| Store Set预测器 | 反复违规 | 6 KB SRAM | 违规率降低90%+ |
| STLF | Store到Load数据传递 | Store Buffer CAM + 数据MUX | 避免1030周期等待 |
| 部分转发 | 不同宽度的STLF | 字节使能匹配 + 移位器 | 减少Store Fwd Stall |
| Store合并 | Cache写端口压力 | 合并检测 + 合并缓冲 | 写事务减少10%40% |
| WCB | 流式写入效率 | 410项缓冲 | 总线事务减少64 |
| 分离LSQ | 搜索效率 | 额外年龄比较逻辑 | 功耗降低50% |
| Bank化CAM | CAM功耗 | Bank选择逻辑 | 功耗降低75% |
存储器指令加速技术总结
从表中可以看出,推测式消歧和STLF是对IPC影响最大的两项技术——前者让Load不需要等待先序Store,后者让Load可以直接从Store Buffer获取数据。这两项技术是现代乱序处理器的核心,几乎所有商业设计都实现了它们。
其他技术(Store Set预测器、部分转发、Store合并、Bank化CAM等)是在基础技术之上的优化,每项带来个位数百分比的IPC提升或面积/功耗改善。这些优化的价值取决于具体的工作负载和设计约束——高性能桌面/服务器处理器通常全部实现,而面积/功耗受限的移动处理器可能选择性实现。
案例研究 6 — RISC-V开源处理器核的LSQ实现
开源RISC-V处理器核提供了不同复杂度级别的LSQ实现参考:
BOOM(Berkeley Out-of-Order Machine):实现了分离的Load Queue和Store Queue,支持推测式消歧(盲推测,无预测器)和基本的STLF(只支持完全覆盖转发)。LQ和SQ大小可配置(默认LQ 16项、SQ 16项)。违规检测使用简单的地址低位比较。BOOM的LSQ实现相对简洁,适合学术研究和教学。
XiangShan(香山):实现了更完整的LSQ,包括Store Set预测器(类似Chrysos-Emer方案)、支持部分转发的STLF、Bank化的Load Queue、以及与一致性协议集成的Snoop搜索。LQ 80项、SQ 64项,与其256项ROB匹配。XiangShan的LSQ实现接近商业处理器的复杂度,是学习高性能LSQ设计的优秀参考。
Rocket:作为有序(in-order)处理器核,Rocket使用保守式消歧——不需要Load Queue和Store Set预测器。Store Buffer实现基本的STLF。其简洁性使其成为理解LSQ基本概念的良好起点。
性能计数器与调优
现代处理器提供了丰富的硬件性能计数器(Hardware Performance Counters)来监控存储器指令加速相关结构的行为。性能工程师可以使用这些计数器来诊断存储器指令的性能瓶颈:
Store Forwarding成功/失败计数:监控STLF的成功率。高失败率可能意味着程序中存在大量不支持的部分转发模式——提示编译器需要调整访问模式。在Intel的处理器上,对应的性能事件为
LD_BLOCKS.STORE_FORWARD。Memory Order Violation计数:监控违规检测的触发频率。高违规率意味着内存依赖预测器工作不良——可能是因为工作负载的访问模式过于复杂或不可预测。对应事件:
MACHINE_CLEARS.MEMORY_ORDERING。Store Buffer满阻塞计数:监控Store Buffer满导致流水线暂停的频率。高频率的SB满阻塞意味着Store排空带宽不足或Store密度过高。对应事件:
RESOURCE_STALLS.SB。Load Queue满阻塞计数:类似地,监控LQ满导致的暂停。高频率意味着LQ大小不足,可能需要优化代码的MLP或减少飞行中Load数量。
STLF延迟统计:部分处理器支持STLF延迟的直方图统计,帮助分析STLF是否成为关键路径。
基于这些计数器的调优策略包括:
如果Store Forwarding失败率5%:检查代码中是否存在不同宽度的Store-Load对(如char写入后int读取),使用类型一致的访问方式。
如果Memory Order Violation频率每万条指令一次:检查是否存在通过指针别名导致的Load-Store冲突。编译器可以使用
restrict关键字来告知编译器两个指针不别名。如果Store Buffer满阻塞频率每千条指令一次:优化代码中的Store密度。考虑将频繁写入的数据保留在寄存器中,减少Store指令数量。
如果Load Queue满阻塞频率每千条指令一次:减少飞行中的Cache未命中Load数量。考虑使用软件预取来提前将数据调入Cache,减少长延迟Load的数量。
前沿研究方向
存储器指令加速领域仍在不断演进。以下是几个活跃的研究方向:
无CAM的LSQ设计
CAM结构是LSQ面积和功耗的主要瓶颈。学术界提出了多种无CAM(CAM-free)的LSQ替代方案:
基于哈希的搜索:使用地址的哈希值索引一个SRAM表,每个表项存储一个链表指针指向地址匹配的LQ/SQ条目。STLF搜索和违规检测变成了哈希查找 + 链表遍历,避免了CAM的面积开销。缺点是哈希冲突会导致链表过长,延迟不可预测。
Bloom Filter替代CAM:用多个Bloom Filter替代CAM进行存在性检查。每个Store在地址计算完成后将其地址插入Bloom Filter;Load执行时查询Bloom Filter判断是否可能冲突。如果Bloom Filter报告"不存在",Load可以安全推测。这种方案消除了CAM但引入了假阳性,需要额外的精确验证机制来处理假阳性。
基于预测的无序LSQ:通过精确的内存依赖预测器,在分发阶段就确定每条Load需要等待哪条Store。Load只需在发射队列中等待指定Store的地址就绪,不需要在执行时搜索整个Store Queue。这从根本上消除了STLF的CAM搜索,但要求预测器具有极高的精度——任何预测错误都需要回退到完整的CAM搜索或违规恢复。
这些方案在学术论文中展示了有竞争力的性能,但商业处理器目前仍然以传统CAM为主。主要原因是CAM的可靠性和确定性——CAM搜索总是在固定的时间内完成,而哈希和Bloom Filter方案的延迟可能不确定,给时序设计带来挑战。
Store-to-Load预测
一种新兴的优化思路是预测Load是否会从Store Buffer获取数据(STLF命中),并据此决定是否需要并行查询Store Buffer和Cache。
如果预测STLF不会命中(85%95%的情况),可以跳过Store Buffer的CAM搜索,直接从Cache读取。这节省了Store Buffer CAM搜索的功耗。
如果预测STLF会命中(5%15%的情况),可以提前启动Store Buffer的数据路径,减少STLF的延迟。
如果预测错误(预测不命中但实际命中),需要重新从Store Buffer读取数据——增加了一次回放的开销。
STLF预测器可以使用以PC索引的饱和计数器表实现,面积开销很小(12 KB)。在实际工作负载中,STLF的命中模式通常高度可预测——特定的Load PC(如栈操作)总是STLF命中,其他Load PC(如数组访问)几乎从不STLF命中。2位饱和计数器可以实现95%的预测精度。
解耦的Load/Store执行
传统的乱序处理器中,Load和Store共享同一个发射队列和执行引擎。一种新兴的设计思路是将Load和Store的执行完全解耦——为Load和Store各自维护独立的发射队列和执行管道。
解耦执行的优势在于:
Load和Store的调度可以独立优化——Load侧可以追求最低延迟(快速调度和旁路),Store侧可以追求最高吞吐(批量执行和合并)。
Store的地址计算和数据写入可以在独立的管道中进行,不与Load竞争AGU端口。
解耦设计天然地将STLF的CAM搜索隔离在Store侧,减少了对Load关键路径的影响。
但解耦执行也增加了复杂度——Load和Store之间的依赖关系(STLF、违规检测)需要通过跨管道的信号传递来处理,这可能引入额外的延迟。
安全感知的消歧设计
Spectre v4等攻击推动了安全感知的消歧设计。未来的处理器可能采用以下安全增强技术:
推测隔离区(Speculative Taint Tracking):跟踪每条指令的数据来源——如果Load的数据来自推测执行(可能不正确),标记该数据为"tainted"。Tainted数据不允许用于Cache索引或分支条件,从而阻止侧信道泄露。这种方案需要在数据路径中增加1位的"taint"标记,面积和延迟开销很小。
延迟可见性(Delayed Visibility):推测执行的Load结果只对本条Load的数据路径可见,不会影响微架构状态(如Cache替换策略、预取器状态)。这需要在Cache和预取器中增加"推测/确认"两阶段逻辑——推测阶段的Cache访问不更新替换策略,确认阶段(指令提交后)才更新。
上下文感知的推测控制:根据当前的安全上下文(用户态/内核态、沙箱内/沙箱外)动态调整推测的激进程度。在高安全上下文中(如内核态),禁用推测式消歧或限制推测的范围;在低安全上下文中(如用户态应用),允许完全的推测执行以最大化性能。
这些安全增强技术正在从学术研究向商业实现过渡。Intel、AMD和ARM的最新处理器已经实现了部分安全增强功能(如自动SSBD、推测执行限制),未来可能会看到更多的硬件级安全机制被集成到存储器子系统中。
面向数据中心工作负载的优化
随着云计算和数据中心工作负载的兴起,存储器指令加速面临新的挑战:
多租户隔离:在虚拟化环境中,同一物理核可能运行多个虚拟机。推测式消歧可能泄露跨虚拟机的信息(Spectre v4的变体)。数据中心处理器需要更强的安全隔离——可能需要在虚拟机切换时刷新Store Buffer和Load Queue中的推测状态。
SMT与LSQ共享:支持同时多线程(SMT)的处理器中,多个硬件线程共享同一个LSQ。不同线程的存储器指令混合在同一个Load Queue和Store Queue中,增加了CAM搜索的无效匹配(不同线程的Store不应该与Load产生STLF匹配)。解决方案包括:为每个线程维护独立的SSID命名空间、在CAM匹配中加入线程ID检查、或为每个线程分配独立的LSQ分区。
大工作集的MLP需求:数据中心工作负载(如数据库、Web服务器)通常具有很大的工作集和较高的Cache未命中率。这要求更大的Load Queue来支持更高的MLP。Intel Golden Cove的192项Load Queue部分是为此类工作负载设计的。
原子操作密集:多线程数据结构(如并发队列、锁)大量使用原子操作。原子操作对LSQ的占用和性能影响需要特别优化——如LR/SC的保留集管理和AMO的Cache行锁定机制。
机器学习驱动的预测器
近年来,学术界探索使用机器学习(ML)技术来改进内存依赖预测。传统的Store Set预测器基于简单的"PC SSID"映射,可能无法捕获复杂的上下文相关冲突模式。ML驱动的预测器可以利用更丰富的特征(如Load/Store的PC历史、全局分支路径、最近的冲突模式等)来做出更准确的预测。
然而,ML预测器面临以下挑战:(1) 推理延迟必须足够短(12个周期),与分发阶段的时序预算匹配;(2) 模型必须足够小,面积不超过传统预测器的23倍;(3) 训练和推理必须完全在硬件中实现,不依赖软件支持。目前的ML预测器研究主要处于模拟评估阶段,商业处理器中尚未采用。但随着ML加速硬件(如神经网络推理单元)在处理器中的普及,未来可能出现共享ML推理引擎的预测器设计——分支预测器、内存依赖预测器和预取器共享同一个轻量级神经网络推理单元,通过时分复用来为不同的预测任务服务。
Near-Data Processing与存储器指令
近数据处理(Near-Data Processing, NDP)/存内计算(Processing-in-Memory, PIM)是另一个可能改变存储器指令加速设计格局的技术方向。在NDP/PIM架构中,计算逻辑被放置在存储器附近(如DRAM芯片内部),数据不需要搬移到CPU核心进行处理。
如果大量的存储器操作可以在NDP单元中完成,CPU核心的存储器指令吞吐量需求将显著降低——Load/Store Queue、Store Buffer和STLF逻辑的压力都会减小。这可能允许设计者缩小LSQ的大小,将省下的面积用于其他结构(如更大的ROB或更多的执行单元)。
但NDP也带来了新的挑战——CPU核心需要与NDP单元之间同步数据一致性。当CPU核心执行一条Store后紧跟一条对NDP单元的命令时,需要确保Store的数据对NDP单元可见。这可能需要在NDP命令发送前排空Store Buffer,类似于FENCE指令的效果。NDP的一致性管理是一个活跃的研究领域。
设计提示
Spectre系列攻击揭示了一个深刻的设计哲学问题:推测执行的微架构副作用(如Cache状态变化)是否应该在推测失败时被完全撤销?从功能正确性的角度看,推测失败后只需要恢复架构状态(寄存器和内存);但从安全性的角度看,微架构状态(Cache内容、TLB状态、预测器状态等)的变化同样可以泄露信息。未来的处理器设计可能需要引入"微架构状态隔离"或"推测执行沙箱"等机制,在推测失败时清除所有微架构副作用。但这种清除的性能代价和硬件复杂度是否可接受,仍然是一个开放的研究问题。
本章讨论的存储器指令加速技术——推测式消歧、Store-to-Load转发、Load/Store队列管理和违规检测——是现代超标量处理器的核心组成部分。这些技术使得存储器指令能够像寄存器指令一样高效地参与乱序执行,将存储器访问的有效延迟降至最低。然而,这些技术也带来了巨大的硬件复杂度(Store Buffer的多端口CAM、Load Queue的违规检测逻辑)和安全挑战(Spectre v4等侧信道攻击)。
在未来的处理器设计中,如何在保持高性能的同时提供强安全保证,将是存储器子系统设计的核心课题。RISC-V的弱序内存模型为硬件实现提供了比x86 TSO更大的优化空间——不需要Load-Load顺序保证机制,可以简化Load Queue的设计。但弱序模型也要求FENCE指令的高效实现和精细的FENCE类型支持。
随着指令窗口的不断增大(ROB从256项向512+项演进)和工作负载的日益复杂(多线程、虚拟化、机器学习),存储器指令的加速技术将继续演进。CAM结构的可扩展性挑战推动了Bank化、Bloom Filter辅助等新型设计方案的探索;安全需求推动了推测隔离和上下文感知消歧控制的发展;数据中心工作负载推动了更大的LSQ和更精细的SMT资源管理。
处理器设计者需要在微架构层面同时考虑性能优化和安全防护。这两个目标之间的张力将长期存在,推动存储器子系统设计不断创新。
第 27.0 章(发射队列与依赖等待):本章详细讨论的Store Set预测器输出——“Load需要等待特定Store的地址计算完成”——在IQ中的硬件落实机制是第 27.0 章的核心内容之一。Store Set预测器通过SSITLFST两级查找得到的Store Queue编号被编码为IQ中的专用等待位,与标准的寄存器依赖唤醒机制协同工作。IQ的选择逻辑必须同时检查源操作数就绪和Store地址就绪两个条件——这是IQ设计中少有的“双重唤醒条件”。
第 38.0 章(ROB与违规检测):本章讨论的Load违规(Load推测执行后发现与先序Store地址冲突)是ROB违规检测机制需要处理的核心场景之一。Store Set预测器的训练信号直接来源于第 38.0 章的违规检测逻辑——当ROB检测到一条Load的结果因地址冲突而无效时,将违规Load和冲突Store的PC对反馈给Store Set预测器。
第 39.0 章(恢复机制):Load违规发生后的恢复策略(部分冲刷 vs. 全流水线冲刷)直接影响Store Set预测器的“投资回报率”。如果恢复代价很低(如只重放违规Load),Store Set的价值降低;如果恢复代价很高(全流水线冲刷30+周期),Store Set通过预防违规节省的周期数更加可观。第 39.0 章中讨论的检查点恢复机制也为Store Set预测器的“安全投机”提供了硬件基础——即使Store Set预测错误(预测冲突但实际不冲突),最坏情况只是Load多等了几个周期,不会导致功能错误。
本章讨论了存储器指令在单个处理器核心内部的加速技术——消歧、转发、LSQ管理和违规检测。但这些机制都假设了一个单核的执行环境。在多核系统中,新的问题浮现:一个核心的Store Buffer中的数据何时对其他核心可见?Store Buffer的排空策略如何与缓存一致性协议交互?当核心A的Store与核心B的Load访问同一地址时,TSO的顺序保证如何在硬件层面实现?
这些问题将在第 37.0 章中展开。Store Buffer不仅是核心内部的性能优化结构,更是内存模型(第 21.0 章中讨论的TSO和RVWMO)在硬件层面的关键实现组件。本章讨论的Store Set预测器与一致性协议之间也存在微妙的交互:当一个远程核心的Invalidation请求到达本地L1 Cache时,如果本地Store Buffer中有一条尚未提交的Store与被Invalidation的地址匹配,Store Set预测器建立的“该Load可以安全执行”的判断可能被打破——远程写入改变了本地Load“应该看到”的值。第 37.0 章将详细讨论这种跨核心交互对消歧逻辑的影响。
[^1]: G. Z. Chrysos and J. S. Emer, “Memory Dependence Prediction Using Store Sets,” Proceedings of ISCA-25, 1998.