5.3 通过监听维护缓存一致性
如前所述,监听既可以用来维护单个处理器及其私有缓存的一致性,也可以用来维护通过广播介质连接的一小组处理器的一致性。下面假定这一广播介质是总线。我们以多个处理器连接在同一总线上的情况来说明协议;但对于单个处理器及其缓存,基本监听操作完全相同。
总线的关键优势在于,它具有广播能力,因此很容易完成一致性协议中的两项关键操作:使副本失效,以及找到共享数据项的最新值。例如,要使某个块失效,处理器只需取得总线访问权,在总线上广播需要失效的地址。所有处理器持续监听总线,观察地址,并检查该地址对应的数据项是否在自己的缓存中;如果在,就使缓存中对应的项失效。
写入一个共享块时,写入处理器必须取得总线访问权,广播失效请求。如果两个处理器同时尝试写共享块,它们广播失效操作的尝试,会在总线仲裁时被串行化。先取得总线访问权的处理器,会使它所写入块的所有其他副本失效。如果两个处理器尝试写的是同一个块,总线所施加的串行化也就使两次写入串行化。这一方案的一个含义是:对共享数据项的写入,在获得总线访问权之前不能完成。稍后讨论实现细节时,我们还会回到这一问题。所有一致性方案都必须用某种办法,将对同一缓存块的访问串行化:可以串行化对通信介质的访问,也可以串行化对其他共享结构的访问,例如下一节介绍的目录。
除了使被写入块的现存副本失效,缓存未命中时还必须找到数据项。在直写缓存中,很容易找到数据的最新值,因为所有写入数据都会送往内存,因而始终可以从内存取得最新值。写缓冲区会增加一些复杂性,必须将其有效地视为额外的缓存项。
对于写回缓存,寻找最新值更困难,因为数据项的最新值可能位于某个私有缓存,而非共享缓存或内存中。幸运的是,写回缓存可以对缓存未命中和写操作采用同一种监听方案:每个处理器监听放到共享总线上的每个地址。如果某处理器发现自己持有所请求缓存块的脏副本,就响应读请求,提供该块,并中止对内存或 LLC 的访问。额外复杂性来自必须从另一个处理器的私有 L1 或 L2 缓存取得块;这通常比从 LLC 取得它更慢。写回缓存对内存带宽的要求较低,因此能支持更多、更快的处理器。于是,所有多核处理器的 L2 和 L3 都采用写回缓存,下面也以写回缓存来考察一致性的实现。
正常的缓存标签就能用于监听,每个块的有效位则使失效操作易于实现。读未命中也很好处理,无论它由失效还是其他事件引起,都只需依赖监听能力。对于写操作,我们希望知道是否有其他缓存持有该块副本:如果没有其他副本,写回缓存就不必把写操作放到总线上。省去这一总线操作,既缩短写入时间,也降低带宽需求。
为了跟踪一个缓存块是否共享,可以像有效位和脏位一样,为每个块再增加一个状态位。加入表示共享与否的位后,就可以判断一次写入是否必须生成失效请求。写入处于共享状态的块时,缓存会在总线上发出失效请求,并将该块标记为独占。此后,这个核心不再为该块发送失效请求。持有某缓存块唯一副本的核心,称为该块的拥有者(owner)。
失效请求发送后,拥有者缓存中的块从共享状态转为非共享,即独占状态。如果之后其他处理器请求这个块,状态必须重新变为共享。由于监听缓存也能看到所有未命中,因此它知道其他处理器何时请求了这个独占块,也就知道何时应将状态改为共享。
每次总线事务都必须检查缓存地址标签,这可能干扰处理器自身的缓存访问。一种减少干扰的办法是复制标签,让监听访问检查标签副本。如果 LLC 具有包含性,即 L1 或 L2 中的所有项都存在于 LLC,就可以用 LLC 判断是否需要监听。这样,具有包含性的 LLC 能区分哪些未命中可以直接访问内存,哪些需要发起监听。许多近期多核处理器要求 L2 具有包含性,但 LLC 不必如此;这种情况下,可以监听 L2,只有 L2 命中时才监听 L1。L2 还可能分成多个存储体以提高带宽,从而降低争用。
一个协议示例
监听一致性协议通常在每个核心中加入有限状态控制器来实现。控制器响应核心内处理器和总线或其他广播介质的请求,改变选中缓存块的状态,并利用总线访问数据或使其失效。从逻辑上,可以认为每个块都关联一个独立控制器;也就是说,对不同块的监听操作或缓存请求可以独立进行。实际实现中,一个控制器允许针对不同块的多个操作交错执行:即使一次只能进行一个缓存访问或一次总线访问,也可以在一个操作尚未完成时启动另一个操作。在具有非阻塞缓存的处理器中,这些事务能够重叠。可以利用未命中状态处理寄存器(MSHR)确定完成某个事务需要更新哪个缓存块。还要记住,虽然下文称之为总线,但任何支持向所有一致性控制器及其私有缓存广播的互连网络,都能用来实现监听。同一协议也可以维护各个核心的一致性,并与目录机制连接,以获得更好的可扩展性。
我们考虑的简单协议具有三个状态:无效(Invalid)、共享(Shared)和已修改(Modified)。共享状态表示私有缓存中的块可能被共享;已修改状态表示该块已在私有缓存中更新。注意,已修改状态意味着该块为独占。
图 5.5 列出了核心产生的请求,位于表格上方约三分之二,以及来自总线的请求,位于下方约三分之一。对这一基本协议最常见的扩展,是增加一个独占状态,表示块尚未修改,且仅保存在一个私有缓存中。本节后文将讨论这一扩展及其他扩展。
| 请求 | 来源 | 被寻址缓存块的状态 | 缓存操作类型 | 功能与说明 |
|---|---|---|---|---|
| 读命中 | 处理器 | 共享或独占 | 普通命中 | 读取本地缓存中的数据。 |
| 读未命中 | 处理器 | 无效 | 普通未命中 | 在总线上发出读未命中。 |
| 读未命中 | 处理器 | 共享 | 替换 | 地址冲突未命中:在总线上发出读未命中。 |
| 读未命中 | 处理器 | 独占 | 替换 | 地址冲突未命中:写回缓存块,然后在总线上发出读未命中。 |
| 写命中 | 处理器 | 独占 | 普通命中 | 写入本地缓存中的数据。 |
| 写命中 | 处理器 | 共享 | 一致性 | 在总线上发出失效请求。这类操作常称为升级未命中(upgrade miss)或所有权未命中(ownership miss),因为它不获取数据,只改变状态。 |
| 写未命中 | 处理器 | 无效 | 普通未命中 | 在总线上发出写未命中。 |
| 写未命中 | 处理器 | 共享 | 替换 | 地址冲突未命中:在总线上发出写未命中。 |
| 写未命中 | 处理器 | 独占 | 替换 | 地址冲突未命中:写回缓存块,然后在总线上发出写未命中。 |
| 读未命中 | 总线 | 共享 | 无操作 | 允许共享缓存或内存为读未命中提供服务。 |
| 读未命中 | 总线 | 独占 | 一致性 | 尝试读取共享数据:将缓存块放到总线上,写回该块,并将其状态改为共享。 |
| 失效 | 总线 | 共享 | 一致性 | 尝试写入共享块:使该块失效。 |
| 写未命中 | 总线 | 共享 | 一致性 | 尝试写入共享块:使该缓存块失效。 |
| 写未命中 | 总线 | 独占 | 一致性 | 尝试写入在别处被独占的块:写回该缓存块,并将它在本地缓存中的状态改为无效。 |
图 5.5(续)。 第四列将缓存操作分为普通命中或未命中,与单处理器缓存中相同;替换,即单处理器缓存的替换未命中;以及为维护缓存一致性所需的一致性操作。普通操作或替换操作也可能触发一致性操作,取决于该块在其他缓存中的状态。对于从总线上监听到的读未命中、写未命中或失效请求,只有在读写地址与本地缓存中的某个有效块匹配时,才需要采取动作。
总线上出现失效或写未命中请求时,所有在私有缓存中持有该块副本的核心,都使其失效。对于写回缓存中的写未命中,如果某个私有缓存独占该块,它还必须写回该块;否则,可以从共享缓存或内存读取数据。
图 5.6 是对私有写回缓存的单个块采用写失效协议时的有限状态转换图。为简明起见,将协议的三个状态画了两遍:左边表示处理器请求引起的转换,对应图 5.5 上部;右边表示总线请求引起的转换,对应图 5.5 下部。粗体表示总线动作,以区别于状态转换的触发条件。每个节点表示处理器或总线请求所选定的私有缓存块的状态。

