第六章 缓存一致性协议 A Primer on Memory Consistency and Cache Coherence - 2nd Edition
第六章 一致性协议
在本章中,我们将回到第二章介绍的缓存一致性主题。在第二章中,我们定义了缓存一致性以理解其在支持内存一致性中的作用,但未深入探讨缓存一致性协议具体的工作原理或实现方式。本章将先概述缓存一致性协议的通用原理,随后两章再讨论特定类别的协议。我们将在 6.1 节介绍缓存一致性协议的整体框架,6.2 节说明如何描述协议,6.3 节给出一个简单具体的协议示例,并在 6.4 节探讨协议设计空间。
6.1 整体框架
缓存一致性协议的目标是通过执行第 2.3 节引入(此处重新陈述)的不变量来维护缓存一致性:
1. 单写多读(SWMR)不变量:对于任意内存位置 A,在任意给定(逻辑)时刻,仅存在单个核心可对 A 执行写操作(同时也可读取),或多个核心仅可读取 A。
2. 数据值不变量:一个周期开始时内存位置的值,与上一个读写周期结束时该位置的值相同。
为实现这些不变量,我们为每个存储结构(每个缓存和 LLC / 内存)关联一个称为一致性控制器的有限状态机。这些控制器构成一个分布式系统,通过相互交换消息确保每个缓存块始终满足 SWMR 和数据值不变量。这些有限状态机的交互由缓存一致性协议定义。
缓存一致性控制器有多项职责。缓存中的一致性控制器(称为缓存控制器)如图 6.1 所示。缓存控制器需处理两类源发出的请求。
核心侧:与处理器核心接口连接,接收核心的加载(load)和存储(store)请求,并返回加载值。缓存未命中时,控制器通过发送对包含目标地址的缓存块的缓存一致性请求(如只读权限请求)发起缓存一致性事务。该请求通过互连网络发送至一个或多个缓存一致性控制器。事务包含请求及为满足请求而交换的其他消息(如从其他控制器发送给请求者的数据响应消息)。事务类型和消息内容取决于具体协议。
网络侧:通过互连网络与系统其他部分接口连接,接收并处理缓存一致性请求和响应,处理逻辑同样依赖具体协议。
LLC / 内存中的缓存一致性控制器(称为内存控制器)如图 6.2 所示,其结构与缓存控制器类似,但通常仅有网络侧,不发起缓存一致性请求或接收响应【注,仅发起响应或接收请求】。其他代理(如 I/O 设备)可根据需求表现为类似缓存控制器、内存控制器或两者兼具的行为。
每个缓存一致性控制器为每个缓存块实现一组有限状态机(逻辑上每个块对应一个独立但相同的状态机),并根据块的状态接收和处理事件(如传入的一致性消息)。对于块 B 的事件类型 E(如核心向缓存控制器发起的存储请求),控制器执行的操作(如发起读写权限的一致性请求)是 E 和 B 当前状态(如只读状态)的函数,操作后可能改变 B 的状态。
6.2 缓存一致性协议的描述方法
缓存一致性协议通过定义缓存一致性控制器的行为来描述。描述方式有多种,但控制器的行为特性适合用表格形式规范 [9]。如表 6.1 所示,表格的行对应块状态,列对应事件,表中每个状态 / 事件项称为一个状态转移。事件 E 对应块 B 的转移包含两部分:(a) E 发生时执行的操作;(b) 块 B 的下一个状态。转移格式为 “操作 / 下一个状态”,若下一个状态与当前状态相同则可省略 “下一个状态” 部分。例如,若核心发送对块 B 的存储请求且块 B 处于只读状态(RO),表格显示控制器将执行 “发起对块 B 的读写权限一致性请求” 操作,并将块 B 状态改为 RW。
为简化说明,表 6.1 的示例有意未完全展开,但已体现表格规范方法描述控制器行为的能力。完整描述一致性协议需明确定义缓存控制器和内存控制器的表格。
不同缓存一致性协议的差异体现在控制器规范的不同,包括块状态集合、事务类型、事件类型和状态转移逻辑的差异。6.4 节将通过探讨各方面的设计选项来描述一致性协议的设计空间,在此之前先介绍一个简单具体的协议。
6.3 简单一致性协议示例
为帮助理解一致性协议,我们以一个简单协议为例。系统模型基于 2.1 节的基准模型,但互连网络限定为共享总线:核心可通过一组共享线路发送消息,所有核心和 LLC / 内存均可监听总线消息。
每个缓存块有两种稳定一致性状态:I(无效)和 V(有效)。LLC / 内存中的块同样有 I 和 V 两种状态,其中 I 表示所有缓存中的该块均为 I 状态,V 表示某一缓存中的该块为 V 状态。缓存块还有一个临时状态 IVD(见下文讨论)。系统启动时,所有缓存块和 LLC / 内存块均处于 I 状态。核心可向其缓存控制器发起加载和存储请求,缓存控制器需要为其他块腾出空间时会隐式生成 “驱逐块” 事件。缓存未命中的加载和存储操作会发起一致性事务以获取有效缓存块副本。与本指南中的所有协议一样,假设采用回写缓存:存储命中时仅更新本地缓存,待 “驱逐块” 事件触发时再将整个块写回 LLC / 内存。
协议通过三种总线消息实现两类一致性事务:
Get:请求获取块;
DataResp:传输块数据;
Put:将块写回内存控制器。
加载或存储未命中时,缓存控制器发送 Get 消息发起 Get 事务,并等待对应的 DataResp 消息。Get 事务具有原子性:从缓存发送 Get 消息到总线上出现对应 DataResp 消息期间,总线不可被其他 Get 或 Put 事务占用。“驱逐块” 事件触发时,缓存控制器向内存控制器发送包含完整缓存块数据的 Put 消息。
图 6.3 展示了稳定一致性状态之间的转移关系。我们用 “Own” 和 “Other” 前缀区分当前缓存控制器发起的事务消息与其他控制器发起的消息。需注意,若当前缓存控制器的块处于 V 状态,而其他缓存通过 Get 消息(OtherGet)请求该块,当前控制器必须通过 DataResp 消息(图中未显示)返回块数据,并将状态转为 I。
表 6.2 和表 6.3 更详细地描述了该协议,阴影单元格表示不可能的状态转移。例如,若缓存中的块处于 V 状态,缓存控制器不应在总线上接收到自己发送的 Put 请求(因此时块应已转为 I 状态)。
临时状态 IVD 对应处于 I 状态但正在等待数据(通过 DataResp 消息)以转为 V 状态的块。当稳定状态之间的转移非原子时会出现临时状态。在此简单协议中,单个消息的发送和接收是原子的,但从内存控制器获取块需发送 Get 消息并接收 DataResp 消息,其间存在不确定的时间间隔,IVD 状态即表示协议正在等待 DataResp。6.4.1 节将深入讨论临时状态。
该一致性协议在许多方面较为简单且效率有限,但其目的是帮助理解协议的描述方法。本书在介绍不同类型的一致性协议时,将始终采用这种描述方法。
6.4 一致性协议设计空间概述
如 6.1 节所述,一致性协议设计者必须为系统中每种类型的一致性控制器选择状态、事务、事件和状态转移逻辑。稳定状态的选择在很大程度上与协议的其他部分无关。例如,存在监听(snooping)和目录(directory)两类不同的一致性协议,架构师可以使用相同的稳定状态集合设计监听协议或目录协议。我们将在 6.4.1 节独立于具体协议讨论稳定状态。类似地,事务的选择也基本独立于特定协议,相关内容在 6.4.2 节讨论。然而,与稳定状态和事务不同,事件、状态转移和特定临时状态高度依赖于一致性协议,无法孤立讨论。因此,6.4.3 节将探讨一致性协议中的几个主要设计决策。
6.4.1 状态
在仅有单个参与者的系统中(如无一致性 DMA 的单核处理器),缓存块的状态要么是有效(valid),要么是无效(invalid)。若需要区分脏块(dirty block),缓存块可能有两种有效状态:脏块的最新写入值与其他副本不同。例如,在包含回写式 L1 缓存的两级缓存层次结构中,L1 中的块相对于 L2 缓存中的过期副本可能是脏的。
具有多个参与者的系统也可仅使用上述两到三种状态(如 6.3 节的示例),但我们通常希望区分不同类型的有效状态。缓存块的状态可编码以下四个特征:有效性(validity)、脏状态(dirtiness)、独占性(exclusivity)和所有权(ownership)[10],后两者是多参与者系统特有的属性:
有效性:有效块包含该块的最新值,可被读取,但仅当块处于独占状态时允许写入。
脏状态:与单核处理器类似,若缓存块的值为最新且与 LLC / 内存中的值不同,且缓存控制器负责最终将新值更新到 LLC / 内存,则该块为脏块。“干净”(clean)通常作为脏状态的反义词使用。
独占性:若缓存块是系统中唯一的私有缓存副本(即除共享 LLC 外,其他缓存无该块副本),则该块处于独占状态<sup>1</sup>。
所有权:若缓存控制器(或内存控制器)负责响应某块的一致性请求,则称其为该块的所有者。在大多数协议中,每个块在任意时刻恰好有一个所有者。由于容量不足或冲突未命中而需要驱逐块时,若该块是所有者,必须先将所有权转移给其他一致性控制器;在某些协议中,非所有者块可直接驱逐(无需发送消息)。
本节先讨论常用的稳定状态(即块未处于一致性事务中的状态),再介绍用于描述事务中块状态的临时状态。
稳定状态
许多一致性协议采用 Sweazey 和 Smith 首次提出的经典五状态 MOESI 模型的子集 [10]。MOESI(通常发音为 “MO-sey” 或 “mo-EE-see”)描述缓存中块的状态,其中最基本的三个状态是 MSI,O 和 E 状态可选但非必需。每个状态对应上述特征的不同组合:
M (odified,修改态):块有效、独占、拥有所有权,可能为脏状态。块可被读写,缓存中是唯一有效副本,缓存必须响应块请求,LLC / 内存中的副本可能过期。
S (hared,共享态):块有效但非独占、非脏、无所有权。缓存持有只读副本,其他缓存可能有有效只读副本。
I (nvalid,无效态):块无效。缓存要么不包含该块,要么包含可能过期的副本且不可读写。本指南中不区分这两种情况(尽管前者有时称为 “未存在” 状态)。
最基本的协议仅使用 MSI 状态,但在某些场景下添加 O 和 E 状态可优化性能。后续章节讨论带或不带这些状态的监听协议和目录协议时,将详细分析这些优化。以下是完整的 MOESI 状态列表:
M (odified,修改态)
O (wned,拥有态):块有效、拥有所有权、可能为脏状态,但非独占。缓存持有只读副本并必须响应块请求,其他缓存可能有只读副本但非所有者,LLC / 内存中的副本可能过期。
E (xclusive,独占态):块有效、独占、干净。缓存持有只读副本,其他缓存无有效副本,LLC / 内存中的副本为最新。本指南中,独占态被视为拥有所有权(尽管某些协议不将独占态视为所有权状态)。后续章节介绍 MESI 监听协议和目录协议时,将讨论是否将独占块视为所有者的相关问题。
S (hared,共享态)
I (nvalid,无效态)
图 6.4 以维恩图展示 MOESI 状态的特征归属:除 I 态外均为有效态;M、O、E 态为所有权状态;M 和 E 态表示独占性(其他缓存无有效副本);M 和 O 态表示块可能为脏状态。回顾 6.3 节的简单示例,该协议实际上将 MOES 状态简化为单一 V 态。
尽管 MOESI 状态非常常见,但并非稳定状态的全部可能。例如,F (orward,转发态) 与 O 态类似,但块为干净状态(LLC / 内存中的副本为最新)。一致性状态有多种可能,本指南重点讨论知名的 MOESI 状态。
临时状态
截至目前,我们仅讨论了块无一致性活动时的稳定状态(协议名称通常基于稳定状态,如 “MESI 协议”)。但正如 6.3 节示例所示,从一个稳定状态过渡到另一个稳定状态时可能存在临时状态。在 6.3 节中,IVD 状态(处于 I 态,等待转为 V 态,等待 DataResp 消息)即为临时状态。在更复杂的协议中,可能出现数十种临时状态。临时状态通常用 XYZ 符号表示,其中 X 为源稳定状态,Y 为目标稳定状态,Z 为触发状态转移完成的事件类型。例如,后续章节的协议中,IMD 表示块先前处于 I 态,待 D (ata) 消息到达后将转为 M 态。
LLC / 内存中块的状态
前述状态(稳定态和临时态)均针对缓存中的块。LLC 和内存中的块也有对应状态,通常有两种命名方式(命名惯例不影响功能或性能,但可能使不熟悉的架构师产生困惑):
缓存中心法:这是最常见的方式,LLC / 内存中块的状态是各缓存中该块状态的聚合。例如:
所有缓存中的块均为 I 态 → LLC / 内存状态为 I;
一个或多个缓存中的块为 S 态 → LLC / 内存状态为 S;
单个缓存中的块为 M 态 → LLC / 内存状态为 M。
内存中心法:LLC / 内存中块的状态对应内存控制器对该块的权限(而非缓存的权限)。例如:
所有缓存中的块均为 I 态 → LLC / 内存状态为 O(而非缓存中心法的 I),因 LLC / 内存此时充当块的所有者;
一个或多个缓存中的块为 S 态 → LLC / 内存状态仍为 O(同理);
单个缓存中的块为 M 或 O 态 → LLC / 内存状态为 I(因 LLC / 内存中的副本无效)。
本指南中的所有协议均对 LLC / 内存中的块状态采用缓存中心法命名。
块状态的维护
系统实现必须维护缓存、LLC 和内存中块的状态:
缓存和 LLC:每个块的状态通常只需扩展几位(如 MOESI 协议的 5 种状态需 3 位 / 块)。一致性协议可能有大量临时状态,但仅需为处于未完成一致性事务的块维护这些状态。实现中通常通过向未命中状态处理寄存器(MSHR)或类似跟踪未完成事务的结构中添加额外位来维护临时状态 [4]。
内存:内存的大容量可能带来挑战,但当前多核系统常采用包含性 LLC(inclusive LLC),即 LLC 存储系统中所有缓存块的副本(包括独占块)。在包含性 LLC 下,内存无需显式表示一致性状态:若块存在于 LLC 中,内存中的状态与 LLC 一致;若块不在 LLC 中,内存状态隐式为无效(因包含性 LLC 缺失意味着块不在任何缓存中)。侧边栏讨论了包含性 LLC 出现前内存状态的维护方式。
以上对内存状态的讨论假设系统为单多核芯片(与本指南大部分内容一致),多芯片多核系统可能需要在逻辑上为内存显式维护一致性状态。
6.4.2 事务
大多数一致性协议都包含一组类似的事务,因为一致性控制器的基本目标是相似的。例如,几乎所有协议都有一个用于获取块的共享(只读)访问权限的事务。表 6.4 列出了一组常见事务,并针对每个事务描述了发起事务的请求者的目标。这些事务均由缓存控制器响应其关联核心的请求而发起。表 6.5 则列出了核心可向其缓存控制器发出的请求,以及这些请求如何促使缓存控制器发起一致性事务。
侧边栏:多核时代之前 —— 在内存中维护一致性状态
传统的多核之前协议需要为每个内存块维护一致性状态,且无法像 6.4.1 节所述那样利用 LLC。我们简要讨论几种维护状态的方法及相关工程权衡:
为每个内存块增加状态位
最通用的实现是为每个内存块添加额外位以维护一致性状态。若内存有 N 种可能状态,每个块需要 log2 N
位额外空间。尽管这种设计通用性强且概念简单,但存在几个缺点:
成本增加:现代面向块的 DRAM 芯片通常至少 4 位宽(常见更宽),添加 2-3 位额外位存在物理难度。此外,修改内存结构会导致无法使用商用 DRAM 模块(如 DIMM),显著增加成本。幸运的是,对于每个块仅需少量状态位的协议,可通过修改 ECC 码存储这些位 —— 通过在更大粒度(如 512 位而非 64 位)上维护 ECC,可在使用商用 DRAM 模块的同时 “隐藏” 少量额外位 [1,5,7]。
延迟问题:在 DRAM 中存储状态位意味着获取状态需承担完整的 DRAM 延迟,即使块的最新版本存储在其他缓存中,这可能增加缓存间一致性传输的延迟。
性能影响:状态变更需执行 DRAM 的 “读 - 改 - 写” 周期,可能影响功耗和 DRAM 带宽。
内存中每个块单状态位
Synapse 协议 [3] 采用的设计是使用单个位区分两种稳定状态(I 和 V),临时状态通过小型专用结构维护。这种设计是上述方案的子集,存储成本最低。
零位逻辑或
为避免修改内存,可让缓存按需重构内存状态。内存状态是所有缓存中块状态的函数,因此若所有缓存聚合其状态,即可推断内存状态。系统可通过让所有核心向输入数等于缓存数的逻辑或门(或或门树)发送 “是否拥有?” 信号,推断内存是否为块的所有者:若输出为高,表示某缓存是所有者;若为低,则内存是所有者。此方案无需在内存中维护任何状态,但实现快速或操作(无论是逻辑门还是线或)可能存在难度。
尽管大多数协议使用类似的事务集合,但一致性控制器执行事务的交互方式差异显著。如下一节所述,在某些协议(如监听协议)中,缓存控制器通过向系统中所有一致性控制器广播 GetS 请求发起获取共享事务,当前块所有者的控制器用包含目标数据的消息响应请求者。相反,在其他协议(如目录协议)中,缓存控制器通过向预定义的特定一致性控制器(块的归属节点)单播 GetS 消息发起事务,该控制器可能直接响应或转发请求至其他控制器处理。
6.4.3 协议主要设计选项
一致性协议的设计方式繁多,即使使用相同的状态和事务集合,也可能衍生出多种不同协议。协议设计决定了每个一致性控制器可能的事件和状态转移逻辑 —— 与状态和事务不同,事件和转移无法脱离具体协议单独罗列。
尽管一致性协议的设计空间庞大,但有两个关键设计决策对协议的其他部分有重大影响,我们逐一讨论:
监听协议 vs. 目录协议
一致性协议主要分为两类:监听(snooping)协议和目录(directory)协议。以下先简要概述,第 7 章和第 8 章将分别深入讨论:
监听协议:缓存控制器通过向所有其他一致性控制器广播请求消息发起块请求,各控制器协同 “执行正确操作”(如所有者向请求者发送数据响应)。监听协议依赖互连网络以一致顺序向所有核心传递广播消息。大多数监听协议假设请求按全局顺序到达(如通过共享总线),但也可支持更高级的互连网络和宽松顺序。
目录协议:缓存控制器通过向块归属的内存控制器单播请求发起块请求,内存控制器维护目录,记录 LLC / 内存中每个块的状态(如当前所有者或共享者列表)。当块请求到达归属节点时,内存控制器查询目录状态:若为 GetS 请求,目录状态可确定所有者 —— 若 LLC / 内存是所有者,直接向请求者发送数据响应;若某缓存控制器是所有者,则将请求转发至该控制器,由其响应请求者。
设计权衡:
监听协议逻辑简单,但广播机制难以扩展至大规模核心;
目录协议通过单播实现可扩展性,但当归属节点非所有者时,事务需额外消息转发,延迟更高;
协议选择影响互连网络设计(如传统监听协议要求请求消息全局有序)。
无效化 vs. 更新
一致性协议的另一关键设计决策是处理核心写操作的方式,该决策与协议类型(监听或目录)无关,有两种选择:
无效化协议:核心写块时,发起一致性事务使所有其他缓存中的副本无效。副本无效后,请求者可安全写入,避免其他核心读取旧值。若其他核心后续需读取该块,必须发起新事务获取最新副本,从而维持一致性。
更新协议:核心写块时,发起事务将所有其他缓存中的副本更新为新值。此方案减少了核心读取新写块的延迟(无需等待 GetS 事务完成),但通常比无效化协议消耗更多带宽(更新消息需包含地址和新值,而非仅地址)。此外,更新协议使内存一致性模型的实现大幅复杂化 —— 例如,当多个缓存需对同一块的多个副本执行更新时,维持写原子性(5.5 节)变得极为困难。由于复杂性高,更新协议极少实现,本指南重点讨论更常见的无效化协议。
混合设计
上述两类设计决策均存在混合方案:
监听 - 目录混合协议[2,6] 结合两者特性,例如在小规模集群内使用监听,跨集群使用目录;
无效化 - 更新混合协议[8] 在部分场景使用更新(如近邻缓存)以减少延迟,其他场景使用无效化以降低带宽消耗。
一致性协议的设计空间丰富多样,架构师无需局限于特定设计风格。