提高Cache性能的方法
一个32 KB的8路组相联L1 D-Cache在5 GHz处理器上的访问延迟是4个时钟周期。如果将容量翻倍到64 KB,延迟就增加到5个周期。仅仅1个周期的差距看似微不足道,但对于指针追踪(pointer chasing)型工作负载——链表遍历、树查找、哈希表探测——每一次Load都依赖前一次Load的结果,这1个周期的差距意味着遍历速度下降20%。在整数程序中,指针追踪占Load指令的15%25%,折合到IPC上可能导致10%15%的下降。这就是Cache性能优化的精髓:在容量、延迟和功耗的三角约束中寻找每一个可能的改进空间。
设计提示
统一视角。本章讨论的技术几乎都可以映射到投机框架上。路预测(Way Prediction)是Cache中引入投机的最典型例子——投机地假设数据在某一路,只读取该路的Data SRAM,以12周期的错误惩罚换取倍的功耗节省。预取(Prefetch)是对未来访问地址的投机——投机成功则将缺失变为命中,投机失败则浪费带宽和污染Cache。甚至Cache层次本身也是一种投机:L1投机地假设最热的数据在最小最快的Cache中,L2投机地假设L1投机失败时数据仍然在较近的存储中。回顾第 5.0 章中的Cache基本结构和第 3.0 章中讨论的SRAM物理延迟约束,本章将展示如何在这些物理限制下通过投机机制来逼近理论性能上限。
在第 5.0 章中,我们建立了Cache的基本原理,并引入了AMAT(Average Memory Access Time)公式作为衡量存储系统性能的核心指标。回顾式 (5.1),AMAT可以分解为三个基本要素:
其中是Cache命中时的访问延迟,是缺失率,是缺失代价(即从下一级存储取回数据所需的额外时间)。提高Cache性能的所有技术,本质上都是在降低这三个参数中的一个或多个——同时尽量不恶化其余参数。
本章将系统地讨论提高Cache性能的主要方法。我们首先介绍写缓存(Write Buffer)技术,它通过缓冲写操作来隐藏写入延迟;接着讨论Cache的流水线化,这是现代高频处理器将L1 Cache延迟控制在可接受范围内的关键手段;然后详细分析多级Cache层次结构的设计选择,包括各级Cache的参数权衡和包含性策略;之后介绍Victim Cache这一减少冲突缺失的精巧硬件机制;最后深入讨论预取技术——在数据被实际需要之前将其提前取入Cache。
写缓存
在第 5.0 章中我们讨论了两种写入策略:写直达(Write-Through)和写回(Write-Back)。无论采用哪种策略,写操作都可能产生对下一级存储的访问:写直达策略需要将每次写操作都传播到下一级存储;写回策略在脏行(dirty line)被替换时需要将整行写回到下一级存储。如果处理器每次都必须等待这些写操作完成,将会产生严重的性能损失。写缓存(Write Buffer)正是为了解决这一问题而引入的硬件缓冲结构。
写缓存的结构
写缓存是一个位于L1 D-Cache和L2 Cache之间的小型FIFO缓冲队列。当L1 D-Cache需要向下一级存储发起写操作时(无论是写直达模式下的每次Store操作,还是写回模式下的脏行替换),写数据首先被放入写缓存,处理器随即可以继续执行后续指令,而不必等待写操作实际完成。写缓存中的数据随后在后台逐步写入L2 Cache或更低层存储。
图图 6.1展示了写缓存在存储层次中的位置及其内部结构。
写缓存的每一个表项通常包含以下字段:
有效位(Valid, V):指示该表项是否包含有效数据。
地址(Address):待写入的物理地址。在写直达模式下,这是Store操作的目标地址;在写回模式下,这是被替换的脏行的起始地址。
数据(Data):待写入的数据。在写直达模式下,可能只是一个字(4字节)或双字(8字节);在写回模式下,通常是整个Cache行的数据(64字节)。
字节使能(Byte Enable / Write Mask):标记数据中哪些字节是有效的。这在写合并时尤为重要——当多个不同偏移的写操作被合并到同一个表项时,只有实际被写入的字节需要标记为有效。
脏位(Dirty, D):在写回模式下,指示数据是否已被修改。
一个关键的设计细节是:当处理器发起一个Load操作时,不能仅查询L1 D-Cache,还必须同时查询写缓存。因为写缓存中可能包含最新的数据——如果一个Store操作的数据还在写缓存中尚未写入L1或L2,而后续的Load又读取了相同地址,那么Load必须从写缓存中获取最新数据,否则将读到过期的旧值,违反内存一致性。这种机制称为Store-to-Load Forwarding。
在硬件实现上,写缓存使用内容可寻址存储器(Content-Addressable Memory, CAM)来进行地址匹配:将Load地址与写缓存中所有有效表项的地址并行比较,如果匹配,则从写缓存中转发数据。如果写缓存和L1 D-Cache同时命中,优先使用写缓存中的数据。
需要注意的是,这里的“写缓存”与处理器Store Queue(存储队列)中的Store-to-Load Forwarding是不同层次的机制。Store Queue中的转发发生在Store指令将数据提交到L1D之前——Store Queue记录了所有已执行但尚未提交的Store操作。写缓存中的转发发生在Store已经提交到L1D之后——写缓存记录的是已经从L1D发往L2的写操作。在实际的处理器设计中,Load操作需要同时查询Store Queue和写缓存(以及L1 D-Cache本身),以确保获取最新的数据。
写缓存的CAM匹配逻辑是L1D访问关键路径上的一个重要时序约束点。对于12项的写缓存,需要12个地址比较器,每个比较器的宽度为3040位(Cache行地址宽度)。这些比较器的延迟约为23个FO4门延迟,通常可以与L1D的Tag比较并行完成,不增加额外的流水线级数。
写合并
写合并(Write Merging / Write Coalescing)是写缓存的一项重要优化。其基本思想是:当一个新的写请求到达写缓存时,如果写缓存中已经存在一个表项的地址与新请求的地址属于同一个Cache行,则将新写入的数据合并到已有的表项中,而不是分配一个新的表项。
写合并的好处是多方面的:
(1)减少写缓存的占用。多个对同一Cache行的写操作只占用一个表项,有效地增大了写缓存的逻辑容量。
(2)减少对下级存储的写带宽需求。合并后只需一次写操作即可将多个Store的结果写入L2,而非多次独立的写操作。
(3)提高总线利用率。在写直达模式下,如果不进行写合并,每个Store操作都会产生一次总线事务。写合并可以将多个小粒度的写操作(如单字节写入)合并为一次大粒度的写操作(如整行写回),大幅减少总线事务数量。
写合并的实现条件是:
新写请求的地址与写缓存中某个现有表项的地址在同一Cache行内(即高位地址匹配,仅行内偏移不同)。
合并后的数据不会产生字节冲突——即同一偏移位置不能出现两个不同来源的有效数据。在实践中,如果出现对同一字节的重复写入,新数据直接覆盖旧数据。
目标内存区域的属性允许写合并。某些设备内存(Device Memory)或不可缓存(Uncacheable)区域的写操作不能被合并,因为这些写操作可能具有副作用(例如写入MMIO寄存器),必须严格按序执行。
设计提示
写合并对于初始化类的代码模式特别有效。例如,memset将一段内存清零时,会产生大量地址连续的Store操作。如果没有写合并,每个Store都会占用写缓存的一个表项,很快就会导致写缓存满,处理器被迫停顿(stall)等待写缓存腾出空间。有了写合并,对同一Cache行的8个连续的8字节Store可以合并为一个64字节的整行写入,将写缓存的有效利用率提高8倍。
写合并的硬件实现需要在写缓存的每个表项中增加一个行地址匹配比较器,将新写入请求的Cache行地址(即去掉行内偏移后的高位地址)与所有有效表项的行地址并行比较。如果找到匹配的表项,则将新的数据字节写入该表项的对应偏移位置,并将对应的字节使能位(byte enable)设置为1。这一比较操作与写缓存的分配操作并行进行,不会增加额外的延迟。
需要特别注意的是,写合并必须尊重内存排序(Memory Ordering)的约束。在TSO(Total Store Order)模型下,Store操作之间的顺序必须被保留。如果两个Store操作写入同一Cache行的不同偏移,它们可以被合并到同一表项中,因为最终写入L2时它们的效果等价于按序执行。但如果涉及不同Cache行的Store之间有严格的顺序要求(例如通过SFENCE指令分隔),则不能将它们重排或合并。
此外,写合并缓冲(Write Combining Buffer, WCB)是写合并概念的一种特殊形式,专门用于处理对不可缓存(Uncacheable)但可以写合并的内存区域的写操作。在x86架构中,这类内存区域的类型标记为WC(Write Combining)。典型的应用场景是对显存(Frame Buffer)的写入:图形渲染需要向显存写入大量像素数据,这些数据不需要缓存(因为CPU很少重新读取刚写入的像素),但可以通过写合并来提高写入效率。Intel处理器通常配备46个独立的WCB表项,每个表项可以容纳一个64 B的Cache行大小的数据。
WCB的工作模式与普通写缓存有重要区别。WCB中的数据不参与Cache一致性协议(因为它写入的是不可缓存区域),也不需要被Load操作查询(因为WC区域的语义允许弱一致性的读写行为)。当一个WCB表项被填满(所有64字节都被写入)或被显式刷新(如通过SFENCE指令),其内容以一次突发传输(burst write)写入总线,效率远高于逐字节的独立写操作。
写缓存的排空策略
写缓存中的数据不能无限期地停留——它们最终必须被写入下一级存储。写缓存的排空策略(Drain Policy)决定了何时以及以何种优先级将数据写入L2。常见的策略包括:
(1)机会主义排空(Opportunistic Drain):只在L2接口空闲时(没有L1 demand缺失需要服务)才排空写缓存表项。这种策略最大化了L2端口对demand请求的可用性,但可能导致写缓存在持续高负载时无法排空。
(2)水位线排空(Watermark Drain):设置一个高水位线(如表项数的75%),当写缓存的占用率超过高水位线时,开始强制排空,即使L2接口有demand请求在等待。这防止了写缓存完全填满导致的处理器停滞。
(3)年龄驱动排空(Age-driven Drain):当写缓存中最老的表项的年龄超过一定阈值时(如256个周期未被排空),强制排空该表项。这防止了数据在写缓存中停留时间过长,降低了数据丢失的风险(在电源故障等异常情况下)。
现代处理器通常组合使用这些策略:正常情况下使用机会主义排空以最大化性能,当占用率或年龄超过阈值时切换到强制排空以保证安全性。
写缓存的深度选择
写缓存的深度(即表项数量)是一个重要的设计参数,直接影响处理器的性能:深度过浅容易导致写缓存满时的停顿(stall),深度过深则增加面积、功耗,以及Load查询写缓存时的CAM匹配延迟。
在写直达模式下,由于每个Store都会向下级存储发起写操作,写缓存的压力较大,需要较深的写缓存来吸收突发的写流量。在写回模式下,只有脏行被替换时才会产生写回操作,频率远低于写直达模式,因此写缓存的压力较小。
表表 6.1列出了若干现代处理器的写缓存深度参数。
| 处理器 | L1D写策略 | Write Buffer深度 | 备注 |
|---|---|---|---|
| Intel Golden Cove | Write-Back | 12项 | L1D L2 WB Queue |
| Intel Redwood Cove | Write-Back | 12项 | 与Golden Cove相似 |
| AMD Zen 4 | Write-Back | 8项 | L1D Writeback Buffer |
| AMD Zen 5 | Write-Back | 8项 | 与Zen 4类似 |
| ARM Cortex-X4 | Write-Back | 8项 | L1D Eviction Buffer |
| Apple Everest (M4) | Write-Back | 10项(推测) | 非公开参数 |
现代处理器的写缓存/写回缓冲深度
从表表 6.1可以观察到,现代高性能处理器的L1D写缓存深度通常在812项之间。这个范围的选择是经过仔细权衡的:
(1)写缓存过浅的风险。如果写缓存只有4项,那么在密集写入的代码段中(如矩阵转置、结构体拷贝等),4个表项可能在几个周期内就被填满,此时处理器必须停顿等待写缓存将至少一个表项的数据写入L2。由于L2的写入延迟通常为1014个周期,这意味着每次停顿将浪费大约10个周期的执行时间。
(2)写缓存过深的代价。写缓存的每个表项需要存储一个完整Cache行的数据(64字节)加上地址和控制信息,每个表项约占7080字节的SRAM。更重要的是,Load操作每次都需要查询写缓存,写缓存越深,CAM匹配逻辑越复杂,查询延迟越高。一个16项的写缓存需要16路并行比较器,会显著增加面积和功耗。
(3)写合并对深度的缓解。有了写合并机制,有效深度可以超过物理深度。在典型的工作负载中,相邻的Store操作经常落在同一个Cache行内(空间局部性),因此8项物理深度在写合并后可能等效于1624项的逻辑深度。
性能分析 1 — 写缓存深度对AMAT的影响
设写缓存深度为项,Store操作的到达率为(单位:项/周期),L2的写入吞吐量为(单位:项/周期),写合并率为(即平均每个Store操作合并为一个写缓存表项)。当写缓存稳态占用率接近100%时,会产生停顿。利用排队论中的M/D/1模型可以近似估计停顿概率:
其中是服务强度。对于典型参数, , (L2写延迟10周期,单端口),。此时的停顿概率约为62%,约为12%,约为2%。这解释了为何现代处理器选择812项深度。
Cache的流水线化
随着处理器频率的不断提高,L1 Cache的单周期访问变得越来越困难。在5 GHz的处理器上,一个时钟周期仅有200 ps,而一个32 KB的8-way组相联SRAM的访问延迟(包括地址解码、位线感应、输出驱动)在先进工艺节点下约需400 ps600 ps。这意味着L1 Cache无法在单个时钟周期内完成访问,必须通过流水线化(Pipelining)将Cache访问分解为多个流水段。
单周期访问与多周期访问
在早期低频处理器(如1990年代的200 MHz500 MHz处理器)中,L1 Cache可以在单个时钟周期内完成全部操作:地址解码、SRAM读取、Tag比较、数据选择。例如,MIPS R4000(150 MHz,1991年)和Alpha 21164(300 MHz,1995年)的L1 Cache都是单周期访问。
但随着频率的提升,单周期访问变得不可能。现代高性能处理器的L1 D-Cache访问延迟普遍为35个时钟周期:
Intel Golden Cove(Alder Lake P-core,5.2 GHz):L1D延迟5个周期。
AMD Zen 4(Ryzen 7000,5.7 GHz):L1D延迟4个周期。
Apple Everest(M4 P-core,4.4 GHz):L1D延迟3个周期。
ARM Cortex-X4(3.4 GHz):L1D延迟4个周期。
注意Apple的L1D延迟仅为3个周期,部分原因是其频率相对较低(4.4 GHz),每个周期的时间预算更充裕。Intel的L1D延迟为5个周期,反映了其更高的频率目标和48 KB/12-way的较大容量。
在物理设计层面,Cache流水线化的另一个挑战是SRAM阵列的物理划分。流水线寄存器需要被插入到SRAM阵列的内部——例如在字线解码器输出和位线感应放大器输入之间。这要求SRAM阵列在物理布局上被分割为对应各流水段的子阵列,增加了布线复杂度和面积开销。在先进工艺节点下,这种物理划分还需要考虑金属互联层的布线规则和时钟树分布。
一个自然的问题是:为什么不通过降低频率来获得更低的L1D周期数延迟?答案在于频率和延迟周期数对性能的综合影响。L1D的绝对延迟(以纳秒计)在不同处理器之间其实差异不大:Intel Golden Cove的5周期5.2 GHz = 0.96 ns,Apple Everest的3周期4.4 GHz = 0.68 ns,AMD Zen 4的4周期5.7 GHz = 0.70 ns。真正的差异在于周期数,因为在乱序处理器中,其他操作(如算术运算、分支预测)都是以周期为单位计量的。L1D延迟从3周期增加到5周期意味着处理器的调度器需要处理更长的依赖链,增加了乱序窗口的压力。
Cache流水线化的一个重要设计决策是在哪里插入流水线寄存器。以一个典型的4周期L1D为例,流水段划分可能如下:
Stage 1:地址生成(Address Generation)——计算Load/Store指令的有效地址。
Stage 2:SRAM访问——用EA的Index字段读取Tag和Data SRAM。
Stage 3:Tag比较和Way选择——将读出的Tag与EA的Tag字段比较,确定命中路,通过MUX选出数据。
Stage 4:对齐和转发——根据数据宽度和Offset进行字节对齐,将结果写回寄存器文件或转发给依赖指令。
L1 D-Cache延迟的增加直接影响Load-Use延迟——从发出Load指令到Load的结果可以被后续指令使用的时间间隔。在一个理想的单周期Cache中,Load指令的结果在下一个周期就可以使用。但当L1D延迟为4个周期时,后续依赖于Load结果的指令必须等待4个周期。对于指针追踪(pointer chasing)型的代码(如链表遍历、树查找),每次Load都依赖于前一次Load的结果,Load-Use延迟直接决定了遍历速度:
其中是链表长度。在L1D延迟4周期的处理器上,遍历一个1000节点的链表至少需要4000个周期——即使所有数据都在L1 Cache中。
流水线化对命中延迟的影响
Cache的流水线化主要涉及两种基本的组织方式:并行访问(Parallel Access)和串行访问(Serial Access)。这两种方式在延迟、功耗和面积之间有不同的权衡。
并行访问方式
在并行访问方式中,Tag SRAM和Data SRAM同时被读取。Index字段同时送入Tag SRAM和Data SRAM,读出所有路的Tag值和数据。然后将每一路的Tag与地址的Tag字段进行比较,确定命中路,最后通过Way MUX从所有路的数据中选出命中路的数据。
图图 6.2展示了4-way组相联Cache并行访问的流水线时序。整个访问可以分为两个流水段:第一段读取Tag和Data SRAM(SRAM Access),第二段进行Tag比较和Way选择(Compare & Select)。
并行访问的优点是延迟较低——只需要2个流水段:一段读SRAM,一段比较和选择。但其缺点是功耗较高:在-way的Cache中,每次访问都需要读取所有路的Data SRAM,但最终只有一路的数据是有用的,其余路的数据白白消耗了功耗。对于8-way或12-way的Cache,功耗浪费尤为严重。Data SRAM的读取功耗通常占整个Cache访问功耗的60%70%,因此并行访问方式的总功耗大约是仅读取一路Data SRAM所需功耗的倍的70%30%35倍(对于8-way Cache)。
串行访问方式
在串行访问方式中,先访问Tag SRAM确定命中路,然后仅读取命中路的Data SRAM。这避免了读取所有路Data SRAM的功耗浪费,但增加了一个流水段——整个访问需要3个流水段。
图图 6.3展示了串行访问的流水线时序。
串行访问方式的优点是功耗低——每次仅读取一路Data SRAM,功耗约为并行方式的(仅Data SRAM部分)。其缺点是增加了一个流水段的延迟。对于Load-Use延迟敏感的应用(如指针追踪),多一个周期的延迟意味着性能下降约25%33%(从3周期增加到4周期,或从4周期增加到5周期)。
在实践中,大多数现代高性能处理器的L1 D-Cache采用并行访问方式,以获得最低的Load-Use延迟。功耗问题则通过路预测(Way Prediction)等技术来缓解,下一小节将详细讨论。L2 Cache和L3 Cache由于延迟本身较长,且对功耗更敏感,通常采用串行访问方式。
路预测
路预测(Way Prediction)是一种在并行访问和串行访问之间取得折中的技术。其基本思想是:在Cache访问开始之前,预测数据可能位于哪一路(way),然后仅读取被预测的那一路的Data SRAM。如果预测正确,则只消耗了一路Data SRAM的读取功耗,同时获得了与并行访问相同的延迟;如果预测错误,则需要重新读取正确路的Data SRAM,延迟增加12个周期。
路预测的预测准确率通常在85%95%之间。对于-way Cache,如果访问在各路之间均匀分布,随机猜测的准确率只有;而利用历史信息,可以大幅提高准确率。
DEC Alpha 21264是最早使用路预测的高性能处理器之一(1998年)。Alpha 21264的L1 D-Cache是64 KB/2-way组相联,采用了路预测来避免读取两路Data SRAM。每个Cache组(set)附带一个1位的路预测位(Way Prediction Bit),记录上次访问该组时命中的是哪一路。当新的访问到来时,根据路预测位仅读取预测路的Data SRAM:
预测正确:延迟与单路(直接映射)Cache相同,功耗也仅为读取一路SRAM的开销。
预测错误:需要在下一个周期重新读取另一路的Data SRAM,总延迟增加1个周期。同时更新路预测位。
Alpha 21264的路预测准确率约为85%90%。考虑到2-way Cache只有两种选择,这一准确率意味着大部分访问的时间局部性很强——同一组的连续访问倾向于命中同一路。
现代处理器中,路预测的实现方式更加精细。一种常见的方式是使用路预测表(Way Prediction Table),类似于分支预测器中的PHT(Pattern History Table),用PC(或Load/Store指令的地址)来索引,每个表项存储预测的路编号:
对于4-way Cache,每个表项需要2位来编码路编号(03)。
对于8-way Cache,每个表项需要3位。
表的大小通常为2561024项,用PC的低位进行哈希索引。
需要注意的是,路预测错误的处理比分支预测错误简单得多。分支预测错误需要冲刷(flush)整个流水线的推测指令,代价可能高达1020个周期。而路预测错误只需要重新读取正确路的Data SRAM,代价仅为12个周期。这使得路预测可以容忍相对较低的准确率(85%即可获得良好的综合效果),而分支预测器需要97%以上的准确率才能有效工作。
路预测还可以与部分标签匹配(Partial Tag Match / Way Hash)技术结合使用。部分标签匹配不使用独立的预测表,而是在Cache的每个组(set)中存储一个缩短的Tag(例如只取Tag的低8位),在Cache访问的第一个周期中,用这个缩短的Tag与地址进行快速的预匹配,确定可能命中的路,然后在第二个周期中仅读取该路的Data SRAM。缩短的Tag比较延迟远低于完整Tag比较(因为比较宽度更小),可以在更短的时间内完成。
这种方式的预测准确率取决于缩短Tag的位宽:8位的部分Tag在每组中只有的虚假匹配概率,对于4-way或8-way Cache,出现多路同时匹配的概率极低(约),因此部分Tag方式的准确率可以超过99%。MIPS R10000(1996年)和某些ARM Cortex核心采用了类似的技术。
部分Tag匹配技术的存储开销也非常低。对于8-way、64组的Cache,每组存储位的部分Tag,总计位。相比整个32 KB的Cache,这只增加了约1.5%的面积——远小于真正多端口SRAM带来的面积开销。
路预测技术还可以用于降低Tag SRAM的读取功耗。在传统的并行访问方式中,所有路的Tag都被读出并比较。有了高精度的路预测后,可以先只读取预测路的Tag进行比较,只有在预测错误时才读取其余路的Tag。这将Tag SRAM的每次访问功耗降低到原来的(预测正确时)。
路预测对于L1 I-Cache同样有价值。由于指令流具有很强的空间局部性(指令通常顺序执行),L1 I-Cache的路预测可以利用上次访问同一组时的命中路作为预测依据,准确率通常在90%以上。某些处理器甚至在分支预测器中记录分支目标所在的Cache路,在分支预测的同时完成路预测。
设计权衡 1 — 并行访问 vs. 串行访问 vs. 路预测
三种Cache访问方式在延迟、功耗和面积上各有优劣:
| 特性 | 并行访问 | 串行访问 | 路预测 |
|---|---|---|---|
| 命中延迟 | 最低(2段) | 最高(3段) | 预测正确:最低 |
| 预测错误:+12周期 | |||
| Data SRAM功耗 | 路全读 | 仅1路 | 预测正确:仅1路 |
| 预测错误:2路 | |||
| 面积开销 | Way MUX | 无 | Way预测表 |
| 适用场景 | L1 Cache | L2/L3 Cache | L1 Cache |
路预测的有效延迟可以用以下公式估算:
其中是预测准确率,是预测正确时的延迟(与并行访问相同),是预测错误时的延迟。对于、周期、周期,有效延迟为周期——仅比理想的并行访问多0.1个周期,但功耗降低了约70%(对于8-way Cache)。
性能分析 2 — 五步算例:4路组相联Cache的Way Prediction准确率与有效延迟
本算例推导一个4路组相联L1 D-Cache使用MRU-based路预测的准确率和有效延迟。
给定参数:4路组相联Cache;路预测策略为基于MRU(Most Recently Used)的历史预测;Cache命中时的基准延迟周期;路预测错误惩罚周期。
Step 1: 建立命中概率分布。根据实测数据,L1 D-Cache中连续两次访问同一组时命中同一路(MRU路)的概率约为60%。命中次近路(second MRU)的概率约为25%。其余两路各约7.5%。
Step 2: 计算预测准确率。如果路预测器只预测MRU路,准确率为:
如果使用2位预测器同时跟踪MRU和次MRU路,并在MRU预测失败时回退到次MRU:
Step 3: 计算有效延迟(MRU-only方案)。
相比并行读取所有4路的功耗,MRU-only方案在60%的访问中只读1路Data SRAM,功耗降低。
Step 4: 计算有效延迟(MRU+2nd方案)。采用两级预测:首先尝试MRU路,1周期后若发现错误则尝试次MRU路,再1周期后若仍错误则回退到并行读取。
但由于85%的访问只读12路,平均Data SRAM功耗降低。
Step 5: 对IPC的影响评估。对于指针追踪型工作负载(Load延迟在关键路径上),有效延迟从4.00周期增加到4.40周期,等效于Load-Use延迟增加10%。假设程序中20%的指令是指针追踪型Load,IPC下降约。但功耗节省45%57%可以被用于提高频率或增加其他结构的容量,综合效果通常是正收益。这正是现代处理器普遍采用路预测的原因。
SRAM工艺对Cache设计的影响
Cache的物理实现基于SRAM(Static Random-Access Memory)工艺,SRAM的特性直接决定了Cache的延迟、容量和功耗上限。
标准的6T SRAM单元(6-Transistor SRAM Cell)由两个交叉耦合的反相器组成,形成一个双稳态存储元件,通过两条位线(Bit Line, BL和)和一条字线(Word Line, WL)进行读写。6T单元的面积在先进工艺节点下约为(5nm节点),一个32 KB的Cache需要约个SRAM位单元,占用面积约——加上外围电路(地址解码器、感应放大器、写驱动器)后约为,在现代SoC中占非常小的面积。
然而,SRAM的面积效率随工艺节点的缩小并未与逻辑电路同步提升。从7nm到3nm,逻辑电路的密度大约提升了2,但SRAM的密度提升只有约1.31.5。这是因为SRAM单元需要保持足够的噪声容限(Noise Margin)来确保可靠的读写操作,限制了晶体管尺寸的缩小。这种SRAM密度瓶颈意味着在先进工艺节点上,Cache在芯片总面积中的占比反而在增加。
为了在先进工艺中继续扩展Cache容量,处理器厂商采用了以下策略:
高密度SRAM库:使用专门优化面积(而非速度)的SRAM单元。高密度SRAM单元使用更小的晶体管、更紧凑的布局,但读写速度较慢。L1 Cache使用高速SRAM库,L2/L3使用高密度SRAM库。
FinFET到GAA的演进:从FinFET(Fin Field-Effect Transistor)到GAA(Gate-All-Around)晶体管的过渡有望改善SRAM的密度和功耗特性。三星和台积电都在2nm及更先进节点上采用GAA架构(分别称为MBCFET和N2/A16)。
3D堆叠:如AMD的3D V-Cache所示,通过在逻辑die上方堆叠SRAM die来突破面积限制。这种方法不受单一die上SRAM密度的限制,但需要解决die间互联的延迟和散热问题。
Cache分体技术
随着Cache容量的增大和频率的提高,单一的SRAM阵列成为访问延迟和功耗的瓶颈。Cache分体(Cache Banking)技术将Data SRAM划分为多个独立的Bank,每个Bank可以独立地进行读写操作。
分体技术的主要好处包括:
(1)降低SRAM访问延迟。将一个大的SRAM阵列分为个Bank后,每个Bank的容量为原来的,其位线和字线更短,电容更小,因此读写速度更快。对于一个48 KB的L1D,分为4个Bank后每个Bank只有12 KB,访问延迟可以缩短10%15%。
(2)支持多端口访问。通过将不同的Load/Store操作分配到不同的Bank,可以在每周期服务多个访存请求,而无需使用昂贵的真正多端口SRAM。例如,一个4-bank的L1D可以在一个周期内同时服务4个访问不同Bank的Load操作。
(3)降低功耗。每次访问只激活一个Bank,而非整个SRAM阵列,动态功耗降低为原来的。
Bank的映射方式通常基于Cache行地址的低位。最简单的方式是使用行地址的低位直接作为Bank号。然而,这种方式在某些访问模式下可能产生高Bank冲突率——例如,步幅恰好等于Bank数的访问模式会导致所有请求落在同一个Bank中。为此,实践中常使用XOR映射:将行地址的低位与中间位进行XOR运算来确定Bank号,从而打散步幅模式导致的Bank冲突。
Bank冲突(Bank Conflict)是分体技术的主要缺点:当多个并发的Load/Store操作落在同一个Bank中时,只能串行化处理,其余操作必须等待或被重放(replay)。Bank冲突率取决于Bank数量和工作负载的地址分布特征。典型的4-bank L1D在一般工作负载下Bank冲突率约为5%10%;使用XOR映射后可以降低到3%5%。
Bank冲突的检测和处理是多端口Cache设计中的关键问题。检测通常在Cache流水线的第1段(地址生成后)完成:将所有并发Load/Store的Bank号进行两两比较。对于个端口的Cache,需要个Bank号比较器。当检测到冲突时,有两种常见的处理策略:
(1)延迟重发(Replay):将冲突的请求退回到调度器(Scheduler),在后续周期重新调度。AMD Zen系列和Intel Golden Cove都使用这种策略。冲突的请求通常在12个周期后被重新发射。
(2)仲裁排队(Arbitration):在Bank入口设置仲裁逻辑,当多个请求竞争同一Bank时,优先服务高优先级的请求(通常是更老的指令),其余请求被缓冲到等待队列中。这种策略避免了重发的流水线气泡,但需要额外的仲裁逻辑和缓冲空间。
Bank冲突对IPC的影响通常较小(1%3%),但在某些特定的访问模式下(如多个数组以相同步长遍历)可能导致显著的性能下降。编译器的循环优化(如循环分裂、数组填充padding)可以帮助减少这类冲突。
多级Cache结构
现代处理器普遍采用23级Cache层次结构,每一级在容量、延迟、相联度和组织方式上都有不同的设计选择。这些选择反映了该级Cache在整个存储层次中所扮演的角色——离处理器越近的Cache越强调低延迟,离处理器越远的Cache越强调高命中率。
多级Cache的设计动机可以用一个简单的思想实验来理解:假设我们只有一级Cache,要将AMAT控制在5个周期以内,需要什么样的Cache?如果主存延迟为200个周期,则需要,即。若,则。达到0.5%的缺失率需要非常大的Cache(通常8 MB以上,取决于工作负载),但如此大的Cache的访问延迟可能达到2030个周期,远超4周期的预算。这就是单级Cache面临的容量-延迟悖论:大容量降低缺失率但增加命中延迟,小容量保持低命中延迟但缺失率高。
多级Cache通过将这个矛盾分解为多个层次来解决:L1小而快(低),L2/L3大而慢(低),每一层在自己的角色上做到最优。
多级AMAT的递归分解
多级Cache的AMAT可以用递归公式精确表达。对于级Cache层次:
其中是第级Cache的命中延迟,是第级Cache的局部缺失率(Local Miss Rate)——即在第级Cache中的缺失率,条件是请求已经到达了第级(即已经在前级中缺失)。
需要特别区分局部缺失率和全局缺失率。全局缺失率是所有访问中在第级Cache中缺失的比例:
例如,如果、、,则——即只有万分之六的访问需要访问主存。
多级Cache的一个重要设计原则是:每增加一级Cache,全局缺失率应该至少降低一个数量级,以证明增加该级Cache的面积开销是值得的。从上面的例子可以看到,L2将全局缺失率从5%降低到0.75%(降低7倍),L3进一步降低到0.06%(再降低12倍)。如果L3只能将缺失率降低2倍(从0.75%到0.375%),则32 MB L3的面积可能不值得投入——用同样的面积增加L2容量或增加核心数可能更有价值。
为什么是3级而非更多级
一个自然的问题是:既然多级Cache可以递归地降低AMAT,为什么现代处理器通常只使用3级而非4级或5级?
答案在于边际收益递减和管理复杂性递增的双重约束。每增加一级Cache:
全局缺失率的改善幅度减小(因为工作集中最容易被捕获的部分已经被内层Cache覆盖)。
Cache管理的复杂性增加(包含性维护、一致性协议、替换策略的层间交互)。
物理上的互联延迟增加(数据需要经过更多的层级传递)。
在当前的工艺技术和典型工作负载下,3级是最优的平衡点。L1提供15周期的快速访问,L2提供1014周期的中速访问,L3提供3050周期的慢速但大容量访问,三者共同将有效AMAT控制在接近L1命中延迟的水平。
一个值得关注的例外是Apple的SoC设计:Apple使用一个系统级Cache(SLC, System Level Cache)作为“L4”——位于LLC和DRAM之间,被所有核心和GPU共享。SLC的容量约为32 MB,其存在使得Apple可以使用较慢但更省电的LPDDR内存而不显著影响性能。
Apple的SLC设计理念值得深入分析。传统的PC处理器直接面向高速DDR5内存(带宽可达70 GB/s+),但Apple的移动处理器使用LPDDR5(带宽约50 GB/s,延迟更高)。SLC作为内存和所有片上计算单元之间的缓冲层,有效地降低了对内存带宽和延迟的依赖:
GPU和CPU共享带宽。在没有SLC的设计中,GPU的大量纹理访问会与CPU的Cache缺失竞争内存带宽。SLC可以缓存频繁的GPU纹理数据,大幅减少对内存带宽的争用。
隐藏LPDDR的高延迟。LPDDR5的延迟高于DDR5(约多20%30%),SLC的高命中率将大部分访问的延迟限制在SLC的访问延迟(30周期)而非LPDDR的延迟(150周期)。
降低内存功耗。每次DRAM访问都消耗显著的功耗(约20 pJ/bit)。SLC命中避免了DRAM访问,直接降低了系统级的功耗。对于电池供电的移动设备,这一节省非常关键。
设计提示
Apple的多级Cache设计(大L1 + 共享L2 + SLC作为“L4”)与传统的“小L1 + 私有L2 + 共享L3”设计形成了鲜明对比。两种设计哲学都在其目标场景下取得了优秀的性能-功耗平衡:x86阵营的设计优化了多核高频场景下的延迟可预测性,而Apple的设计优化了移动场景下的功耗效率和带宽利用率。这再次说明了Cache层次设计没有“放之四海而皆准”的最优解——最优的设计取决于目标频率、核心数、功耗预算和典型工作负载。
各级Cache的设计目标差异
在讨论各级Cache的具体设计之前,有必要先明确各级Cache的设计目标差异。这些差异决定了每一级Cache在容量、相联度、延迟和带宽上的参数选择。
| 设计维度 | L1 | L2 | L3/LLC | |
|---|---|---|---|---|
| 首要目标 | 最低延迟 | 高命中率 | 最大覆盖 | |
| 容量 | 32 KB192 KB | 256 KB16 MB | 8 MB128 MB | |
| 延迟目标 | 35周期 | 1014周期 | 3050周期 | |
| 端口数 | 24 Load + 12 Store | 1 | 1/切片 | |
| 共享方式 | 核心私有 | 通常私有 | 所有核心共享 | |
| 访问方式 | 并行(Tag+Data同时) | 串行(先Tag后Data) | 串行 | |
| 替换策略 | PLRU | PLRU/RRIP | DRRIP/SHiP | |
| ECC保护 | 奇偶校验 | SECDED | SECDED | |
| 写策略 | Write-Back | Write-Back | Write-Back | |
| 预取器 | Stride, Streamer | Streamer, Spatial | 无/时间预取 |
各级Cache的设计目标差异
从表表 6.2可以看出,随着Cache层级的增加,设计重心从“速度优先”逐渐转向“容量优先”和“覆盖率优先”。L1的一切设计决策都围绕着最小化Load-Use延迟;L2在延迟可以接受的范围内尽可能增大容量;L3则追求最大化对工作集的覆盖,同时承担一致性过滤的辅助功能。
L1 Cache的设计
L1 Cache是距离处理器核心最近的Cache层次,其设计目标是以最低的延迟为处理器提供指令和数据。L1 Cache通常分为独立的L1指令Cache(L1 I-Cache)和L1数据Cache(L1 D-Cache),这种分离设计(称为Harvard架构)允许处理器同时从L1I取指和从L1D访存,避免结构冲突。
表表 6.3列出了20242025年主流高性能处理器的L1 Cache参数。
从表表 6.3中可以提炼出几个关键的设计趋势:
(1)L1D容量从32 KB增长到48 KB128 KB。早期的L1D通常为32 KB(如ARM Cortex-A77、AMD Zen 13),但随着工艺进步和工作负载对容量需求的增加,48 KB已成为x86处理器的主流选择(Intel自Golden Cove、AMD从Zen 5开始),而Apple更是将L1D推至128 KB。更大的L1D意味着更高的命中率,但也意味着更高的相联度需求(以维持较小的组数,从而减少冲突缺失)和更高的延迟风险。
(2)L1D相联度从4-way增长到812-way。相联度的增加与容量增加相配合:48 KB/12-way的每组容量为4 KB/组,即仅用6位Index;32 KB/8-way则有组。两者组数相同,这意味着在48 KB/12-way的设计中,虽然容量增加了50%,但Index的位宽没有增加,Tag比较器从8个增加到12个,这是一个合理的面积和延迟权衡。
(3)多端口设计。为了支持每周期多条Load和Store指令的并行执行,L1D必须提供多个读/写端口。Apple的设计最为激进,提供4个Load端口和2个Store端口。多端口SRAM的面积和功耗随端口数的平方增长,因此端口数量是一个关键的设计约束。实践中常使用banked SRAM(分体SRAM)来代替真正的多端口SRAM——将Data SRAM划分为多个bank,每个bank为单端口,通过地址交叉(interleaving)使多个并发访问大概率落在不同bank中。
(4)Apple的异常值。Apple Everest的L1I 192 KB和L1D 128 KB远超x86和ARM阵营,这得益于Apple芯片较低的频率(4.4 GHz vs. x86的5 GHz+)和极为激进的流水线设计。更大的L1 Cache使Apple在许多工作负载中实现了更低的缺失率,从而部分补偿了较低的频率。
Apple能够使用如此大的L1 Cache有几个技术原因:首先,较低的频率意味着每个时钟周期的时间预算更长(4.4 GHz下为0.227 ns),SRAM阵列有更多的访问时间;其次,Apple使用台积电最先进的工艺节点(3nm),提供了更快的晶体管速度和更紧凑的SRAM密度;第三,Apple的芯片主要面向移动设备,其散热条件和功耗预算与桌面/服务器处理器不同——Apple可以接受较低的时钟频率来换取更大的Cache和更宽的执行引擎。
L1 Cache的设计趋势
从近几年的处理器演进可以观察到以下L1 Cache设计趋势:
(1)L1D容量持续增长。Intel从Golden Cove的48 KB开始,预计后续代际将进一步增大。AMD从Zen 5开始也将L1D从32 KB增加到48 KB。L1D容量的增加直接降低了L1D的缺失率,减少了对L2的访问压力。
(2)L1I容量增长更快。Intel的Lion Cove将L1I从32 KB增加到64 KB。这反映了现代应用程序的代码体积增长(部分原因是虚拟化、容器化和JIT编译产生的代码膨胀),以及服务器工作负载中频繁的上下文切换对I-Cache的压力。
(3)多端口数持续增加。从2 Load/1 Store增长到3 Load/2 Store(AMD Zen 4/5、Intel Lion Cove),Apple甚至达到了4 Load/2 Store。更多的端口支持更宽的超标量执行,但也增加了L1D的面积和功耗。
(4)Load-Use延迟维持稳定。尽管L1D容量在增大,Load-Use延迟维持在35个周期——这得益于工艺进步带来的晶体管速度提升。维持低Load-Use延迟是L1D设计的硬约束,因为它直接影响处理器在指针追踪等延迟敏感型工作负载上的性能。
L2 Cache的设计
L2 Cache位于L1和L3之间,其主要功能是捕获L1缺失的大部分访问,避免这些访问去到延迟更高的L3或主存。L2 Cache通常是每个核心私有的(per-core),但也有例外(如Apple的L2是4个核心共享的)。
L2 Cache的设计面临与L1不同的约束:
容量优先于延迟。L2的访问延迟对性能的直接影响不如L1大(因为L2只在L1缺失时才被访问,而L1的命中率通常在90%95%),因此L2可以牺牲一些延迟来获得更大的容量和更高的命中率。
单端口即可。L2不需要像L1那样提供多个读写端口,因为L1的缺失请求通常是串行到达的(L1的MSHR队列对缺失请求进行排队)。
Inclusive/Exclusive策略。L2是否包含L1的内容(Inclusive vs. Exclusive)是一个影响整个Cache层次设计的重要决策,将在后续小节中详细讨论。
表表 6.4列出了现代处理器的L2 Cache参数。
| 处理器 | 容量 | 相联度 | 延迟 | 共享方式 |
|---|---|---|---|---|
| Intel Golden Cove | 1.25 MB | 10-way | 14 cyc | 每核私有 |
| Intel Lion Cove | 2 MB | 16-way | 14 cyc | 每核私有 |
| AMD Zen 4 | 1 MB | 8-way | 12 cyc | 每核私有 |
| AMD Zen 5 | 1 MB | 8-way | 12 cyc | 每核私有 |
| Apple Everest (M4) | 16 MB | 16-way | 10 cyc | 4核共享 |
| ARM Cortex-X4 | 2 MB | 8-way | 10 cyc | 每核私有 |
现代高性能处理器的L2 Cache参数
几个值得注意的设计选择:
(1)L2容量从256 KB增长到1 MB2 MB。几年前,主流处理器的L2容量为256 KB512 KB(如Intel Skylake的256 KB、AMD Zen 2的512 KB),但现在1 MB2 MB已经成为标配。L2容量的增加使得更多的工作集可以被L2捕获,减少了对L3的访问压力。
(2)L2延迟维持在1014个周期。尽管L2容量在增加,但通过工艺进步和电路优化,L2延迟并未显著增加。这部分归功于先进工艺节点(如5nm、3nm)更快的晶体管速度,部分归功于更优化的SRAM设计。
(3)Apple的共享L2设计。Apple将16 MB的L2在4个P-core之间共享,这与x86和ARM阵营的每核私有L2形成鲜明对比。共享L2的优点是可以动态地将容量分配给负载最重的核心(一个核心可以使用全部16 MB),缺点是多个核心竞争同一个L2可能导致冲突和延迟增加。Apple的选择反映了其在移动设备上的典型使用场景:很少同时有4个核心都在运行计算密集型任务。
设计权衡 2 — 私有L2 vs. 共享L2
私有L2和共享L2各有优劣:
私有L2的优势在于延迟可预测(不受其他核心影响)、端口竞争少、以及一致性处理简单。缺点是容量不灵活——即使核心空闲,其L2容量也不能被其他核心使用。
共享L2的优势在于容量的动态共享和对不对称工作负载的适应性。当只有一个核心活跃时,该核心可以享受全部L2容量。缺点是访问延迟受仲裁和互联延迟的影响而增加,且多核心并发访问时可能产生带宽瓶颈。
Apple选择共享L2的一个重要原因是其大容量(16 MB)使得即使在4核心共享的情况下,每核仍可获得4 MB的平均份额——这已经超过了大多数x86处理器的私有L2容量。Apple实际上是用一个大型共享L2来替代传统的“小L2 + 大L3”两级结构,从而减少了一级Cache层次,降低了整体延迟。
L2 Cache的设计权衡
L2 Cache的关键设计权衡集中在以下几个方面:
(1)容量与延迟的折中。从256 KB增大到1 MB通常会增加12个周期的延迟(因为SRAM阵列更大,字线和位线更长)。但增大容量带来的缺失率降低通常远超过延迟增加的负面效果。设L2命中率从92%提升到96%(因为容量增大),L2延迟从10增加到12周期,L3延迟为40周期。改善前的AMAT贡献 = 周期(假设L2局部缺失率15%不变);改善后 = 周期。容量增大使AMAT改善了0.56周期。
(2)相联度选择。L2的相联度通常为816-way。更高的相联度可以降低冲突缺失,但增加了Tag比较器的数量和面积。由于L2采用串行访问(先比较Tag再读Data),Tag比较器的数量不影响Data SRAM的功耗,因此L2可以使用较高的相联度而不显著增加功耗。Intel Lion Cove的L2使用16-way,反映了在先进工艺下更高相联度的可行性。
(3)L2的预取器配置。L2通常配备专用的预取器,因为L2看到的缺失流(经过L1过滤后的残余缺失)具有与L1不同的特征。L1预取器已经覆盖了简单的短步幅和顺序流,L2的缺失流中更多是长步幅、不规则模式或L1预取器覆盖不了的复杂模式。因此L2预取器通常使用更复杂的算法(如BOP、SPP等),尽管这些算法的存储开销更大。
L2 Cache的带宽设计同样重要。L2需要同时服务来自L1I和L1D的缺失请求,以及来自硬件预取器的预取请求。为此,L2通常配备独立的请求队列和MSHR(Miss Status Holding Register)。MSHR记录当前正在处理的L2缺失请求,避免对同一地址发出重复的下级存储请求。现代处理器的L2 MSHR通常有1632个表项,允许同时处理多个未完成的缺失请求,实现缺失级并行(Miss-Level Parallelism)。
L2 Cache还承担了预取数据的暂存功能。许多处理器选择将硬件预取器预取的数据首先放入L2而非L1,以避免预取不准确时污染宝贵的L1容量。只有当预取的数据确实被处理器访问(产生L1缺失并命中L2中的预取行)时,数据才被提升到L1。这种策略在Intel处理器中普遍使用。
L2 Cache的流水线组织
L2 Cache由于容量较大(1 MB2 MB),其SRAM阵列的物理尺寸和线延迟都远超L1。因此L2的访问通常被划分为更多的流水段——典型配置为35段:
Stage 1(请求仲裁):L2接口从多个来源(L1I缺失、L1D缺失、L2预取器、一致性Snoop)中仲裁,选出一个请求进入流水线。由于L2是单端口设计,每周期只能服务一个请求。仲裁优先级通常为:Snoop L1D demand L1I demand 预取。
Stage 2(Tag查找):使用请求地址的Index字段读取Tag SRAM。由于L2采用串行访问方式(先比较Tag,确认命中后再读Data),这一阶段只读取Tag,不读Data SRAM,从而节省功耗。
Stage 3(Tag比较与状态判断):将读出的Tag与请求地址的Tag字段并行比较。如果命中,确定命中路和Cache行的一致性状态(如MESI状态)。如果缺失,分配MSHR并向L3发送请求。
Stage 4(Data读取):仅在Tag命中时才读取Data SRAM,读取命中路的数据。
Stage 5(数据返回):将数据通过L1-L2接口返回给L1 Cache,同时更新替换策略的状态。
这种串行Tag-先-Data-后的组织方式使L2每次只读取一路Data SRAM,功耗大约是并行方式的(为相联度)。对于8-way L2,功耗节省约。
L2的ECC保护
由于L2 Cache容量较大且距离处理器核心较远,软错误(Soft Error)的影响更加显著。L2中的每个数据字(通常以8字节为粒度)都附带ECC(Error-Correcting Code)保护,典型配置为SECDED(Single Error Correction, Double Error Detection)码,每64位数据需要8位ECC校验位,存储开销约为12.5%。
ECC的检查和纠正发生在数据从L2 SRAM读出之后、返回给L1之前。ECC解码器在关键路径上增加了约1个周期的延迟,但这一延迟通常被隐藏在L2流水线的Stage 4或Stage 5中。当检测到单比特错误时,ECC解码器自动纠正错误并将纠正后的数据写回SRAM(scrubbing);当检测到双比特错误时,触发机器检查异常(Machine Check Exception),因为无法纠正的数据错误可能导致程序产生错误结果。
L2的Tag SRAM同样需要保护。Tag中的一个比特翻转可能导致Cache行被错误地识别(命中了错误的地址),这比数据错误更危险。Tag通常使用简单的奇偶校验(Parity)保护——发现奇偶错误后将该行无效化并触发重新取回。
设计提示
L2 Cache的ECC设计反映了一个重要的工程原则:越大的SRAM阵列,越需要可靠性保护。L1 Cache的SRAM阵列较小(32 KB48 KB),软错误率较低,通常只使用奇偶校验保护;而L2的1 MB2 MB SRAM在先进工艺节点下每小时可能出现数次软错误(取决于环境辐射水平和工艺节点的敏感度),ECC保护是必须的。L3/LLC由于容量更大,通常也使用SECDED或更强的纠错码。
L2的Bank组织
为了提高L2的有效带宽,现代处理器的L2 Cache内部通常划分为多个Bank。虽然L2的接口在逻辑上是单端口的(每周期接受一个请求),但内部的Data SRAM可以划分为48个Bank,每个Bank独立操作。这种设计的好处是:当L2流水线连续处理访问不同Bank的请求时,前一个请求的Data读取阶段和后一个请求的Tag查找阶段可以重叠,实现流水线级的并行。
此外,Bank化的L2可以更高效地处理回写操作。当L1驱逐一个Dirty行需要写回L2时,写回操作可以在一个空闲的Bank中进行,而不必阻塞同时进行的读操作(只要读写操作落在不同的Bank上)。这对于写密集型工作负载的性能至关重要。
L3/LLC的设计
L3 Cache(也称为LLC,Last-Level Cache)是片上存储层次的最后一级。L3通常是所有核心共享的,因此它不仅要提供高命中率来减少主存访问,还要在多核环境中承担一致性过滤(Coherence Filtering)的功能——通过在L3中记录每个Cache行的存在信息,可以快速确定一致性请求(snoop)是否需要发送到某个核心。
现代处理器的L3通常采用分片(Sliced)设计:将L3物理地划分为多个切片(slice),每个切片具有独立的SRAM阵列、控制逻辑和网络接口。物理地址通过一个哈希函数映射到特定的切片,确保访问在各切片之间均匀分布。
图图 6.4展示了一个典型的8核处理器的LLC分片设计。
LLC分片设计的关键优势包括:
(1)带宽扩展。每个切片有独立的读写端口,个切片的总带宽是单片的倍。对于一个8切片、每切片4 MB、总容量32 MB的LLC,8个切片可以同时服务来自不同核心的请求,提供远高于单片LLC的聚合带宽。
(2)延迟均衡。通过哈希函数将地址均匀地分散到各切片,避免了热点(hot spot)问题——如果某些地址范围被频繁访问,而这些地址恰好映射到同一个切片,该切片就会成为瓶颈。Intel使用一种未公开的哈希函数来实现地址到切片的映射,逆向工程显示这是一种基于地址高位多个bit的XOR组合。
(3)一致性过滤。在Inclusive LLC设计中(见6.3.6 节),每个LLC切片可以记录哪些核心拥有该切片管辖的Cache行的拷贝。当一致性协议需要发送snoop请求时,只需查询目标地址所在的LLC切片,即可确定需要snoop哪些核心,而无需向所有核心广播。这大大降低了一致性流量。
LLC切片的哈希映射
地址到LLC切片的映射方式直接影响访问的均衡性和延迟的可预测性。最简单的映射方式是使用地址的若干中间位(例如bit[13:11]对应8个切片)直接作为切片号。然而,这种位选择映射有一个严重问题:如果程序的数据以某种步长访问(如步长为字节),所有访问都可能映射到同一个切片,造成该切片过载而其他切片空闲。
为了避免这种病态行为,现代处理器使用哈希映射。Intel处理器使用的哈希函数(通过逆向工程由Hund等人和Maurice等人分别在2013年和2015年揭示)基于地址高位的XOR组合。对于个切片,需要位的切片号。每一位切片号由地址中不同位组的异或产生:
其中是一组精心选择的地址位下标集合。例如,对于8切片(),Intel Sandy Bridge的哈希函数大致为:
这种XOR哈希的关键性质是:任何单一地址位的变化都会改变切片号,因此即使程序以字节的步长访问内存,访问也会均匀地分散到所有切片。从硬件实现的角度看,XOR哈希只需要若干个异或门组成的树结构,延迟仅为23个门级延迟,面积开销可以忽略不计。
案例研究 1 — AMD Zen的LLC切片映射
AMD Zen系列采用了一种与Intel不同的LLC组织方式。Zen 4的每个CCD(Core Complex Die)包含8个核心和32 MB的共享L3 Cache。L3被物理划分为8个4 MB的切片,每个切片与一个核心物理相邻。AMD同样使用哈希映射来分配地址到切片,但具体的哈希函数细节未公开。
AMD的一个独特设计决策是:虽然L3切片与核心物理相邻,但数据并不按照核心亲和性分配——核心0产生的缺失可能被映射到任何一个切片。这确保了当只有部分核心活跃时,所有L3切片仍然被利用。AMD的3D V-Cache(如Zen 4的96 MB版本)通过在逻辑die上方堆叠额外的SRAM die来扩展L3容量,堆叠的SRAM被视为额外的切片加入到哈希映射中,不需要修改核心端的地址映射逻辑。
LLC的访问延迟构成
LLC的访问延迟不仅包括SRAM本身的读写时间,还包括片上网络(On-chip Network)的传输延迟。对于一个通过Ring Bus互联的8核处理器,从核心到目标LLC切片的延迟可以分解为:
其中是请求在Ring Bus上的传输延迟(取决于核心到目标切片的跳数,每跳约12个周期),是Tag查找延迟(约35个周期),是Data SRAM读取延迟(约35个周期),是数据返回的传输延迟。
以一个具有8个Ring Stop的处理器为例,核心到切片的平均跳数为跳(在Ring Bus上平均距离是节点数的),每跳延迟2周期,则周期。加上Tag和Data读取的周期,总LLC延迟约为周期——这解释了为什么实测的LLC延迟(如Intel的45周期)远高于LLC SRAM本身的读取延迟。
当处理器核心数增加到16、28甚至60个时,Ring Bus的跳数增加,显著增长。为此,Intel从Skylake-SP开始采用Mesh互联替代Ring Bus。Mesh互联将核心和LLC切片排列在二维网格上,请求通过XY路由在网格中传输。Mesh互联的最坏情况延迟为级别(而Ring Bus为),在大核心数下具有更好的可扩展性。
LLC的功耗管理
LLC是片上面积和功耗的重要贡献者。一个32 MB的LLC在先进工艺节点下可能占芯片总面积的30%50%,其静态功耗(泄漏电流导致)也是不可忽视的。现代处理器通过以下技术来管理LLC的功耗:
(1)电源门控(Power Gating)。当处理器处于低负载状态时,可以关闭部分LLC切片的电源,将其中的数据先写回或无效化。这种技术在移动处理器中尤为重要——当只有12个核心活跃时,不需要全部32 MB的LLC容量。
(2)动态容量调节。即使不完全关闭电源,也可以通过禁用部分路来降低有效容量和功耗。例如,一个16-way LLC可以动态地将有效相联度降低到8-way或4-way,减少Tag比较器和Data读取的功耗。
(3)时钟门控(Clock Gating)。在没有请求到达的周期,关闭LLC切片的时钟以消除动态功耗。由于LLC并非每周期都被访问(L1/L2的命中率通常在90%97%),时钟门控可以显著降低平均功耗。
LLC的写回策略与脏行管理
LLC是片上存储层次的最后一级,承担着向主存写回脏数据的责任。LLC的写回策略直接影响内存总线的利用率和延迟。
在写回(Write-Back)模式下,LLC只在脏行被替换时才将其数据写回主存。这最大化了写操作的合并效果——同一Cache行可能被多次修改,但只需在最终被替换时写回一次。然而,替换驱动的写回有一个问题:它将写回操作的时间与替换事件绑定,可能导致写回操作集中在内存访问高峰期发生(因为高访问率意味着高替换率),加剧了带宽拥塞。
为了缓解这一问题,现代处理器的LLC通常实现了投机性写回(Speculative Write-Back,也称为Background Scrubbing)。投机性写回在内存总线空闲时主动将LLC中的脏行写回主存,即使这些行尚未被替换。这样做的好处是:
(1)写回时间分散化。将写回操作分散到空闲周期,避免在高负载时段集中写回导致的带宽争用。
(2)降低替换延迟。当LLC需要替换一个行时,如果该行已经通过投机性写回将脏数据写入了主存,则替换操作不需要等待写回完成,可以立即释放该行。这将替换延迟从“写回延迟 + 填充延迟”降低到仅“填充延迟”。
(3)改善功耗突发。将写回功耗分散到较长的时间段,降低峰值功耗。
投机性写回的实现通常使用一个低优先级的后台扫描器(Background Scanner),周期性地扫描LLC的各个组,选择脏行进行写回。写回请求被放入一个低优先级的写回队列中,只有在内存总线有空闲带宽时才被发出。
LLC与内存控制器的接口
LLC缺失后的请求通过片上互联发送到内存控制器(Memory Controller, MC)。内存控制器负责将请求转换为DRAM的命令序列(ACT-RD/WR-PRE)并管理DRAM的时序约束。从LLC到DRAM的完整延迟可以分解为:
其中是请求在内存控制器队列中的等待时间(取决于队列深度和仲裁策略),是DRAM行操作时间(如果需要行打开/关闭),是CAS Latency(从列命令发出到数据在数据总线上可用的时间),是数据从DRAM芯片返回到LLC的传输时间。
对于DDR5-5600内存(CL=36),在5 GHz的处理器时钟下,个处理器周期(DDR5-5600的时钟为/周期,36个DDR5周期;处理器周期,个处理器周期——但考虑到流水线重叠,实际有效CL约为3645个处理器周期)。加上行打开和返回延迟,总DRAM延迟约为60100个处理器周期。
内存控制器通常维护一个64128项深的请求队列,使用复杂的调度策略(如FR-FCFS, First Ready-First Come First Served)来最大化DRAM的行命中率和带宽利用率。行命中率是一个关键指标:如果请求的数据所在的DRAM行已经被激活(行打开),则只需要发出列命令,延迟约为;如果行未打开,需要先关闭当前行、打开新行,延迟增加约2030个处理器周期。内存控制器通过重排序请求队列中的请求来最大化行命中率——将访问同一DRAM行的请求优先调度,即使它们的到达顺序不同。
Mesh互联与LLC延迟的可预测性
Intel从Skylake-SP开始在服务器处理器中使用Mesh互联替代Ring Bus来连接核心和LLC切片。Mesh互联将核心和LLC切片排列在一个二维网格上,每个节点有东西南北四个方向的链路。请求通过XY路由在网格中传输:先在X方向移动到目标列,再在Y方向移动到目标行。
与Ring Bus相比,Mesh互联的主要优势是:
(1)更低的最坏情况延迟。在的Mesh中,最大跳数为。对于核的网格,最大跳数为跳。而等效的Ring Bus最大跳数为跳。
(2)更高的聚合带宽。Mesh中每个节点都有多个方向的链路,总链路数为条。多条并行路径可以同时传输不同的请求,避免了Ring Bus的带宽瓶颈。
(3)更好的扩展性。增加核心数只需要扩展网格维度,延迟增长为而非Ring Bus的。
Mesh互联的缺点是延迟的变化性更大——从核心到近处LLC切片可能只需2跳(约4个周期),到远处切片可能需要9跳(约18个周期)。这种延迟的不均匀性意味着LLC的访问延迟取决于地址哈希到哪个切片以及发出请求的核心位置。对于延迟敏感的工作负载,这种不确定性可能导致性能波动。
一种缓解措施是近切片优先分配(Near-Slice Allocation):当数据从内存取回时,优先分配到距离请求核心较近的LLC切片,而非严格按照哈希函数分配。这需要在LLC切片之间进行数据迁移的额外机制,增加了设计复杂度,但可以显著降低平均LLC访问延迟。
Intel的LLC切片架构演进
Intel的LLC架构在不同世代中经历了显著演进:
Nehalem(2008年):LLC统一为一个物理单元,通过Ring Bus与核心连接。访问延迟均匀。
Sandy Bridge(2011年):LLC被分片,每片2.5 MB,通过双向Ring Bus互联。引入地址哈希映射。
Skylake-SP(2017年):服务器处理器采用Mesh互联替代Ring Bus。每核1.375 MB LLC切片。从Inclusive切换到NINE策略。
Sapphire Rapids(2023年):引入1.875 MB/核的LLC切片,4个Tile构成一个SoC,Tile间通过EMIB互联。LLC切片增加了缓存行迁移支持。
Granite Rapids(2024年):进一步增加核心数(128核),Mesh规模扩大,LLC切片容量和互联带宽相应增加。
这一演进趋势表明,LLC的设计正从一个相对简单的共享SRAM阵列演变为一个复杂的分布式存储系统,其设计挑战已经从纯粹的SRAM电路设计转向了包括互联拓扑、一致性协议和资源分配策略在内的系统级设计问题。
现代处理器的L3参数差异较大,表表 6.5进行了详细对比。
| 处理器 | 总容量 | 相联度 | 延迟 | 切片数 | 包含性 |
|---|---|---|---|---|---|
| Intel Golden Cove | 30 MB | 12-way | 45 cyc | 8 | NINE |
| AMD Zen 4 | 32 MB/CCD | 16-way | 40 cyc | 8 | Exclusive (L2–L3) |
| AMD Zen 4 V-Cache | 96 MB/CCD | 16-way | 42 cyc | 8+ | Exclusive |
| Apple M4 (SLC) | 32 MB | — | 30 cyc | — | NINE |
| ARM Cortex-X4 (DSU) | 12 MB | 16-way | 40 cyc | 4 | NINE |
现代高性能处理器的L3/LLC参数
几个值得关注的设计要点:
Intel:每个核心分配约2 MB3 MB的LLC容量,总容量随核心数线性增长。例如,Core i9-14900K的24核(8P+16E)配备36 MB LLC。LLC通过Ring Bus或Mesh互联与各核心通信。
AMD:Zen 4/5架构采用CCD(Core Complex Die)设计,每个CCD包含8个核心和32 MB的共享L3(V-Cache版本可达96 MB128 MB)。AMD的L3采用16-way相联,延迟约40个周期。AMD的3D V-Cache技术通过在逻辑die上方堆叠额外的SRAM die来增加L3容量,这是一种利用先进封装技术扩展Cache容量的创新方法。
Apple:M4系列使用SLC(System Level Cache)作为LLC,容量32 MB,被所有核心和GPU共享。
LLC的替换策略也值得一提。由于LLC是所有核心共享的,不同核心的工作集可能差异很大。如果使用简单的LRU替换策略,一个具有大工作集的"扫描型"(streaming)核心可能快速填满LLC,将其他核心的有用数据驱逐出去,导致全局性能下降。为此,现代LLC通常使用自适应替换策略:Intel使用一种称为Quad-Age LRU(QLRU)的策略,AMD使用基于重用距离的自适应策略。这些策略能够识别并保护频繁被重用的Cache行,同时快速驱逐不太可能被重用的行。
Cache替换算法的硬件实现
Cache替换策略决定了当一个新的Cache行被取入时,应当驱逐哪一个已有的行。理想的替换策略应当驱逐那个在未来最长时间内不会被再次访问的行(即Belady的最优替换),但这需要知道未来的访问序列,在硬件中不可实现。实际的替换策略使用过去的访问历史来近似预测未来。
True LRU的硬件实现
真LRU(True LRU)策略驱逐最近最少被使用的那一路。要在-way组相联Cache中精确实现LRU,需要为每组维护一个个元素的全序(total order),记录最近到最久的访问顺序。
最直接的实现方式是使用一个LRU矩阵(也称为NRU Matrix或Age Matrix)。对于-way Cache,每组需要一个的比特矩阵,其中表示路比路更近被访问过。矩阵满足反对称性:,因此实际只需要存储上三角部分,共位。
当路被访问时,更新操作为:
将第行的所有位设为1:, 。
将第列的所有位设为0:, 。
这两步操作使路成为最近被访问的路。
选择LRU替换路的方法是:找到第行全为0的路(即对所有其他路来说都是最久未访问的),。
对于不同的相联度,LRU矩阵的存储开销如下:
4-way:位/组。32 KB/4-way/64 B行有128组,总计768位96 B。
8-way:位/组。32 KB/8-way有64组,总计1792位224 B。
12-way:位/组。48 KB/12-way有64组,总计4224位528 B。
16-way:位/组。4 MB/16-way(LLC切片)有4096组,总计491520位60 KB。
可以看到,对于L1级别的48路Cache,True LRU的存储开销是可接受的。但对于16-way的LLC,60 KB的LRU元数据已经相当可观。更重要的是,True LRU的更新操作需要在每次Cache访问时修改矩阵的一整行和一整列(共位),对于16-way来说是30位的并行写操作——这在高频流水线中是一个非平凡的设计挑战。
伪LRU(PLRU)的硬件实现
由于True LRU在高相联度下的开销过大,实际的处理器设计中广泛使用伪LRU(Pseudo-LRU, PLRU)作为近似。最常见的PLRU实现是树形PLRU(Tree-based PLRU, TPLRU)。
树形PLRU使用一棵叶子节点的完全二叉树来近似维护LRU顺序。每个内部节点有一个1位的标志(指向左或右),共个内部节点,因此每组只需要位——远少于True LRU的位。对于8-way Cache,每组只需要7位(而True LRU需要28位)。
树形PLRU的操作规则如下:
替换选择:从根节点开始,沿着每个内部节点的指向方向(0 = 左子树,1 = 右子树)向下遍历,到达的叶子节点即为替换目标。这个路径指向的是"近似最久未访问"的路。
访问更新:当路被访问时,从根到路的叶子节点的路径上,将所有内部节点的指向翻转为远离路的方向。这等效于将路标记为"最近被访问",使替换路径避开它。
树形PLRU的更新操作只需修改个位(沿路径的内部节点),远少于True LRU的位。对于8-way Cache,每次访问只需更新3个位。
树形PLRU的近似质量很高:在大多数工作负载上,PLRU的命中率与True LRU相差不到1%2%。其主要缺陷是在特定的"病态"访问模式下(如工作集恰好比Cache大一路的循环访问),PLRU可能做出明显劣于LRU的选择。
RRIP替换策略
RRIP(Re-Reference Interval Prediction)由Jaleel等人于2010年在ISCA上提出,是现代LLC替换策略的基础。RRIP的核心思想是:为每个Cache行预测其重用距离(re-reference interval),优先驱逐预测重用距离最远的行。
RRIP使用一个位的RRPV(Re-Reference Prediction Value)计数器来表示每行的预测重用距离。RRPV = 0表示即将被重用(near-immediate re-reference),RRPV = 表示不太可能被重用(distant re-reference)。典型配置为位(4个重用距离级别),位(8个级别)。
RRIP的操作分为三个部分:
插入:当新的Cache行被插入时,其RRPV被初始化为一个中间值。SRRIP(Static RRIP)将新行的RRPV设为(即"长距离重用预测"),BRRIP(Bimodal RRIP)以小概率(如)将新行的RRPV设为,大概率设为。
命中更新:当行被重新访问时,将其RRPV设为0(表示"即将再次被使用")。这使得频繁被访问的行获得保护。
替换选择:需要替换时,搜索RRPV = 的行作为牺牲者。如果没有找到RRPV = 的行,则将所有行的RRPV递增1,然后重新搜索。这种"老化"(aging)机制确保长时间未被访问的行最终会被替换。
性能分析 3 — RRIP相比LRU的优势
LRU替换策略隐含一个假设:最近被访问的行最有可能在近期再次被访问。这个假设在扫描(scan/streaming)模式下严重失效——大量一次性访问的数据会不断将有用的数据驱逐出Cache。例如,一个循环扫描10 MB数据的程序会反复替换4 MB LLC中的所有有用数据。
RRIP通过将新插入的行设为"长距离重用预测"(而非LRU中的"最近使用"位置),有效地对抗了扫描模式的侵蚀。在SPEC CPU 2006的mcf和sphinx3等扫描密集型基准上,SRRIP相比LRU可以将LLC缺失率降低10%20%,对应3%8%的IPC提升。
SRRIP和BRRIP的自适应组合称为DRRIP(Dynamic RRIP)。DRRIP使用组采样(Set Dueling)技术:将LLC的一小部分组(如32个组)分为两组——一组强制使用SRRIP策略,另一组强制使用BRRIP策略——然后用一个全局计数器跟踪哪个策略在采样组中的缺失数更少,将获胜策略应用到所有非采样组。
组采样的硬件开销极低:32个采样组只需要在其Tag中增加1位标志(标识属于哪个采样组),全局计数器只需约10位。然而,这种简单的机制使得DRRIP能够动态地在SRRIP和BRRIP之间切换,在不同阶段的工作负载中都能选择最优策略。
SHiP:基于签名的LLC替换
SHiP(Signature-based Hit Predictor)由Wu等人于2011年提出,进一步改进了RRIP。SHiP的核心观察是:不同的指令PC(或其他签名信息)触发的Cache行具有不同的重用特征——某些PC访问的数据会频繁被重用(如循环中的数组访问),而另一些PC访问的数据是一次性的(如初始化代码、流式扫描)。SHiP利用这一观察,为每个PC维护一个独立的RRPV插入策略。
SHiP使用一个签名表(Signature History Counter Table, SHCT),以PC的低位哈希为索引,每个表项是一个3位的饱和计数器。当由某个PC触发的Cache行在被驱逐前至少被重用一次时,对应的SHCT计数器递增;如果被驱逐时从未被重用,计数器递减。
在插入新行时,SHiP根据触发PC的SHCT计数器值来确定RRPV:如果计数器值高(表示该PC的数据经常被重用),新行的RRPV设为中间值(有一定的保护);如果计数器值低(表示该PC的数据很少被重用),新行的RRPV设为(最容易被替换)。
硬件描述 1 — SHiP的硬件实现
SHiP的硬件实现在RRIP的基础上增加了以下组件:
SHCT表:个表项,每项3位饱和计数器 。以PC的低14位哈希为索引。
每行签名字段:LLC的每个Cache行增加14位,存储填充该行时的PC签名。这用于在行被驱逐时更新SHCT。对于4 MB的LLC切片(4096组16路),总额外存储 = 。
命中时更新逻辑:Cache行被命中时,读出其签名并递增SHCT对应表项;被驱逐但未被重用时,递减SHCT。
SHiP的总硬件开销约为118 KB(以上述配置为例),相对于4 MB的LLC只增加了约3%的面积。SHiP在SPEC CPU 2006上比DRRIP进一步降低了5%10%的LLC缺失率,特别是在混合工作负载(多核心运行不同程序)场景下优势更加明显。
各级Cache的替换策略选择
不同层级的Cache使用不同的替换策略,反映了各层级面临的不同约束:
L1 D-Cache(412-way):由于相联度中等且在关键路径上,通常使用Tree-PLRU或Bit-PLRU。Tree-PLRU以位/组的开销提供了接近True LRU的性能。Intel的L1D(12-way)使用一种改进的PLRU变体。
L1 I-Cache(48-way):由于指令访问模式更加规则(强空间局部性),简单的PLRU或甚至随机替换即可获得良好的命中率。I-Cache的替换策略对性能的影响通常小于D-Cache。
L2 Cache(816-way):L2接收来自L1的缺失流,其访问模式已经经过L1的过滤,局部性较弱。L2通常使用Tree-PLRU或简单的RRIP。AMD Zen系列的L2使用一种自适应的替换策略,根据缺失模式动态调整。
L3/LLC(1216-way):LLC是所有核心共享的,面临来自不同核心的混合访问模式。LLC需要最精细的替换策略来应对扫描型(scanning)工作负载和混合工作负载。DRRIP或SHiP是LLC替换策略的主流选择。Intel从Ivy Bridge开始使用Quad-Age LRU(QLRU),这是一种4级年龄的PLRU变体,每行使用2位年龄计数器。
表表 6.6总结了各替换策略的关键特征。
表中的“相对命中率”以True LRU为基准1.00。RRIP及其变体的命中率超过1.00,是因为LRU在扫描模式下的性能退化——RRIP通过区分短期使用和长期使用的行来避免这种退化。
替换策略的硬件实现时序约束
替换策略的更新操作位于Cache访问流水线的关键路径或旁路径上,其时序约束不容忽视。
对于L1 D-Cache,替换策略的更新发生在每次Cache命中时。在4周期的L1D流水线中,替换状态更新通常被安排在最后一个周期(Stage 4),与数据对齐和寄存器写回并行完成。由于PLRU更新只需修改个位(如8-way只需3位),这个操作的延迟非常短,不会成为关键路径。
对于LLC,替换策略更新的时序更加宽松(因为LLC本身的访问延迟就有3050个周期),但LLC的替换策略更复杂(如SHiP需要更新SHCT表),可能需要额外的流水线级。在实践中,LLC的替换策略更新通常被延迟到数据返回之后异步完成,不在LLC访问的关键路径上。
替换策略状态的读操作(用于选择替换路)发生在Cache缺失时。对于L1D,替换路的选择需要在MSHR分配的同一周期完成,时序约束较紧。对于LLC,替换路选择可以提前准备——维护一个“下一个替换候选”(Next Victim)的预计算值,在缺失发生时直接使用而无需遍历替换状态。
包含式策略
包含式(Inclusive)策略要求:外层Cache中的每一行,在内层Cache中必定也存在一份拷贝。具体而言,L3 Inclusive意味着L3包含L1和L2中所有数据的超集——任何存在于L1或L2中的Cache行,必然也存在于L3中。
包含式策略的主要优点是简化一致性协议。当处理器收到一个对某地址的snoop请求时,只需查询LLC即可确定该地址的状态:
如果该地址在LLC中不存在,那么它一定也不在任何核心的L1或L2中,可以立即回复"未持有"(Not Present),无需向核心发送snoop。
如果该地址在LLC中存在,LLC的目录信息会记录哪些核心持有该行的拷贝,只需向这些核心发送精确的snoop请求。
这种精确过滤能力在核心数较多的处理器中尤为重要:如果没有Inclusive LLC的过滤,每个snoop请求都需要广播到所有核心,snoop流量随核心数的平方增长。
然而,包含式策略有一个显著的缺点:容量浪费和反向无效化(Back-Invalidation)。
容量浪费:由于L3必须包含L1和L2的所有内容,L3中有一部分容量与L1/L2重复。例如,如果一个核心有32 KB L1D + 1 MB L2,而L3中为该核心分配了4 MB,那么L3中有约1 MB的内容与L2完全重复,L3的有效独占容量仅为3 MB。
反向无效化:当L3中的某行因容量不足被替换时,即使这行在某个核心的L1或L2中仍然是活跃的(频繁被访问),它也必须被同时从L1/L2中无效化,以维持包含性。这种反向无效化可能导致L1/L2的命中率意外下降。当L3容量远大于L1+L2的总容量时,反向无效化较为稀少;但当核心数增加导致每核分到的L3容量减小时,反向无效化会变得频繁,成为性能瓶颈。
Intel的处理器从Nehalem(2008年)到Skylake(2015年)一直使用Inclusive LLC策略。从Skylake-SP(服务器版)开始,Intel切换到了Non-Inclusive策略,因为服务器处理器的核心数(28+核)使得每核分到的LLC容量不足以维持包含性。
包含式策略的硬件实现
实现包含式策略需要在LLC中为每个Cache行维护额外的目录信息(Directory),记录哪些核心的L1/L2中持有该行的拷贝。最简单的目录格式是位向量(Bit Vector):对于核处理器,每行需要位的核心在场向量(Presence Vector),其中第位为1表示核心持有该行的拷贝。
位向量的总开销为位/行。对于8核处理器的32 MB LLC(16-way, 64 B行),共有行,位向量总开销 = ,约占LLC容量的1.6%——这是可以接受的。但对于64核处理器,位向量开销增加到,占LLC容量的12.5%——这就开始成为一个不容忽视的开销。
当LLC需要替换一个包含在某些核心L1/L2中的行时,必须先发送反向无效化(Back-Invalidation)请求到持有该行拷贝的核心。反向无效化的流程是:
LLC选定替换牺牲行,查看其位向量确定哪些核心持有拷贝。
向这些核心发送Invalidation请求。
等待所有核心的Invalidation Acknowledgement。如果某核心的行是Dirty的,核心需要将脏数据写回LLC。
收到所有应答后,LLC才能安全地替换该行。
反向无效化的延迟可能很长(1030个周期,取决于互联延迟和核心响应速度),在此期间LLC的替换操作被阻塞,可能导致后续的LLC缺失被延迟。如果反向无效化过于频繁(每核LLC份额太小),这种延迟会显著影响性能。
一个关键的量化指标是反向无效化率(Back-Invalidation Rate):每千条指令中发生的反向无效化次数。设每核LLC份额为字节,核心的L1+L2总容量为字节。在最坏情况下(L1+L2中的所有行都是独占的),LLC中有行(为Cache行大小)需要被包含。如果,反向无效化将变得频繁。
以Intel Skylake客户端为例:每核,每核LLC份额。,即LLC份额是私有Cache的7倍——这提供了足够的“包含余量”来减少反向无效化。但在Skylake-SP服务器上,,每核LLC份额。——LLC份额仅比私有Cache大30%,反向无效化非常频繁,这正是Intel决定切换到NINE策略的直接原因。
排他式策略
排他式(Exclusive)策略与包含式相反:它要求外层Cache和内层Cache中不存在同一Cache行的重复拷贝。具体而言,如果一个Cache行存在于L1中,它就不会同时存在于L2或L3中;反之亦然。
排他式策略的主要优点是最大化有效容量。由于各级Cache之间没有数据重叠,总有效容量等于各级容量之和。例如,32 KB L1D + 1 MB L2 + 32 MB L3在排他式策略下的总有效容量为,而在包含式策略下仅为32 MB(以L3为准)。
AMD从K7(1999年)开始就采用排他式Cache策略,并在后续的K8、K10、Bulldozer、Zen 15系列中延续了这一传统。在AMD的设计中,L1 D-Cache和L2之间是排他的:
当L1D缺失时,数据从L2取入L1D,同时从L2中删除(evict)该行。
当L1D中的行被替换时,被替换的行移入L2(swap),而非直接丢弃。
这种交换(swap)操作增加了L1-L2之间的数据移动量,但确保了被L1替换的数据不会立即丢失——它会在L2中得到"第二次机会"。
排他式策略的缺点包括:
(1)一致性处理复杂。由于一个Cache行可能只存在于L1或L2中的某一级,snoop请求必须检查所有级别的Cache,增加了snoop的延迟和复杂度。在AMD的设计中,snoop需要同时查询L1D、L2和L3,这需要精心设计snoop通道以避免阻塞正常的Cache访问。
(2)数据交换开销。L1-L2之间的排他性要求在Cache行移动时进行swap操作,增加了L1-L2之间的带宽需求和延迟。具体而言,每次L1缺失(命中L2)都需要两个方向的数据传输:L2L1传输请求的行,L1L2传输被替换的行。如果L1的缺失率为5%,则排他式策略下L1-L2接口的数据传输量约为包含式策略(仅需L2L1单向传输)的2倍。
(3)冷启动惩罚。当一个Cache行首次被加载时,它被放入L1,但不会同时在L2中保留拷贝。如果它很快被L1替换,下次再被访问时需要从L3或更远的地方取回。在包含式策略下,该行同时存在于L1和L2中,被L1替换后仍可在L2中找到。
尽管有这些缺点,AMD坚持采用排他式策略超过20年,其原因主要是AMD在每一代产品中都将L2的容量设计得相对较大。在Zen 4中,1 MB的私有L2以排他方式运行,使得每核的有效Cache容量为,高于同代Intel处理器在包含式/NINE策略下的有效容量。
排他式策略的Swap操作实现
排他式策略中L1-L2之间的数据交换(swap)操作是其实现的核心复杂性所在。一次典型的swap流程如下:
L1D发生缺失,请求的行地址为,L1D中将被替换的行地址为(Victim行)。
L1D将行的数据和Tag发送到L2接口。
L2查找行:如果命中,读出行的数据并从L2中删除。
L2同时将从L1D收到的行写入L2(如果L2中有空闲位置或通过替换腾出位置)。
行的数据被返回给L1D并写入L1D的被替换位置。
这个swap操作需要L1D-L2接口在同一事务中同时传输两个方向的数据(从L1D到L2,从L2到L1D),这对接口带宽的要求是包含式策略的2倍。在AMD的Zen系列设计中,L1D到L2的数据通路为32 B/周期双向,以64 B的Cache行计算,完成一次swap需要至少4个周期的数据传输(两个方向各2个周期)。
排他式策略还需要处理一个边角情况:当L1D缺失的行恰好也需要从L3或内存中取回(L2也缺失),那么行仍然需要被写入L2,但行的数据将在稍后从L3返回。在此期间,行可能已经被L2替换策略选中替换——如果此时行又被L1D需要,就会产生额外的L2缺失。这种“ping-pong”现象在工作集略大于L1D但远小于L2时可能发生,需要通过足够大的L2相联度或反ping-pong过滤机制来缓解。
非包含非排他式策略
非包含非排他(Non-Inclusive Non-Exclusive, NINE)策略是包含式和排他式之间的一种折中:它既不要求外层Cache包含内层Cache的所有内容,也不要求各层之间完全没有数据重叠。一个Cache行可能同时存在于多个层级中,也可能只存在于某一个层级中——这完全取决于Cache操作的历史。
NINE策略的典型行为是:
当L1缺失从L2取数据时,数据同时保留在L1和L2中(类似包含式)。
当L2中的行被替换时,不需要反向无效化L1中的对应行(不同于包含式)。如果L1中仍然持有该行,它可以继续使用,直到L1自己替换它。
这意味着某些Cache行可能只存在于L1而不在L2中(类似排他式的效果),但这不是一种强制保证。
Intel从Skylake-SP(2017年)开始在服务器处理器中采用NINE策略,并在后续的Ice Lake-SP、Sapphire Rapids等产品中延续。Intel的LLC每行附带了一个Snoop Filter(一致性过滤器),即使LLC不包含某行的数据,Snoop Filter中仍然记录了哪些核心可能持有该行的拷贝。这样,NINE策略在获得排他式策略的容量优势的同时,通过Snoop Filter保留了包含式策略的一致性过滤能力。
Snoop Filter的硬件实现
NINE策略的Snoop Filter是整个设计的核心难点。Snoop Filter本质上是一个目录(Directory),它跟踪所有可能被核心持有的Cache行的位置信息,但不存储这些行的数据。Snoop Filter的每个表项包含:
行Tag:Cache行的地址标签。
在场向量(Presence Vector):位的位向量,标记哪些核心持有该行。
状态位:该行在各核心中的聚合状态(如是否有核心持有Exclusive/Modified拷贝)。
Snoop Filter的容量需要足以覆盖所有核心的L1+L2中可能持有的所有Cache行。如果每个核心有32 KB L1D + 32 KB L1I + 1 MB L2,总计约1.06 MB,可容纳约16K行。8个核心共计约128K行。Snoop Filter需要至少128K个表项才能避免溢出。如果Snoop Filter溢出(容量不够),需要退化为广播snoop——向所有核心发送snoop请求——这会增加互联流量。
Intel的NINE LLC在每个LLC切片中集成了Snoop Filter功能。由于数据也存储在LLC中(只是不保证包含性),Snoop Filter可以复用LLC的Tag阵列。具体实现方式是:LLC的每个Tag条目增加一组位向量位,即使该行的数据不在LLC中(数据可能只在某个核心的L1/L2中),其Tag和位向量仍然保留在LLC的Tag阵列中。这种设计被称为Tag-only entry——占用LLC的一个路的Tag空间但不存储数据。
Tag-only entry的管理增加了LLC替换策略的复杂度:替换一个Tag-only entry不会释放数据空间(因为它本来就没有数据),而替换一个有数据的entry需要考虑是否应该保留其Tag-only版本以继续提供Snoop Filter功能。
NINE策略的另一个实现挑战是Snoop Filter溢出处理。当LLC中的Tag-only entry数量过多时(因为大量的Cache行只存在于核心私有Cache中但不在LLC的数据部分中),LLC可能面临Tag存储空间不足的问题。处理Snoop Filter溢出有两种策略:
(1)惰性驱逐(Lazy Eviction):当需要为新的Tag-only entry腾出空间时,选择一个现有的Tag-only entry,向其对应的核心发送Invalidation请求,然后释放该entry。这种方式的缺点是会导致核心中有效数据被无效化——本质上退化为了包含式策略的反向无效化行为。
(2)广播退化(Broadcast Fallback):当Tag-only entry的压力超过一定阈值时,停止为新的Cache行创建Tag-only entry,转而使用广播snoop来处理这些行的一致性请求。这增加了互联流量但避免了不必要的数据无效化。
Intel的实现可能结合使用了这两种策略:对于热点行(频繁被snoop的行)保留Tag-only entry以避免广播,对于冷行则允许Tag-only entry被替换,依赖偶尔的广播snoop来处理一致性。
NINE策略的一致性流程示例
以下是NINE策略下一个典型的读缺失处理流程:
核心发出Load,L1D和L2均缺失。
请求被发送到LLC切片(通过地址哈希确定)。
查找Tag阵列:
如果Tag命中且有数据:直接返回数据。更新Presence Vector,标记持有该行。
如果Tag命中但是Tag-only entry:查看Presence Vector确定哪些核心持有数据。如果某核心持有Modified状态的拷贝,向发送Snoop请求获取数据。
如果Tag缺失:该行不在任何核心的Cache中(至少Snoop Filter中没有记录),直接向内存发送请求。
数据返回后,决定是否将数据写入LLC的数据部分(取决于LLC的容量压力和替换策略决策)。
数据被转发给,Presence Vector中标记持有该行。
这个流程展示了NINE策略的灵活性——LLC可以根据当前的容量压力选择性地缓存数据,而Snoop Filter始终维护准确的核心在场信息,确保一致性协议的正确性。
案例研究 2 — Intel的包含性策略演进
Intel在Cache包含性策略上经历了明显的演进:
NehalemBroadwell(20082015):客户端和服务器均采用Inclusive LLC。此时每个处理器最多48核,每核分到的LLC容量为2 MB3 MB,足以维持包含性而不频繁触发反向无效化。
Skylake-SP(2017):服务器处理器核心数增加到28核,每核分到的LLC容量降至约1.375 MB(38.5 MB/28),反向无效化变得频繁。Intel切换到NINE策略,LLC中增加Snoop Filter。
Alder Lake及之后(2021):客户端处理器引入大小核混合架构,核心数进一步增加。客户端也采用NINE策略。
这一演进清楚地展示了包含性策略的选择如何随核心数和每核LLC容量的变化而变化。
表表 6.7总结了三种包含性策略的特征比较。
| 特性 | 包含式 | 排他式 | NINE |
|---|---|---|---|
| 有效总容量 | L3 | L1+L2+L3 | 介于两者之间 |
| 一致性过滤 | 天然支持 | 不支持 | 需Snoop Filter |
| 反向无效化 | 需要 | 不需要 | 不需要 |
| 数据交换开销 | 无 | L1L2 swap | 无 |
| 实现复杂度 | 低 | 高 | 中 |
| 典型采用者 | Intel (至2015) | AMD (K7至今) | Intel (2017至今) |
三种Cache包含性策略的比较
包含性策略的量化分析
为了更深入地理解三种包含性策略对性能的具体影响,我们通过一个参数化的例子进行量化分析。
考虑一个8核处理器,每核具有32 KB L1D(8-way)+ 1 MB L2(8-way),共享32 MB LLC(16-way),每核的LLC份额为4 MB。DRAM延迟为200个周期。
包含式策略的有效LLC容量 = (因为L1+L2的内容必须在LLC中有拷贝)。相比名义容量32 MB,有效容量损失约26%。
排他式策略(L2-L3排他)的有效LLC容量 = (L2和LLC不重叠)。比名义容量多25%。
NINE策略的有效容量介于两者之间,取决于L2驱逐到LLC的行在LLC中的保留率。如果约50%的L2驱逐行仍在LLC中有拷贝,有效容量约为。
这些容量差异对缺失率的影响取决于工作负载的工作集大小。如果工作集在23.7 MB以内,三种策略的LLC缺失率几乎相同。如果工作集在24 MB40 MB之间,排他式策略的LLC缺失率显著低于包含式策略——因为排他式策略的更大有效容量可以容纳更多的工作集。
性能分析 4 — 包含性策略对SPEC CPU的影响
在SPEC CPU 2017的多核混合工作负载模拟中(8核同时运行不同的基准),三种策略的LLC缺失率对比如下(归一化到包含式策略):
| 策略 | LLC缺失率 | 有效容量 | 一致性开销 |
|---|---|---|---|
| 包含式 | 1.00(基准) | 24 MB | 最低 |
| 排他式 | 0.82 | 40 MB | 最高 |
| NINE | 0.91 | 28 MB | 中等 |
排他式策略的缺失率最低(得益于最大的有效容量),但其一致性开销(snoop需要检查所有层级)部分抵消了缺失率的优势。综合考虑缺失率和一致性开销,NINE策略在多数场景下取得了最好的整体性能。
Victim Cache
Victim Cache的原理
Victim Cache是由Norman Jouppi在1990年提出的一种减少冲突缺失(Conflict Miss)的技术。其基本思想非常简单:在L1 Cache的旁边放置一个小容量的全相联Cache,专门用来暂存从L1中被替换(evict)出去的Cache行。当L1发生缺失时,先查询Victim Cache:
如果Victim Cache命中,说明这个Cache行是最近才被L1替换出去的,是一个冲突缺失。此时从Victim Cache取回数据送入L1,延迟仅为12个周期,远低于从L2取回的1014个周期。
如果Victim Cache也缺失,则需要从L2或更低层存储取回数据。
Victim Cache的核心价值在于:它以极小的硬件代价(通常只有416个Cache行的容量)捕获了一类特定的缺失模式——冲突缺失。冲突缺失是指在Cache容量本应足够的情况下,由于多个地址映射到同一组(set),导致有用的行被不必要地替换。在直接映射Cache和低相联度Cache中,冲突缺失是主要的缺失来源之一。
考虑一个典型场景:程序交替访问两个数组A[]和B[],且A[i]和B[i]恰好映射到同一个Cache组。在4-way Cache中,如果同一组内有超过4个频繁访问的地址,就会产生冲突缺失——这些地址反复驱逐彼此。有了Victim Cache,被驱逐的行暂存在Victim Cache中,下次被需要时可以快速取回,从而将冲突缺失的惩罚从L2延迟(1014周期)降低到Victim Cache延迟(12周期)。
Jouppi的原始论文表明,一个4项的Victim Cache可以消除一个4 KB直接映射Cache中约25%30%的冲突缺失;对于组相联Cache,效果相对较小但仍然有意义。
Victim Cache的效果可以用一个简单的概率模型来理解。设一个-way组相联Cache有个组,其中个组经常发生冲突缺失(热点组)。一个项的Victim Cache可以缓存这些热点组最近被替换的个行。如果热点组的冲突模式涉及个频繁访问的行(为溢出量,即比相联度多出的活跃行数),且,则Victim Cache可以完全消除这些冲突缺失——溢出的个行被Victim Cache捕获。
更具体地,考虑一个8-way L1D,某个热点组有10个频繁访问的地址()。没有Victim Cache时,这10个地址在8个路中反复驱逐彼此,产生高频率的冲突缺失。有一个8项的Victim Cache后,最近被驱逐的2个行总是被Victim Cache捕获,后续访问可以在Victim Cache中以12周期的延迟命中,而非产生L2延迟的缺失。
Victim Cache的实现
Victim Cache的硬件实现相当简单,因为其容量很小(通常416项),可以采用全相联组织——每个表项都需要与访问地址进行比较。对于16项的Victim Cache,需要16个并行的Tag比较器,这在面积和延迟上都是可以接受的。
图图 6.6展示了Victim Cache与L1 D-Cache的连接关系。
Victim Cache的操作流程如下:
(1)L1命中:正常返回数据,Victim Cache不参与。
(2)L1缺失,Victim Cache命中:将Victim Cache中的命中行取出,与L1中将要被替换的行交换——命中行送入L1,L1中被替换的行送入Victim Cache。这种swap操作确保了Victim Cache始终存储的是最近从L1中被替换出来的行。Victim Cache命中的延迟通常为12个周期,远低于从L2取回的延迟(1014周期),因此对性能的改善是显著的。
(3)L1缺失,Victim Cache也缺失:从L2取回数据填入L1,L1中被替换的行送入Victim Cache。如果Victim Cache已满,Victim Cache中最旧的行(FIFO策略)或最近最少使用的行(LRU策略)被替换,写回到L2(如果是脏行)。
从另一个角度来理解Victim Cache的作用:一个-way组相联Cache加上一个项的Victim Cache,在某种程度上等效于一个-way的组相联Cache——但仅对冲突最严重的那些组生效。Victim Cache的全相联特性使得任何被L1替换的行都可以暂存其中,不受组的约束。这比简单地将L1的相联度从增加到更加灵活,因为后者会将额外的路均匀分配到所有组中,而Victim Cache可以将所有个表项集中服务于最需要的组。
一个量化的例子:假设一个4-way 32 KB的L1D Cache有512/4=128个组,其中10个组经常发生冲突缺失。一个8项的Victim Cache可以完全覆盖这10个热点组中最近被替换的行。如果改为增加4路变成8-way,虽然每组的容量翻倍,但总容量也翻倍到64 KB,面积成本远高于8项Victim Cache(仅约576 B的SRAM)。
现代处理器中,Victim Cache以不同的形式存在:
AMD Zen系列:L1D和L2之间存在一个8项的micro-op cache victim buffer。
ARM Cortex-A77/X1:L1D附带一个小型的Victim Buffer。
Intel:Intel的现代设计中没有显式的Victim Cache,而是通过较高的L1D相联度(12-way)和较大的容量(48 KB)来减少冲突缺失。这表明当L1D的相联度足够高时,Victim Cache的边际收益较小。
Victim Cache的替换策略
Victim Cache由于容量极小(416项),其替换策略的选择对性能有明显的影响。常见的选择包括:
(1)FIFO(先进先出):最简单的策略。被L1替换出来的行按到达顺序排列在Victim Cache中,最早到达的行最先被替换。FIFO只需要一个头指针(位),没有更新开销。
(2)LRU(最近最少使用):当Victim Cache被命中时(数据被swap回L1),命中的行被标记为最近使用;被L1新替换出来的行被标记为最近使用。对于16项的Victim Cache,True LRU需要位——面积开销可以接受。
(3)随机替换:不维护任何排序信息,使用LFSR(线性反馈移位寄存器)生成伪随机的替换位置。随机替换的性能在统计上接近LRU,但不需要任何状态更新逻辑。
在实践中,48项的Victim Cache通常使用FIFO或LRU,两者的性能差异在1%以内。16项以上的Victim Cache使用LRU可以获得约2%3%的额外命中率提升。
Victim Cache与L2 Cache的关系
一个有趣的观察是:在某种意义上,L2 Cache本身就是一个“大型Victim Cache”——在排他式策略下,L1替换出的行会进入L2,L2为这些行提供了“第二次机会”。Victim Cache可以被视为L1和L2之间的一个小型中间层,对冲突最严重的少数Cache组提供额外的缓冲。
在包含式或NINE策略下,L1替换出的行通常仍然存在于L2中(或至少有很大概率存在),因此独立的Victim Cache的价值更小——L1缺失后直接查询L2就可以在L2命中延迟内获得数据。这也是为什么Intel在采用包含式/NINE策略的同时选择了较高的L1相联度(12-way),而非使用Victim Cache。
设计提示
Victim Cache的价值与L1 Cache的相联度成反比。对于直接映射或2-way L1 Cache,Victim Cache的收益显著(可消除20%30%的缺失);对于8-way或更高相联度的L1 Cache,冲突缺失本身已较少,Victim Cache的收益可能不到5%。因此,在现代高相联度L1 Cache的设计中,Victim Cache的优先级通常低于其他优化(如增大容量或增加预取机制)。
预取
预取(Prefetching)是提高Cache性能最重要的技术之一。预取的核心思想是:在处理器实际需要数据之前,提前将数据从低层存储取入高层Cache。如果预取足够及时(数据在被需要之前已经到达Cache)且足够准确(预取的数据确实会被使用),则可以将Cache缺失的延迟完全隐藏,使得处理器看到的是一个"零缺失延迟"的理想Cache。
然而,预取是一把双刃剑:不准确的预取不仅无法隐藏延迟,还会浪费存储带宽、污染Cache(将有用的行替换为无用的预取数据),甚至降低性能。因此,预取器的设计需要在覆盖率(Coverage,捕获了多少比例的缺失)、准确率(Accuracy,预取的数据中有多少比例被实际使用)和时效性(Timeliness,预取的数据是否在被需要之前到达)之间取得平衡。
硬件预取的基本原理
硬件预取器是处理器中的一个独立模块,它监控处理器的访存行为模式(地址序列),检测可预测的访问模式,并自动发起预取请求。硬件预取器对软件完全透明——程序无需修改任何代码即可受益。
硬件预取器的设计需要回答以下几个基本问题:
(1)何时预取(When to prefetch)?最常见的触发时机是缺失触发(Miss-triggered):当L1或L2发生Cache缺失时,预取器根据缺失地址的模式发起预取。另一种方式是命中触发(Hit-triggered):即使Cache命中,只要检测到可预测的访问模式,就发起预取。命中触发的预取可以更加及时(因为不需要等到缺失发生),但也更加激进。
(2)预取什么(What to prefetch)?预取器需要预测未来将要被访问的地址。最简单的方式是预取下一个Cache行(Next-Line Prefetch),更复杂的方式包括步幅预取(Stride Prefetch)、关联预取(Correlated Prefetch)等。
(3)预取到哪里(Where to place)?预取的数据可以放入L1 Cache、L2 Cache或专用的预取缓冲区。放入L1可以获得最低的后续访问延迟,但如果预取不准确,会污染L1的宝贵容量;放入L2是更保守的选择,即使预取不准确,对L1的影响也较小。
(4)预取多少(How much / Prefetch degree)?一次预取一个Cache行还是多个?更激进的预取度(prefetch degree)可以提高覆盖率,但也增加了带宽消耗和Cache污染的风险。
顺序预取
顺序预取(Sequential Prefetching / Next-Line Prefetching)是最简单也是最广泛使用的硬件预取技术。其原理非常直观:当处理器访问地址所在的Cache行时,预取器自动发起对下一个Cache行(为行大小,通常64字节)的预取请求。
顺序预取的动机来自空间局部性:程序中大量的代码和数据访问是连续的——指令流的顺序执行、数组的顺序遍历、栈帧的线性增长。在这些场景下,如果当前行被访问,下一行很可能很快也会被访问。
顺序预取的变体包括:
(1)单行预取(Next-1-Line)。每次缺失或访问只预取紧接着的下一行。优点是简单、保守、低带宽消耗;缺点是在高缺失率场景下预取不够及时——当访问速度很快时,预取可能来不及赶上访问。
(2)多行预取(Next-N-Line)。每次预取个后续的Cache行。越大,预取越及时(可以在访问到达之前积累更多的预取行),但带宽消耗和Cache污染风险也越大。Intel的L2 Spatial Prefetcher是一种Next-2-Line预取器:它尝试将相邻的一对Cache行一起取入L2。
(3)流预取器(Stream Prefetcher)。更复杂的顺序预取形式,能够跟踪多个独立的地址流。当检测到一个连续的访问序列(至少23次连续缺失)时,流预取器开始激进地预取后续的Cache行,预取深度可以动态调整。
Intel的L2 Streamer是一个经典的流预取器实现。它可以同时跟踪最多32个独立的数据流,每个流可以向前或向后预取最多2 KB的数据(约32个Cache行)。L2 Streamer的工作流程如下:
监控L2的缺失地址。
当检测到两个或以上的连续缺失(相邻Cache行)时,认为发现了一个数据流。
为该流分配一个跟踪表项,记录流的方向(升序/降序)和当前预取位置。
持续预取后续的Cache行,直到流的尽头或被更新的流替换。
动态调整预取深度(prefetch distance):如果预取的数据总是在被需要之前到达,适当增加深度;如果预取的数据经常无用,减小深度。
流预取器的表项结构
流预取器的跟踪表是其核心数据结构。每个表项通常包含以下字段:
流起始地址:检测到该流的第一个缺失地址(或当前缺失前沿地址)。
预取前沿(Prefetch Frontier):预取器已经预取到的最远地址。预取器在每次触发时将前沿推进行。
需求前沿(Demand Frontier):处理器的实际访问已经到达的最远地址。预取前沿始终领先于需求前沿。
方向(Direction):1位,标记流是升序(向高地址)还是降序(向低地址)。
置信度(Confidence):23位计数器,表示该流的可靠性。连续命中增加置信度,不连续的缺失降低置信度。
预取深度(Distance):当前的预取领先量,通常为432行,可动态调节。
有效位(Valid):1位。
以32个表项、42位地址、42位预取前沿、1位方向、3位置信度、5位深度计算,每项约93位,总存储——非常小的硬件开销。
流检测的训练过程
流预取器的训练需要至少23次连续的缺失来确认流的存在。训练过程如下:
(1)第一次缺失到达:在跟踪表中分配一个表项,记录,置信度设为0。此时不发出预取。
(2)第二次缺失到达:如果(相邻行),确认流方向,置信度增加到1。开始发出少量预取(如预取到)。
(3)第三次缺失到达:如果与流方向一致且命中了之前的预取行(说明预取有效),置信度增加到2,进入稳态。预取深度增加。
(4)稳态下:每当需求前沿推进(新的demand缺失命中了预取的行),预取器自动将预取前沿推进相同的量,保持领先距离。
如果连续多次demand缺失未命中预取的行(说明流已终止或发生了方向变化),置信度递减。当置信度降至0时,流被终止,表项可以被新的流替换。
设计提示
流预取器的设计体现了一个重要的预取器设计原则:渐进式训练(progressive training)。预取器不会在看到第一次缺失后就立即开始激进预取,而是通过多次观察来建立置信度,在置信度足够高时才加大预取力度。这种保守的训练策略避免了对偶然的地址序列(如两个碰巧相邻的独立缺失)做出错误的预取响应,从而维持了较高的预取精度。
为了具体理解流预取器的工作方式,考虑以下代码示例:
// 顺序遍历一个大数组
double A[1000000];
double sum = 0;
for (int i = 0; i < 1000000; i++)
sum += A[i];数组A占用约8 MB,远超L1和L2的容量。在没有预取的情况下,处理器每访问一个新的Cache行(包含8个double元素)时都会产生一次L2缺失,延迟约40个周期(L3命中)或200个周期(L3也缺失)。有了流预取器:
前23次缺失被用于检测流的存在(训练阶段)。
之后,预取器开始提前预取后续的Cache行。若预取深度,即预取器始终保持比当前访问位置领先16个Cache行的预取。
假设每个Cache行被访问需要约80个周期(8个元素 约10周期/元素),L3的访问延迟为40个周期,则行即可保证及时性。但由于延迟变化和流水线效应,实际预取器通常使用更大的深度以提供安全余量。
稳态下,几乎所有的Cache行在被需要时都已经在L2中,有效AMAT接近L2的命中延迟。
现代处理器通常在多个Cache层级都部署了顺序预取器。表表 6.8总结了Intel处理器中已知的预取器类型。
| 预取器 | 所在层级 | 功能描述 |
|---|---|---|
| DCU Prefetcher | L1D | 检测顺序访问模式,预取下一行到L1D |
| IP-based Stride | L1D | 基于指令PC检测步幅模式 |
| Spatial Prefetcher | L2 | 预取与当前缺失行配对的相邻行(128B对齐) |
| L2 Streamer | L2 | 跟踪多达32个独立数据流 |
| AMP (Adaptive) | L2 | 自适应预取器,根据访问模式动态选择策略 |
Intel处理器中的硬件预取器(Golden Cove架构)
步幅预取
步幅预取(Stride Prefetching)是顺序预取的自然扩展。顺序预取只能捕获步幅为1(即地址连续递增或递减)的访问模式,而步幅预取可以捕获任意常量步幅(constant stride)的访问模式。
步幅模式在科学计算和数据处理中极为常见:
按列访问二维数组(C语言中的
A[i][j]按j的步幅为数组行宽)。遍历结构体数组(
array[i].field的步幅为结构体大小)。间接数组访问的某些模式。
步幅预取器的核心是一个步幅检测表(Stride Detection Table / Reference Prediction Table, RPT)。RPT通常由Load/Store指令的PC来索引——因为同一条指令在不同迭代中通常产生相同的步幅。
RPT的每个表项包含以下字段:
PC Tag:用于匹配Load/Store指令的地址。
上次地址(Last Address):该指令上次访问的地址。
步幅(Stride):连续两次访问之间的地址差。
状态(State):表示步幅预测的置信度,通常是一个2位的饱和计数器,经历init transient steady no-prediction等状态。
步幅预取器的工作流程如下:
当一条Load指令执行时,用其PC查询RPT。
如果RPT命中,计算当前地址与上次地址的差值。
如果等于已记录的步幅,则置信度增加;如果不等于已记录的步幅,更新步幅为,置信度降低。
当置信度达到稳态(steady state)时,发起预取:预取地址 ,其中是预取深度(prefetch degree)。
更新RPT中的上次地址为当前地址。
硬件描述 2 — 步幅预取器的RPT状态机
RPT的状态转移可以用一个简单的状态机描述:
只有在Steady状态下,预取器才会发起预取请求。这种保守的状态机设计避免了对不稳定步幅的误预取。
步幅预取器在Intel和AMD的现代处理器中都有实现:
Intel:L1D有一个IP-based stride prefetcher(基于指令PC的步幅预取器),L2有一个独立的stride prefetcher。
AMD Zen 4/5:L1D和L2各有步幅预取器,L2的预取器可以检测复杂的访问模式,包括嵌套循环产生的步幅。
步幅预取器的硬件实现细节
RPT(Reference Prediction Table)的典型大小为64256项。RPT的大小决定了预取器可以同时跟踪多少条不同的Load指令的步幅模式。在一个复杂的应用程序中,可能同时有数十条不同的Load指令具有可预测的步幅模式(例如,多个嵌套循环中的不同数组访问),因此需要足够大的RPT来容纳它们。
RPT每个表项的位宽分析如下。假设处理器使用48位虚拟地址、64 B Cache行,则Cache行地址为位:
PC Tag:使用PC的低位作为索引(如8位对256项RPT),高位的1014位作为Tag。
上次地址(Last Address):42位行地址。
步幅(Stride):有符号整数,表示连续两次访问之间的行地址差。步幅的范围取决于应用场景:对于结构体遍历,步幅通常较小(164行),710位的有符号数即可覆盖。但对于矩阵的列访问(步幅等于矩阵行宽),步幅可能达到数千行,需要1214位。
状态(State):2位饱和计数器或2位状态编码。
有效位:1位。
以256项RPT、14位Tag、42位上次地址、12位步幅、2位状态、1位有效位计算,每项71位,总存储 = 。这是一个非常小的硬件开销。
RPT的查找操作与Cache的Tag比较类似:用Load指令的PC低位索引RPT,读出对应表项,将PC高位与RPT Tag进行比较。如果匹配,则检测步幅模式并可能发出预取。整个操作可以在Load指令执行的同一周期完成(与L1D Tag比较并行),不增加额外的延迟。
步幅预取的预取地址生成
在Steady状态下,步幅预取器为地址生成预取请求。预取地址可以是一个或多个:
其中为检测到的步幅,为预取深度。
预取深度的选择是一个重要的调优参数。设处理器每次迭代执行一条步幅Load(周期数为),下级Cache的命中延迟为,则为了使预取数据在需要时已经到达,要求:
例如,周期(L2延迟),周期/迭代,则。通常的取值范围为18,并可以根据运行时反馈动态调整。
一个需要注意的约束是预取地址的页面边界问题。步幅预取生成的地址可能跨越虚拟页面的边界(4KB页面对应64个Cache行)。跨页预取的问题在于:下一个页面的虚拟到物理地址映射可能与当前页不同,预取器使用当前页的映射关系生成的物理地址可能是错误的。为了避免这个问题,大多数步幅预取器在检测到预取地址跨越页面边界时会停止预取,等待实际访问进入新页面后再重新开始。这被称为页面边界截断(Page Boundary Clipping)。
步幅预取与流预取的协同
一个有趣的设计问题是步幅预取器和流预取器之间的协同与冲突。两者可能同时识别到同一个访问模式,并各自发出预取请求,导致重复预取。现代处理器通过预取去重(Prefetch Deduplication)机制来解决这个问题:在发出预取请求之前,检查目标地址是否已经在Cache中或已经有一个未完成的预取请求(通过查询MSHR)。如果是,则抑制这次预取。
更精细的协调策略是让步幅预取器和流预取器负责不同的模式范围:
流预取器负责步幅为的顺序流——这是最常见的模式,流预取器可以用最低的开销高效覆盖。
步幅预取器负责步幅的非顺序模式——这是流预取器无法覆盖的,需要RPT来跟踪每条指令的步幅。
这种分工避免了两个预取器对同一模式的冗余竞争,同时最大化了预取覆盖率。
此外,更先进的预取算法正在学术界和工业界持续发展,这些将在第第 7.0 章章中详细讨论:
空间模式预取(SMS, Spatial Memory Streaming):学习和记录程序对固定大小内存区域的访问位图模式,在检测到类似触发条件时重播已记录的模式。SMS特别适合结构体数组遍历等具有重复空间模式的场景。
Best Offset Prefetcher(BOP):动态评估一组候选偏移值中哪一个在当前阶段最有效,然后使用该最优偏移值进行简单的地址加偏移预取。BOP以极低的硬件开销(不到1 KB存储)赢得了DPC-3竞赛冠军。
关联预取(Correlated Prefetching):利用地址之间的因果关联关系进行预取,适用于不规则的访问模式,如链表、图遍历等。代表性算法包括Markov预取器和GHB(Global History Buffer)预取器。
基于时间的预取(Temporal Prefetching):记录历史的地址序列,在未来识别到相同的序列时重放预取。STMS(Spatial-Temporal Memory Streaming)和ISB(Irregular Stream Buffer)是两种有代表性的实现。
基于机器学习的预取:使用神经网络或其他机器学习模型来预测未来的访问地址。虽然学术研究显示了巨大的潜力(如Voyager预取器),但硬件实现的面积和功耗开销目前仍然过大,尚未在商用处理器中广泛采用。
软件预取指令
除了硬件自动预取外,处理器还提供软件预取指令,允许程序员(或编译器)显式地发起预取请求。软件预取指令的典型形式是:
x86:
PREFETCHT0(预取到所有Cache层级)、PREFETCHT1(预取到L2及以上)、PREFETCHT2(预取到L3及以上)、PREFETCHNTA(Non-Temporal预取,尽量不污染Cache)。ARM:
PRFM指令,支持多种预取类型和目标Cache级别。RISC-V:
PREFETCH.R(预取用于读取)、PREFETCH.W(预取用于写入),定义在Zicbop扩展中。
软件预取指令是提示性(hint)的:处理器可以忽略预取请求而不影响程序的正确性。预取指令不会产生异常——即使预取地址是非法的或未映射的,处理器也会默默地丢弃该预取请求。
软件预取的典型使用场景是在循环体中提前若干迭代预取数据:
for (int i = 0; i < N; i++) {
__builtin_prefetch(&A[i + DISTANCE]); // 提前DISTANCE次迭代预取
sum += A[i] * B[i];
}其中DISTANCE的选择取决于Cache缺失延迟和每次迭代的执行时间。如果每次迭代需要10个周期,L2缺失延迟为200个周期,则DISTANCE应约为次迭代。
然而,软件预取在现代处理器中的使用已大幅减少,原因包括:
(1)硬件预取器已经足够好。现代处理器的硬件预取器(流预取器+步幅预取器+其他高级预取器)可以自动捕获大部分可预测的访问模式,软件预取的边际收益较小。
(2)指令开销。预取指令本身占用取指和解码带宽,在高度优化的代码中,这些带宽损失可能抵消预取带来的收益。
(3)可移植性差。不同处理器的Cache层次结构参数不同,最优的预取距离也不同。针对特定处理器优化的预取代码可能在其他处理器上反而降低性能。
(4)乱序执行的缓冲效应。现代乱序处理器有较深的指令窗口(300600条指令),可以自然地将多个Cache缺失重叠执行(Memory-Level Parallelism),部分达到了预取的效果。
尽管如此,在某些特定场景下,软件预取仍然有价值——特别是当访问模式不规则(irregular)、硬件预取器无法捕获时。例如,链表遍历中基于当前节点的指针提前预取下一个节点:
node *p = head;
while (p != NULL) {
__builtin_prefetch(p->next); // 预取下一个节点
process(p->data);
p = p->next;
}这种指针预取(Pointer Prefetching)是硬件预取器无法自动检测的,因为下一个地址隐藏在当前节点的数据中,只有在数据被加载后才能知道。
另一个软件预取仍然有价值的场景是数据库系统中的哈希表探测(hash table probing)。在哈希连接(Hash Join)操作中,内表的每一行需要用哈希值查找外表的哈希桶,这些哈希桶地址本质上是随机分布的,硬件预取器无法预测。但软件可以利用组预取(Group Prefetching)技术:先批量计算多行的哈希值并发出预取,然后再批量进行实际的哈希桶探测。这种技术在实际的数据库系统(如ClickHouse、DuckDB)中被广泛使用,可以将哈希连接的性能提高23倍。
RISC-V的Zicbop扩展定义的PREFETCH.R和PREFETCH.W指令值得特别关注。与x86的预取指令不同,RISC-V的预取指令使用标准的Load指令格式编码,将预取目标地址编码在base+offset字段中。PREFETCH.W告诉处理器该地址将被写入,因此处理器可以以Exclusive状态获取该Cache行(在MESI协议中),避免后续写入时还需要额外的一致性升级(Upgrade)操作。
软件预取的最佳实践
尽管硬件预取器在大多数场景下已经足够好,软件预取在以下场景仍然有不可替代的价值:
(1)间接数组访问。当访问模式为A[B[i]]时,B[i]的值决定了A中被访问的位置。硬件预取器无法预测A[B[i]]的地址(因为它依赖于B[i]的数据内容)。但软件可以通过“预取管道化”来优化:在当前迭代中计算B[i+d]并预取A[B[i+d]],其中为预取距离。
(2)哈希表探测。数据库的哈希连接操作需要用键值的哈希值随机访问哈希表,这种完全随机的访问模式对硬件预取器来说是不可预测的。软件的“批量预取-批量探测”(Group Prefetching)技术可以显著提高性能。
(3)树/图遍历的“前瞻”预取。在B+树索引查找中,软件可以在遍历当前节点时预取子节点的数据,利用树结构的可预测性来提前加载数据。
一条重要的实践原则是:软件预取的收益需要大于其指令开销。每条预取指令本身消耗取指、解码和执行的流水线资源。如果预取指令过于密集,它们可能占用过多的执行带宽,反而降低了实际计算指令的吞吐量。一条经验规则是:每条预取指令应该覆盖至少48个有效数据访问,否则预取指令的开销可能超过其收益。
另外一个实践考虑是预取指令与异常处理的交互。在x86中,预取指令访问一个不存在的页面不会产生页面错误——预取被默默丢弃。但在某些场景下(如NUMA系统中跨节点的预取),预取可能触发远端内存访问,其延迟可能高达数百纳秒。如果预取的远端数据最终并未被使用,这种长延迟的预取反而增加了互联的压力。因此,在NUMA感知的系统中,软件预取需要特别注意只预取本地节点的数据,避免跨节点的无效预取。
预取的负面效应
预取并非免费的午餐。不当的预取可能带来以下负面效应:
(1)Cache污染(Cache Pollution)。预取的数据占用了Cache空间。如果预取的数据最终没有被使用(准确率低),它就白白驱逐了Cache中原本有用的数据,导致后续对这些被驱逐数据的访问从Cache命中变成Cache缺失——预取反而增加了缺失率。
Cache污染的严重程度取决于预取的准确率和Cache容量:
准确率90%以上:污染可忽略,净收益显著。
准确率70%90%:轻微污染,通常仍有净收益。
准确率50%以下:污染严重,可能导致性能下降。
(2)带宽浪费。每次预取都需要消耗存储总线的带宽。在带宽受限的系统中(如多核处理器共享有限的内存带宽),不准确的预取会与实际的需求缺失(demand miss)竞争带宽,导致需求缺失的延迟增加。
考虑一个具体的例子:一个8核处理器共享50 GB/s的DDR5内存带宽。如果每个核心的硬件预取器以50%的准确率产生5 GB/s的预取流量,那么8个核心的预取流量为40 GB/s,几乎耗尽了全部可用带宽,只留下10 GB/s给实际的需求访问。在这种情况下,预取非但没有帮助,反而使需求访问的延迟因带宽饱和而显著增加。
现代处理器通过以下机制来控制预取的带宽消耗:
节流(Throttling):当检测到内存带宽接近饱和时,自动降低预取的激进程度(减少预取深度或完全暂停预取)。
优先级:需求访问的优先级始终高于预取。当需求访问和预取访问竞争时,优先服务需求访问。
精确度反馈:跟踪预取数据的使用率,如果使用率持续偏低,自动降低预取激进程度。
(3)时效性问题(Timeliness)。预取的时机至关重要:
过早预取(Too early):如果预取数据到达Cache太早,它可能在被实际需要之前就已经被其他数据替换出Cache,预取白做了。
过晚预取(Too late):如果预取请求发出太晚,数据在被需要时尚未到达Cache,处理器仍然需要等待——但等待时间比完全不预取要短(因为预取请求已经在路上了),这种情况称为部分命中(partial hit)。
恰好及时(Just in time):理想情况下,预取的数据在被需要前的12个周期恰好到达Cache。
预取深度(prefetch distance)的选择直接影响时效性。设需求访问之间的平均间隔为个周期,缺失延迟(从发出请求到数据到达)为个周期,则理想的预取深度为:
例如,L2缺失延迟为200个周期,顺序访问每10个周期一次,则行。
图图 6.7用时间轴的方式展示了预取过早、恰好及时和过晚三种情况的时序关系。
(4)与乱序执行的交互。在乱序处理器中,硬件预取器和乱序执行引擎都试图隐藏Cache缺失延迟,但它们之间可能产生不良交互。例如,乱序窗口已经将两个独立的Cache缺失重叠执行(Memory-Level Parallelism),而预取器又额外产生了预取请求,导致不必要的带宽消耗。理想的预取器应当感知乱序引擎的状态,只为乱序引擎无法自然覆盖的缺失发起预取。
(5)MSHR资源竞争。预取请求和demand请求共享MSHR资源。当MSHR接近耗尽时,激进的预取可能占用宝贵的MSHR表项,导致真正的demand缺失因为找不到空闲MSHR而被迫等待。这种情况下,预取反而增加了demand请求的有效延迟。为此,现代处理器通常实现MSHR水位线保护:当空闲MSHR数量低于某个阈值(如23个),停止接受新的预取请求,保留剩余MSHR给demand请求。
(6)一致性协议开销。在多核系统中,预取一个Cache行可能触发一致性协议的交互。例如,在MESI协议下,预取一个其他核心正在使用的行需要先发送Invalidation请求,这不仅增加了互联流量,还可能导致其他核心的Cache行被无效化。如果预取的行最终并未被使用(不准确的预取),这种一致性开销就是纯粹的浪费。
预取带宽放大效应
预取的一个被经常忽视的副作用是带宽放大(Bandwidth Amplification)。设原始的demand缺失带宽需求为,预取请求的带宽需求为,预取准确率为。则预取的有用带宽为,浪费的带宽为。总带宽需求为:
其中为覆盖率。化简得带宽放大因子:
当(完美准确率)时,放大因子为1——预取不增加任何额外带宽。当(50%准确率)、(80%覆盖率)时,放大因子为——预取使总带宽需求增加了80%。在带宽受限的系统中,这种放大效应可能导致严重的性能下降。
性能分析 5 — 预取性能的量化评估
预取的性能效果可以通过以下指标量化:
覆盖率(Coverage)
准确率(Accuracy)
时效率(Timeliness)
设原始缺失率为,预取后的有效缺失率为:
其中是覆盖率,是时效率,是预取污染引起的额外缺失。好的预取器追求高、高、低,同时控制带宽消耗在可接受的范围内。
典型的高性能处理器的L2预取器可以达到覆盖率60%80%、准确率70%90%,将L2的有效缺失率降低一半以上。
非阻塞Cache与MSHR概述
在传统的阻塞Cache设计中,当一次访存操作发生Cache缺失时,Cache被"锁定"——后续的所有访存操作都必须等待缺失处理完成才能继续。这在现代乱序超标量处理器中是不可接受的性能瓶颈。
非阻塞Cache(Non-blocking Cache,也称Lockup-free Cache)允许Cache在处理一个或多个缺失的同时,继续服务后续的访存请求。这一能力是通过MSHR(Miss Status Holding Register,缺失状态保持寄存器)实现的。每个MSHR表项跟踪一个未完成的缺失请求的状态,包括缺失的Cache行地址、请求来源、以及等待该数据的所有处理器端请求列表(Target List)。
非阻塞Cache的"非阻塞"特性体现在两个层面:
Hit under miss:在缺失正在处理时,后续的Cache命中操作仍然可以正常完成。
Miss under miss:在缺失正在处理时,后续的新缺失也可以被接受并开始处理,只要有空闲的MSHR表项。这是实现MLP(Memory-Level Parallelism,内存级并行度)的前提条件。
当多个对同一Cache行的缺失请求到达时,它们被合并(merge)到同一个MSHR表项的Target List中,只向下级存储发送一次请求。这种缺失合并(Miss Merging)既节省了下级存储的带宽,又节省了MSHR资源。
现代高性能处理器的L1D通常配置1016个MSHR,支持同时处理多个未完成的缺失;L2配置3248个MSHR,以应对更长的下级延迟。MSHR的数量直接限制了处理器可以利用的MLP,是影响存储密集型工作负载性能的关键参数。
MSHR的一个关键设计决策是选择显式MSHR还是隐式MSHR。显式MSHR使用一个独立于Cache的专用硬件表来跟踪缺失状态,其优点是不影响Cache的有效相联度,缺点是需要额外的面积用于CAM搜索逻辑。隐式MSHR将缺失信息直接嵌入Cache行的Tag/Data字段中,复用Cache的Tag比较逻辑来搜索未完成的缺失,优点是零额外面积,缺点是每个使用中的MSHR占用一个Cache路,降低了有效相联度。大多数高性能处理器使用显式MSHR。
在多级Cache层次中,各级Cache的MSHR协同工作形成一个嵌套的缺失跟踪层次:L1D缺失首先由L1D的MSHR跟踪,请求发送到L2;如果L2也缺失,L2的MSHR跟踪该请求并进一步发送到L3。当L2的MSHR全部耗尽时,背压(Back-pressure)向上传播到L1D——L1D发出的新缺失请求被L2拒绝,L1D的MSHR也可能随之耗尽,最终导致处理器的Load/Store单元停滞。这种级联背压是存储密集型工作负载性能下降的主要原因之一。
MSHR的详细结构、操作流程和设计权衡将在第第 8.0 章章中深入讨论。
性能分析 6 — MSHR数量对性能的定性影响
MSHR的数量对性能的影响高度依赖于工作负载的MLP特征。对于MLP较高的工作负载(如独立的数组遍历),增加MSHR数量可以带来接近线性的加速——直到MSHR数量达到工作负载的自然MLP上限。对于MLP较低的工作负载(如指针追踪,每次load依赖前一次load的结果),即使有大量MSHR,也只能有1个同时在使用中,增加MSHR数量几乎没有收益。
以下经验数据可以作为参考:
从1个MSHR增加到4个:SPEC CPU整型平均IPC提升约20%30%。
从4个增加到8个:平均IPC提升约10%15%。
从8个增加到16个:平均IPC提升约5%8%。
从16个增加到32个:平均IPC提升3%。
边际收益递减是选择1016个L1D MSHR的主要原因——更多的MSHR带来的性能提升不足以抵消额外的面积和功耗开销。
本章小结
本章系统地讨论了提高Cache性能的主要技术。表表 6.9从AMAT的三个分量出发,总结了各技术的优化目标和实现代价。
在实际的处理器设计中,这些技术通常组合使用:一个现代高性能处理器的Cache子系统会同时包含写缓存(812项带写合并)、流水线化的L1 Cache(45段,带路预测)、三级Cache层次(采用NINE或Exclusive包含策略)、先进的替换策略(RRIP/SHiP用于LLC)、Victim Buffer、非阻塞Cache(L1D 1016个MSHR,L2 3248个MSHR)、以及多级硬件预取器(L1的顺序/步幅预取器 + L2的流预取器)。这些技术的协同工作使得现代处理器的AMAT可以控制在57个周期——接近L1 Cache的命中延迟,远低于数百周期的主存延迟。
设计权衡 3 — Cache性能优化的全局视角
从AMAT公式出发,我们可以将本章讨论的技术分为三大类别,每类针对不同的AMAT分量:
降低:Cache流水线化和路预测通过精心的流水段划分和推测性路选择,在不显著增加面积的前提下维持了低周期数的命中延迟。路预测的准确率(85%95%)直接影响有效命中延迟:设路预测错误惩罚为周期,则有效。
降低(缺失率):多级Cache层次和预取器是降低缺失率的主要手段。多级Cache通过增加总容量来降低容量缺失;预取器通过提前取入数据来将缺失转化为命中。LLC的替换策略(RRIP/SHiP)通过更智能的驱逐决策来降低LLC的缺失率。Victim Cache针对性地降低冲突缺失。
降低(缺失代价):非阻塞Cache通过允许多个缺失并行处理(MLP),有效地将多个缺失的代价重叠为单个缺失的代价。写缓存通过缓冲写操作来隐藏写延迟。关键字优先和提前开始(第第 8.0 章章讨论)通过优化Cache行填充的传输顺序来缩短缺失延迟。
这三类优化之间存在复杂的交互。例如,增大L1 Cache的容量可以降低缺失率,但可能增加命中延迟(更大的SRAM更慢);激进的预取可以降低有效缺失率,但如果预取不准确,Cache污染反而会增加缺失率。处理器设计者需要在整个AMAT空间中寻找全局最优点,而非在单一维度上过度优化。
为了将本章讨论的内容具体化,我们以一个假想的但参数接近真实处理器的Cache子系统为例,计算各项技术对AMAT的贡献。
性能分析 7 — 综合AMAT分析实例
考虑一个5GHz超标量处理器,Cache参数如下:
L1D:48 KB/12-way,命中延迟5周期,缺失率3%(无预取)
L2:1 MB/10-way,命中延迟14周期,L2局部缺失率15%
L3:32 MB/16-way,命中延迟45周期,L3局部缺失率8%
DRAM:延迟200周期
写缓存:12项,写停顿概率1%
预取器:覆盖率70%,准确率85%,Cache污染导致缺失率增加0.2%
无预取时的AMAT:
有预取时:L1D有效缺失率 (预取覆盖了70%的缺失,但引入0.2%的污染缺失)。
预取使AMAT从5.69降低到5.25周期,改善了7.7%。在IPC约为4的处理器上,这大约等效于0.3的IPC提升——一个非常可观的收益。
面向2030年代的处理器设计,Cache优化技术仍在不断演进。几个值得关注的前沿方向包括:
(1)3D堆叠Cache。AMD的3D V-Cache已经展示了通过芯片堆叠大幅增加L3容量的可行性(从32 MB到96 MB128 MB)。未来,3D堆叠技术可能延伸到L2甚至L1层级,通过在逻辑层上方堆叠高密度SRAM层来突破面积限制。3D堆叠的一个关键挑战是散热——上层SRAM die会增加热阻,限制下方逻辑die的散热能力。AMD通过限制V-Cache die的功耗密度(SRAM的功耗远低于逻辑电路)来缓解这一问题。
(2)智能预取。基于程序行为分析和机器学习的预取器正在从学术研究走向商用实现。这些预取器能够捕获传统步幅和流预取器无法识别的复杂访问模式,如间接数组访问和图遍历。一种可能的部署路径是在L2或LLC级别使用轻量级的ML模型作为传统预取器的补充,只在传统预取器低置信度或未激活时启用ML预取。
(3)自适应Cache管理。根据工作负载的实时特征动态调整Cache的参数,如替换策略、预取激进程度、甚至相联度和容量划分。Intel的硬件反馈机制和AMD的自适应替换策略都是这一方向的早期实例。未来可能出现的技术包括基于工作负载相位检测的Cache参数自动调优——当检测到程序进入不同的执行阶段(如从计算密集切换到内存密集)时,自动调整L1/L2的预取策略和LLC的替换策略。
(4)近数据处理(Processing Near Data)。将部分计算逻辑移到Cache或内存附近,减少数据移动。例如,在L3 Cache的slice中嵌入简单的计算单元,对驻留在该slice中的数据直接执行操作,避免数据在核心和LLC之间的往返传输。
(5)异构Cache层次。随着大小核架构(如Intel的P-core/E-core)的普及,Cache层次也呈现异构化趋势。P-core可能有更大的L1/L2和更激进的预取器,而E-core则使用更小更简单的Cache配置以优化能效比。两种核心共享同一个LLC,LLC的替换和分区策略需要考虑不同核心类型的访存特征差异。
(6)Chiplet间Cache一致性。随着chiplet架构的普及(AMD的CCD/IOD设计、Intel的EMIB/Foveros方案),不同chiplet上的Cache层次需要通过片间互联(die-to-die interconnect)维持一致性。片间互联的带宽和延迟远高于片内互联,这对Cache一致性协议的设计和LLC的分区策略提出了新的挑战。AMD的Infinity Fabric和Intel的CXL(Compute Express Link)协议是当前解决这一挑战的主要方案。
(7)软件感知的Cache管理。未来的处理器可能通过ISA扩展向软件暴露更多的Cache管理能力。例如,RISC-V的CMO(Cache Management Operations)扩展允许软件显式地清理、无效化或预锁定特定的Cache行。x86的CLDEMOTE指令允许软件将一个Cache行从L1降级到L2,为其他高优先级数据腾出L1空间。这些ISA层面的Cache管理接口使得系统软件和编译器可以与硬件预取器和替换策略协同优化Cache的使用效率。
(8)SRAM替代技术。传统的6T SRAM面临面积缩放瓶颈,研究者正在探索替代存储技术用于Cache实现。STT-MRAM(Spin-Transfer Torque Magnetic RAM)具有更高的密度和零静态功耗,但写延迟和写功耗较高,适合用于LLC这种写入频率相对较低的层级。eDRAM(嵌入式DRAM)曾被IBM用于L3 Cache(如Power7处理器),提供比SRAM更高的密度但需要周期性刷新。这些替代技术可能在2030年代成为Cache实现的选项之一,特别是在对功耗极度敏感的数据中心和边缘计算场景中。
设计提示
前向桥接。本章介绍的顺序预取和步幅预取只能捕获规则的地址模式——步幅为常数的数组遍历。然而,现代工作负载中大量的访存模式是不规则的:间接数组访问A[B[i]]、链表和图的指针追踪、数据库索引的随机探测。这些模式的下一个地址隐藏在当前数据的内容中,传统预取器束手无策。第 7.0 章将深入讨论面向这些挑战的高级预取技术:空间模式预取(SMS)学习和重放内存区域的访问位图,Best Offset Prefetcher(BOP)以极低开销动态选择最优预取偏移,关联预取和时间预取利用历史地址序列的因果关系预测未来访问。这些技术本质上仍是投机——投机地假设历史中观察到的访问模式会在未来重现——但投机的策略从简单的线性外推升级为对复杂模式的学习与匹配。
更远地看,即使预取完美地工作,Cache每周期仍然只能服务一个访存请求。当超标量处理器每周期发起35个并发的Load/Store操作时,单端口Cache将成为带宽瓶颈。第 8.0 章将面对这个多端口Cache的挑战:通过Multi-banking将SRAM阵列分为多个独立的Bank,让不同的请求并行访问不同的Bank——以Bank冲突为代价换取多端口效果。