图 5.6(续)。 圆圈表示缓存状态;状态名下方括号中的内容,表示本地处理器在不改变状态的情况下允许执行的访问。转换弧上用普通字体标出触发状态变化的事件,用粗体标出状态转换所产生的总线动作。触发事件针对私有缓存中的一个块,而不是缓存中的某个固定地址。因此,对处于共享状态的缓存块发生读未命中,是访问另一个地址而在该缓存块处未命中。图左边表示与此缓存关联的处理器引起的转换,右边表示总线操作引起的转换。在独占或共享状态下发生读未命中,以及在独占状态下发生写未命中,都意味着处理器请求的地址与本地缓存块中保存的地址不匹配。这是普通的缓存替换未命中。尝试写入共享状态的块,会发出失效请求。每当总线事务发生,所有包含该事务所指缓存块的私有缓存,都执行右半图规定的动作。协议假定:如果一个块在所有本地缓存中都为干净状态,读未命中由内存或共享缓存提供数据。实际实现会合并这两套状态图。实践中,失效协议有许多细微变种,包括增加未修改的独占状态,以及决定未命中时由处理器还是内存提供数据。在多核芯片中,具有包含性的共享 LLC 相当于内存;这里的总线,是连接各核心私有缓存与共享缓存的总线,而共享缓存再与内存相连。
图内术语:Invalid=无效;Shared (read only)=共享(只读);Exclusive (read/write)=独占(可读写);CPU read/write hit/miss=CPU 读/写命中或未命中;Place read/write miss on bus=在总线上发出读/写未命中;Place invalidate on bus=在总线上发出失效请求;Write-back block=写回块;abort memory access=中止内存访问;for this block=针对该块。
这套缓存协议中的所有状态,在单处理器缓存中也都需要,分别对应无效、有效且干净,以及脏状态。图 5.6 左半部分大多数弧所表示的状态变化,在单处理器写回缓存中同样需要;例外是对共享块写命中时发出的失效请求。右半部分的状态变化则完全是为了一致性,单处理器缓存控制器中不会出现。
如前所述,每个缓存只有一个有限状态机,触发事件来自相连的处理器或总线。图 5.7 将图 5.6 左右两部分的转换合并,形成单个缓存块的完整状态图。
要理解协议为何有效,只需注意:任何有效缓存块,要么在一个或多个私有缓存中处于共享状态,要么恰好在一个缓存中处于独占状态。转入独占状态是处理器写该块的前提;任何这类转换,都要求在总线上发出失效或写未命中,从而使其他所有本地缓存中的该块无效。此外,如果其他某个本地缓存原本独占该块,它会写回该块,提供包含所需地址的数据。最后,如果总线上发生对独占块的读未命中,持有独占副本的本地缓存就转为共享状态。
图 5.7 中的灰色动作处理总线上的读写未命中,实际上就是协议的监听部分。此协议与大多数其他协议还保持另一性质:任何处于共享状态的内存块,在共享 LLC 或内存中的副本始终是最新的。这个性质简化了实现。
虽然这个简单缓存协议是正确的,但它省略了若干会让实现棘手得多的复杂情况。其中最重要的是,协议假定操作具有原子性:执行某个操作时,不会有另一个操作插入其中。然而,这一假设并不自然成立,因为许多操作需要多个时钟周期。例如,上述协议假定检测写未命中、取得总线和收到响应是一个原子动作,但实际实现做不到这一点。事实上,甚至读未命中也可能不是原子的:在多核处理器 L2 中检测到未命中后,核心必须仲裁,争取连接共享 L3 或内存的总线访问权。稍后会讨论如何克服这一困难。

图内各状态与动作的中文含义见图 5.6;Write-back data=写回数据。本图的 Exclusive 表示前文三状态协议中的已修改、独占状态,与下面 MESI 新增的干净独占状态需结合上下文区分。
基本一致性协议的扩展
刚介绍的协议是一个简单的三状态协议,通常用各状态名称的首字母称为 MSI(Modified、Shared、Invalid,即已修改、共享、无效)协议。基本协议有许多扩展,通过增加状态和事务来优化某些行为,从而可能提高性能。最常见的两种扩展如下。
- MESI:在基本 MSI 中增加 Exclusive 状态,形成已修改、独占、共享、无效四个状态。独占状态表示该块只在一个缓存中存在,而且是干净的。处于 E 状态的块可以直接写入,不必发出任何失效请求,因此优化了同一缓存先读后写某块的情况。写入后,块从 Exclusive 转为 Modified。如果其他处理器对 E 状态的块发生读未命中,就必须转为 S 状态,以维持一致性。由于后续访问都受到监听,能够持续维护这一状态的准确性。可以方便地加入这一新状态:用编码一致性状态的位表示独占,用脏位表示该块已经修改。CXL 标准采用 MESI;CXL 是支持处理器与加速器之间一致性内存的互连标准。Intel i7 和 i9 使用 MESI 的变种 MESIF,增加 Forward 状态,用来指定哪个共享处理器响应未命中并提供数据,目的是提高分布式内存组织中的性能。
- MOESI:在 MESI 中增加 Owned 状态,表示该缓存拥有此块,而内存副本已过期。在 MSI 和 MESI 中,尝试共享一个处于 Modified 状态的块时,原缓存与新加入共享的缓存都转为 Shared,并且必须将该块写回内存。在 MOESI 中,原缓存可以从 Modified 转为 Owned,而不写回内存。其他新共享该块的缓存保持 Shared;只有原缓存具有的 O 状态,表明主存副本已过期,且指定缓存是拥有者。因为内存不是最新的,拥有者必须在未命中时提供该块的值,并在替换该块时写回内存。AMD Opteron 系列采用 MOESI。较新的 AMD 处理器采用基于 MOESI 的协议,同时加入前述 F 状态及 D(Dirty)状态。D 是 Owned 的一种变体,也表示内存中的副本已过期。
对称共享内存多处理器与监听协议的限制
随着多处理器中的处理器数量增加,或各处理器的内存需求增长,系统中的任何集中式资源都可能成为瓶颈。对于多核处理器,仅有几个核心时,单一共享总线就会成为瓶颈。因此,多核设计转向带宽更高的互连方案,并采用多个独立存储器,以支持更多核心。5.8 节考察的多核芯片采用三种不同方法:
- IBM Power 10 单芯片最多有 15 核,采用 16 个并行环,连接分布式 L3 缓存以及最多 16 个独立内存通道。
- Xeon Platinum 系列既有最多 40 核的单芯片形式,也有原书称为 chiplet 的形式,其中包含四颗处理器芯片以及 HBM 内存或缓存。它用多个环连接芯片上最多 40 个核心、分布式 60 MiB LLC,以及最多八个内存通道,具体取决于配置。原书称,最新 Xeon 服务器 Sapphire rapids 已宣布采用 56 核、每个 chiplet 四颗芯片,总计 224 核。1
- AMD EPYC 基于 chiplet 设计,每个 chiplet 有 8 核,每个插槽最多 4 个 chiplet,最多两个插槽,共 64 核。每个 chiplet 内共享 LLC,并有自己的内存通道。Infinity Path 用于连接 chiplet 内部及 chiplet 之间的核心。下一代将支持最多 12 个 chiplet,总计 96 核。
除了缓存未命中和一致性流量造成的共享互连争用,缓存的监听带宽也可能成为问题,因为每个缓存可能都要检查每一次未命中。因此,增加互连带宽可能只是把争用转移到了缓存。下面的例题说明这个问题。
例题:监听带宽的限制
考虑一个 16 处理器的多核系统,每个处理器都有自己的 L1 和 L2,L2 通过共享总线进行监听。假定每个 L2 请求平均占用 15 个周期,无论它是由一致性未命中还是其他未命中引起。时钟频率为 4.0 GHz,CPI 为 0.5,加载/存储指令占 40%。如果要求一致性流量消耗不超过 L2 带宽的 50%,那么每个处理器允许的最大一致性未命中率(CMR)是多少?
解答。 先写出可使用缓存周期数的方程,其中 CMR 为一致性未命中率:
因此,CMR 必须不超过 0.26%。下一节与附录 I 中,我们将看到一些 CMR 超过 1% 的应用。反过来,如果假定 CMR 可以达到 1%,那么只能支持 4 个处理器。显然,即使小型多核处理器,也需要扩展监听带宽的方法。2
增加监听带宽有几种技术:
- 复制标签。这使缓存层的有效监听带宽翻倍,但对于很大的缓存,代价也很高;如果标签副本表明监听命中,仍需访问主标签。若 L2 分成多个存储体,也能有效提高监听带宽,许多近期设计都采用这种办法。
- 如果多核处理器的 LLC 是共享且具有包含性的,就可以先监听 LLC,只有 LLC 命中时才查询 L2;否则,可以确定数据不在私有缓存中。将 LLC 分成多个存储体,也能提高其带宽。IBM Power 10 使用这一方法,它产生 NUCA 设计,但能使 LLC 的监听带宽有效地按存储体数量扩展。如果监听在 LLC 命中,仍必须向所有 L2 广播,各 L2 再监听自己的内容。注意,这一方法要求 LLC 具有包含性。后面将看到,Power 10 采用继承自早期 Power 处理器的复杂一致性方案,力图尽量减少广播监听。
目录方案提供了另一种扩展一致性带宽的方法:避免监听与广播。由于它不依靠共享广播机制串行化操作,因此必须通过请求—响应机制,知道一致性操作何时完成。5.4 节将介绍目录方案。
AMD Opteron 位于监听协议与目录协议之间的另一个中间位置。内存直接连接到每个多核插槽,最多可连接四个插槽。由于本地内存略快,系统为 NUMA。Opteron 使用点对点链路向最多三颗其他芯片广播,以实现一致性协议。处理器间链路不共享,因此,处理器只有收到显式确认,才知道失效操作已完成。于是,它像监听协议一样广播以寻找可能共享的副本,又像目录协议一样用确认来确定操作顺序。由于 Opteron 实现中的本地内存仅略快于远程内存,一些软件把 Opteron 多处理器当作均匀内存访问系统。
实现监听缓存一致性
魔鬼藏在细节里。
——经典谚语
1990 年编写本书第一版时,最后一个“融会贯通”实例是一个采用监听一致性的 30 处理器单总线多处理器。它的总线带宽略高于 50 MiB/s,而这点带宽在 2023 年甚至不足以支持 Intel i9 的一个核心!1995 年编写第二版时,采用不止一条总线的首批缓存一致性多处理器刚刚出现,我们于是增加了一个附录,描述多总线系统中监听的实现。
到 2024 年,每个核心数超过四个的多核系统,都在封装内部采用片上间接互连,即既不是总线,也不是交叉开关;这种间接网络还会进一步扩展,以连接多颗芯片,构成缓存一致性多处理器。因此,即使在单芯片内部,设计者也必须面对这样的挑战:没有总线来简化事件串行化时,如何实现监听或目录方案。
实现前面的一致性协议,需要满足两个性质:
- 原子性。 一致性事件,例如取得独占访问权,虽然要经过多个时钟周期,却必须表现为原子操作。如果其他插入其间的事件不能改变其结果,该事件就表现为原子的。
- 可串行化。 一致性事件必须表现为按某个串行顺序发生,而且所有处理器看到的顺序相同。
如果其中任何性质不成立,两个处理器就可能看到不同的数据值,说明系统不具有一致性。
可串行化比较容易实现。一致性协议必须保证,对某个地址的全部访问都经过一个公共点;它可以是总线、共享 LLC,或某个内存模块。这个公共点强制可能冲突的访问确定先后次序,从而保证它们串行化。协议还应在代表 CPU 发起新事务前,先处理来自总线或其他通信网络的所有待处理请求。
实际实现监听一致性协议时,更大的困难是:在任何近期多处理器中,写未命中和升级未命中都不天然具有原子性。检测写未命中或升级未命中、与其他处理器和内存通信、为写未命中取得最新值并确保所有失效请求得到处理,最后更新缓存,这些步骤不可能像一个时钟周期内的动作一样完成。
在单总线多核系统中,可以先仲裁取得通往共享缓存或内存的总线,再改变缓存状态,并一直持有总线,直到所有动作完成,从而使这些步骤实际表现为原子操作。处理器如何知道所有失效动作已经完成?早期设计用一条状态线表示:所有必要的失效请求都已收到,正在处理。收到这一信号后,产生未命中的处理器就可以释放总线,因为它知道,在下一次未命中相关操作发生前,所需动作都会完成。在这些步骤期间独占总线,便使各步骤整体具有原子性。
注意,没有共享总线就无法这样做;而且,这种方法与非阻塞缓存不兼容!
替代办法是:处理器在处理读未命中或写未命中,包括升级未命中,的过程中,必须能够接收传入事务,使缓存块失效,并重新发起读写请求。
理解并实现这一协议的最简单方法,是加入额外的瞬态(transient state)。对于核心中需要通信的每个事务,在其内部加入这些额外的“隐藏”状态。它们之所以是瞬态,是因为等待的是存储系统中某个动作完成,而不是等待核心发起新的请求。在瞬态中,如果收到系统发来的、由其他核心生成的写未命中或失效请求,就将缓存块状态改为无效,核心重新尝试访问;此时访问必然未命中。用表格更容易理解这些隐藏状态的作用,如图 5.8 所示。通过重新启动被另一操作打断的动作,这些瞬态有效地使协议动作具有原子性。
| 缓存状态 | 核心动作 | 等待事项 | 成功后的状态 |
|---|---|---|---|
| 无效 | 读未命中 | 未命中请求返回 | 共享 |
| 无效 | 写未命中 | 未命中请求返回 | 独占 |
| 共享 | 读未命中 | 未命中请求返回 | 共享 |
| 共享 | 写命中 | 失效完成 | 独占 |
| 共享 | 写未命中 | 未命中请求返回 | 独占 |
| 独占 | 读未命中 | 未命中请求返回 | 共享 |
| 独占 | 写未命中 | 未命中请求返回 | 独占 |
图 5.8(续)。 这些状态是瞬态:“等待事项”列所列事务完成后,便像基本一致性协议一样转入成功状态。但是,如果待处理事务完成前,收到对同一块的写未命中或失效请求,就将该块改为无效,并从这一状态重新启动事务。由于块已无效,此时只能发生读未命中或写未命中。注意,图 5.6 和图 5.7 中的写回也必须执行。先完成写回较简单;如果将写回暂存于缓冲区,传入事务也必须监听缓冲区内容。
共享内存多处理器的性能
在采用监听一致性协议的多核系统中,多种现象共同决定性能。尤其是,整体缓存性能既取决于单处理器缓存未命中流量,也取决于一致性事务引起的流量;后者导致失效及后续缓存未命中。改变处理器数量、缓存容量和块大小,会以不同方式影响未命中率的这两个组成部分,系统的整体行为则由两种效应共同决定。
附录 B 将单处理器未命中率分为 3C:容量、强制性和冲突未命中,以帮助理解应用行为和缓存设计的改进空间。类似地,处理器间通信引起的未命中,常称为一致性未命中(coherence miss),也可以分为两个来源。
第一个来源是真共享未命中(true sharing miss),它由通过缓存一致性机制传递数据而产生。在失效协议中,处理器第一次写某个共享缓存块时,会发出失效请求,以取得该块的所有权。此外,另一个处理器尝试读取该缓存块中已修改的字时,也会未命中,继而传输该块。这两种未命中都归为真共享未命中,因为它们直接来自处理器之间的数据共享。
第二种效应称为伪共享(false sharing),来自采用每块只有一个有效位的失效一致性算法。如果因为写入了块中的某个字,使整块失效,并导致后续访问未命中,但这个被写入的字并不是随后读取的字,就可能出现伪共享。如果收到失效请求的处理器使用了被写入的字,那么属于真共享访问,无论块大小如何,都将未命中。反之,如果写入和读取的是同一缓存块中的不同字,失效没有传递任何新值,只造成一次额外缓存未命中,那么就是伪共享未命中。此时被共享的是块,块内并没有某个字真正共享;如果块大小只有一个字,这种未命中就不会发生。下面的例题说明这些共享模式。
例题:区分真共享与伪共享
假定 z1 和 z2 两个字位于同一个缓存块中,该块在 P1 和 P2 的缓存中都处于共享状态。对于以下事件序列,判断每次访问是真共享未命中、伪共享未命中还是命中。若块大小为一个字时仍会发生的未命中,就定义为真共享未命中。
| 时间 | P1 | P2 |
|---|---|---|
| 1 | 写 z1 | |
| 2 | 读 z2 | |
| 3 | 写 z1 | |
| 4 | 写 z2 | |
| 5 | 读 z2 |
解答。 按时间步分类如下:
- 真共享未命中,因为 z1 在 P2 中处于共享状态,必须使 P2 中的 z1 失效。
- 伪共享未命中,因为 P1 写 z1 时使 z2 一同失效,但 P2 并不使用 z1 的这个值。
- 伪共享未命中,因为 P2 的读取使包含 z1 的块被标记为共享,但 P2 并没有读取 z1。P2 读取后,包含 z1 的块处于共享状态;必须通过一次写未命中取得独占访问权。在某些协议中,这由升级请求处理:发出总线失效请求,但不传输缓存块。
- 伪共享未命中,原因与第 3 步相同。
- 真共享未命中,因为读取的值是由 P2 写入的。
虽然我们将在商业负载中看到真共享和伪共享未命中的影响,但在紧密耦合、共享大量用户数据的应用中,一致性未命中的作用更为显著。附录 I 研究并行科学负载的性能时,将详细考察这些影响。
小规模多处理器上的商业负载
本节研究一个四处理器共享内存多处理器运行在线事务处理负载时的存储系统行为。该研究于 1998 年在四处理器 Alpha 系统上进行,但对这类负载的多处理器性能而言,它仍是最全面、最有启发性的研究。我们重点理解多处理器的缓存活动,尤其是 L3,因为那里大量流量与一致性有关。
结果来自 AlphaServer 4100 的实测,或以 AlphaServer 4100 为模型的可配置模拟器,见 Barroso et al.(1998)。AlphaServer 4100 的每个处理器都是 Alpha 21164,每时钟最多发射四条指令,运行频率为 300 MHz。虽然它的频率远低于近期设计的处理器,但由四发射处理器和三级缓存层次组成的基本结构,与多核 Intel i7、i9 及其他处理器十分相似,如图 5.9 所示,只是缓存比新多核系统的小。我们不纠缠于具体性能,而是考察每处理器 L3 容量从 2 MiB 到 8 MiB 时的模拟行为。
虽然原研究考察了三种负载,我们只关注以 TPC-B 为模型的在线事务处理(OLTP)负载,底层数据库为 Oracle 7.3.2。TPC-B 的内存行为与较新的 TPC-C 类似,后者见第 1 章。负载由一组产生请求的客户进程,以及一组处理请求的服务器进程组成。服务器进程占用 85% 的用户态时间,其余属于客户进程。通过精心调优并保持足够多的请求,可用忙碌的处理器隐藏 I/O 延迟;不过,服务器进程通常执行约 25,000 条指令后就会因 I/O 阻塞。总体上,71% 的执行时间处于用户态,18% 用于操作系统,11% 为空闲,主要在等待 I/O。在所研究的商业应用中,OLTP 对存储系统的压力最大,即使使用大得多的 L3 进行评估,也存在明显挑战。例如,AlphaServer 中的处理器约有 90% 的周期处于停顿状态;由 L3 未命中造成的内存访问占近一半停顿时间,L2 未命中占 25%。
先考察改变 L3 容量的影响。在这些研究中,每处理器 L3 从 1 MiB 变化到 8 MiB。当每处理器为 2 MiB 时,L3 总量与 Intel i7 6700 相同。但是 i7 的缓存是共享的,既有优势也有劣势。共享的 8 MiB 缓存不太可能胜过总量为 16 MiB 的独立 L3 缓存。
| 缓存级别 | 特性 | Alpha 21164 | Intel i7 |
|---|---|---|---|
| L1 | 容量 | 8 KB 指令/8 KB 数据 | 32 KB 指令/32 KB 数据 |
| L1 | 相联度 | 直接映射 | 指令 8 路/数据 8 路 |
| L1 | 块大小 | 32 B | 64 B |
| L1 | 未命中开销 | 7 | 10 |
| L2 | 容量 | 96 KB | 256 KB |
| L2 | 相联度 | 3 路 | 8 路 |
| L2 | 块大小 | 32 B | 64 B |
| L2 | 未命中开销 | 21 | 35 |
| L3 | 容量 | 2 MiB(共 8 MiB,不共享) | 每核心 2 MiB(共 8 MiB,共享) |
| L3 | 相联度 | 直接映射 | 16 路 |
| L3 | 块大小 | 64 B | 64 B |
| L3 | 未命中开销 | 80 | 约 100 |
图 5.9(续)。 虽然 i7 的容量更大、相联度更高,但未命中开销也更高,因此行为可能差别不大。两种系统从私有缓存取得数据时,都有很高的开销,为 125 个周期或更多。一个关键区别是:i7 的 L3 是共享的,Alpha 服务器则有四个独立、不共享的 L3。
图 5.10 给出了增大缓存容量的影响。缓存采用两路组相联,以减少大量冲突未命中。随着 L3 增大,L3 未命中减少,执行时间得到改善。令人意外的是,几乎全部收益都出现在从 1 MiB 增加到 2 MiB 的过程中,即四个处理器的总缓存从 4 MiB 增加到 8 MiB。再往上增加,额外收益很少,尽管在 2 MiB 和 4 MiB 时,缓存未命中仍造成明显性能损失。这是为什么?

图 5.10(续)。 缓存增大时,空闲时间也增长,抵消了一部分性能收益。这是因为存储系统停顿减少后,需要更多服务器进程来掩盖 I/O 延迟。可以重新调优负载,改善计算与通信之间的平衡,抑制空闲时间的增长。PAL 代码是一组在特权模式下执行的专用操作系统级指令序列,例如 TLB 未命中处理程序。
图内术语:Normalized execution time=归一化执行时间;L3 cache size (MB)=L3 缓存容量(MB,图中单位);Idle=空闲;PAL code=PAL 代码;Memory access=内存访问;L2/L3 cache access=L2/L3 缓存访问;Instruction execution=指令执行。
为了更好地回答这个问题,必须确定哪些因素构成 L3 未命中率,以及它们怎样随 L3 增长而变化。图 5.11 显示五类来源对每条指令内存访问周期数的贡献。L3 为 1 MiB 时,最大的两项是指令未命中和容量/冲突未命中。L3 增大后,这两项降为次要因素。遗憾的是,强制性、伪共享和真共享未命中不受 L3 增大的影响。因此,在 4 MiB 和 8 MiB 时,真共享未命中成为主要部分;它们没有改变,所以 L3 超过 2 MiB 后继续增大,总未命中率只能有限下降。
增大缓存消除了大多数单处理器未命中,却没有改变多处理器未命中。那么,增加处理器数量,对不同类型未命中又有什么影响?

图内术语:Memory cycles per instruction=每条指令的内存访问周期数;Cache size (MB)=缓存容量(MB,图中单位);Instruction=指令未命中;Capacity/conflict=容量/冲突未命中;Compulsory=强制性未命中;False sharing=伪共享未命中;True sharing=真共享未命中。以下图 5.12、5.13 沿用同一分类。
图 5.12 显示这一数据,基准配置是每处理器 2 MiB、两路组相联 L3,与 i7 的每处理器有效缓存容量相同,但相联度更低。正如所料,真共享未命中率增加,而单处理器未命中并未减少来抵消它,因此每条指令的总内存访问周期数上升。
最后考察增大块是否有利于这一负载。更大的块应当降低指令未命中和冷未命中率;在一定限度内,也应减少容量/冲突未命中,还可能降低真共享未命中率。图 5.13 展示块从 32 字节增到 256 字节时,每千条指令的未命中数。这一变化影响了未命中率的四个组成部分:
- 真共享未命中率降至原来的一半以下,说明真共享模式具有一定局部性。
- 强制性未命中率显著下降,符合预期。
- 冲突/容量未命中小幅减少,块大小增为八倍时,仅降至原来的 ,说明在 L3 大于 2 MiB 后发生的单处理器未命中,空间局部性不强。
- 伪共享未命中率虽然绝对值很小,却几乎翻倍。

图中横轴 Processor count 为处理器数量;纵轴及分类含义见图 5.11。

图中纵轴 Misses per 1000 instructions 为每千条指令的未命中数;横轴 Block size (bytes) 为块大小,单位为字节;分类见图 5.11。
指令未命中率没有明显变化,令人吃惊。如果一个纯指令缓存呈现这种行为,我们会认为其空间局部性很差。但对于混合存放指令和数据的 L2、L3,指令与数据冲突等其他因素,也可能导致大块时仍有较高的指令未命中率。其他研究同样发现,大型数据库与 OLTP 负载的指令流空间局部性较低,因为其中有大量短基本块和专用代码序列。根据这些数据,若想让采用较大块的 L3 达到 32 字节块 L3 的性能,允许的未命中开销可以表示为 32 字节块未命中开销的倍数:
| 块大小 | 相对于 32 字节块未命中开销的倍数 |
|---|---|
| 64 字节 | 1.19 |
| 128 字节 | 1.36 |
| 256 字节 | 1.52 |
现代 DDR SDRAM 的块访问速度很快,因此这些数值是可以达到的,尤其是 64 字节,即 i7/i9 的块大小,以及 128 字节。当然,还必须考虑到内存流量增加,以及与其他核心争用内存的影响。后一种效应很容易抵消提高单处理器性能带来的收益。5.4 节将考察规模更大、基于 NUMA 的多处理器在一种高度可扩展但耦合松散的负载下的性能特征。