第二章:一致性基础 A Primer on Memory Consistency and Cache Coherence - 2nd Edition
在本章中,我们将介绍足够多的缓存一致性知识,以便理解一致性模型是如何与缓存相互作用的。我们在 2.1 节首先给出在本入门教程中所考虑的系统模型。为了简化本章以及后续章节的阐述,我们选择了尽可能简单的系统模型,该模型足以说明重要的问题;而将与更复杂系统模型相关的问题留到第 9 章讨论。
2.2 节解释了必须解决的缓存一致性问题,以及不一致情况是如何产生的。2.3 节则精确地定义了缓存一致性。
2.1 基线系统模型
在本入门教程中,我们考虑的是具有多个共享内存的处理器核心的系统。也就是说,所有核心都可以对所有(物理)地址执行加载和存储操作。基线系统模型包括一个单芯片多核处理器和片外主内存,如图 2.1 所示。多核处理器芯片由多个单线程核心组成,每个核心都有自己的私有数据缓存,以及一个由所有核心共享的末级缓存(LLC)。在本入门教程中,当我们使用 “缓存” 一词时,指的是核心的私有数据缓存,而不是末级缓存。每个核心的数据缓存使用物理地址进行访问,并且是回写式的。核心和末级缓存通过互连网络相互通信。尽管末级缓存位于处理器芯片上,但在逻辑上它是一个 “内存侧缓存”,因此不会引入另一层一致性问题。在逻辑上,末级缓存就在内存之前,用于降低内存访问的平均延迟并提高内存的有效带宽。末级缓存还充当片上内存控制器。
这个基线系统模型省略了许多常见但在本入门教程的大部分内容中并非必需的特性。这些特性包括指令缓存、多级缓存、多个核心共享的缓存、虚拟地址缓存、转换后备缓冲器(TLB)以及一致性直接内存访问(DMA)。基线系统模型还排除了存在多个多核芯片的可能性。我们稍后会讨论所有这些特性,但目前,它们会增加不必要的复杂性。
2.2 问题:不一致情况可能如何发生
不一致情况的出现仅仅是因为一个基本问题:存在多个可以访问缓存和内存的参与者。在现代系统中,这些参与者包括处理器核心、DMA 引擎以及可以对缓存和内存进行读写操作的外部设备。在本入门教程的其余部分,我们通常关注的参与者是核心,但值得记住的是,可能还存在其他参与者。
表 2.1 展示了一个不一致的简单示例。最初,内存位置 A 在内存以及两个核心的本地缓存中都存储着值 42。在时刻 1,核心 1 将其缓存中内存位置 A 的值从 42 更改为 43,这使得核心 2 缓存中 A 的值变得过时。核心 2 执行一个 while 循环,反复从其本地缓存中加载(过时的)A 的值。显然,这是一个不一致的例子,因为核心 1 的存储操作对核心 2 来说不可见,结果导致核心 2 陷入 while 循环中。
为了防止不一致,系统必须实现一个缓存一致性协议,使得核心 1 的存储操作对核心 2 可见。这些缓存一致性协议的设计和实现是第 6 至 9 章的主要内容。
2.3 缓存一致性接口
大致来说,一致性协议必须确保写操作对所有处理器都可见。在本节中,我们将通过一致性协议所暴露的抽象接口来更正式地理解一致性协议。
处理器核心通过一个一致性接口(图 2.2)与一致性协议进行交互,该接口提供两种方法:(1)一个读请求方法,该方法以一个内存位置作为参数并返回一个值;(2)一个写请求方法,该方法以一个内存位置和一个(要写入的)值作为参数,并返回一个确认信息。
文献中出现过许多一致性协议,并且这些协议也已在实际处理器中得到应用。我们根据一致性接口的性质将这些协议分为两类 —— 具体来说,是根据一致性与一致性模型是否有清晰的分离,或者它们是否不可分割来划分。
与一致性无关的一致性。在第一类中,写操作在返回之前会对所有其他核心可见。由于写操作是同步传播的,第一类所呈现的接口与原子内存系统(无缓存)的接口相同。因此,与一致性协议交互的任何子系统(例如处理器核心流水线)都可以假设它正在与一个没有缓存的原子内存系统进行交互。从一致性实施的角度来看,这个一致性接口实现了关注点的良好分离。缓存一致性协议完全抽象掉了缓存,并呈现出原子内存的假象 —— 就好像缓存被移除了,只有内存包含在一致性模块中(图 2.2)—— 而处理器核心流水线则执行由一致性模型规范所规定的顺序。
一致性导向的一致性。在较新的第二类中,写操作是异步传播的 —— 因此,写操作可以在对所有处理器可见之前就返回,从而允许观察到(实时的)过时值。然而,为了正确地实施一致性,这类一致性协议必须确保写操作最终可见的顺序符合一致性模型所规定的顺序规则。再参考图 2.2,流水线和一致性协议都执行由一致性模型所规定的顺序。这第二类协议的出现是为了支持基于吞吐量的通用图形处理单元(GP-GPU),并且在本入门教程第一版出版后得到了广泛关注。
本入门教程(以及本章的其余部分)主要关注第一类一致性协议。我们将在异构一致性(第 10 章)的背景下讨论第二类一致性协议。
2.4 (与一致性无关的)一致性不变量
一个一致性协议必须满足哪些不变量,才能使缓存不可见并呈现出原子内存系统的抽象呢?
在教科书和已发表的论文中出现过多种关于一致性的定义,我们并不想介绍所有这些定义。相反,我们给出我们更倾向的定义,因为它有助于深入理解一致性协议的设计。在侧边栏中,我们将讨论其他定义以及它们与我们偏好的定义之间的关系。
我们通过单写者 - 多读者(SWMR)不变量来定义一致性。对于任何给定的内存位置,在任何给定的时刻,要么存在单个核心可以对其进行写入(并且也可以读取),要么存在若干核心可以对其进行读取。因此,永远不会出现这样的情况:一个给定的内存位置在同一时刻可以被一个核心写入,而同时又被其他任何核心读取或写入。另一种理解这个定义的方式是,对于每个内存位置,将其生命周期划分为多个时期。在每个时期内,要么单个核心具有读写访问权限,要么若干核心(可能为零个)具有只读访问权限。图 2.3 展示了一个示例内存位置的生命周期,划分为四个保持 SWMR 不变量的时期。
除了 SWMR 不变量之外,一致性还要求正确传播给定内存位置的值。为了解释为什么值很重要,让我们重新考虑图 2.3 中的示例。即使 SWMR 不变量成立,但如果在第一个只读时期,核心 2 和核心 5 读取到的值不同,那么系统就是不一致的。同样,如果核心 1 在其读写时期未能读取到核心 3 写入的最后一个值,或者核心 1、核心 2 或核心 3 在核心 1 的读写时期未能读取到核心 1 执行的最后一次写入,那么系统也是不一致的。
因此,一致性的定义必须在 SWMR 不变量的基础上增加一个数据值不变量,该不变量与值如何从一个时期传播到下一个时期有关。这个不变量表明,一个内存位置在一个时期开始时的值与它在上一个读写时期结束时的值相同。
还有其他与这些不变量等价的解释。一个值得注意的例子 [5] 从令牌的角度解释了 SWMR 不变量。这些不变量如下。对于每个内存位置,存在固定数量的令牌,其数量至少与核心的数量一样多。如果一个核心拥有所有令牌,它就可以写入该内存位置。如果一个核心拥有一个或多个令牌,它就可以读取该内存位置。因此,在任何给定的时间,不可能出现一个核心正在写入内存位置,而其他任何核心正在读取或写入该位置的情况。
一致性不变量
____________________________________________________________
1. 单写者、多读者(SWMR)不变量。对于任何内存位置 A,在任何给定的时间,要么只存在单个核心可以对 A 进行写入(并且也可以读取),要么存在若干核心只能对 A 进行读取。
2. 数据值不变量。一个内存位置在一个时期开始时的值与它在上一个读写时期结束时的值相同。
_____________________________________________________________
2.4.1 维护一致性不变量
上一节中给出的一致性不变量为一致性协议的工作原理提供了一些思路。绝大多数一致性协议,称为 “无效化协议”,都是专门为维护这些不变量而设计的。如果一个核心想要读取一个内存位置,它会向其他核心发送消息以获取该内存位置的当前值,并确保没有其他核心以读写状态缓存了该内存位置的副本。这些消息结束任何正在进行的读写时期,并开始一个只读时期。如果一个核心想要写入一个内存位置,它会向其他核心发送消息以获取该内存位置的当前值(如果它还没有有效的只读缓存副本),并确保没有其他核心以只读或读写状态缓存了该内存位置的副本。这些消息结束任何正在进行的读写或只读时期,并开始一个新的读写时期。
本入门教程中关于缓存一致性的章节(第 6 至 9 章)将大大扩展对无效化协议的这种抽象描述,但基本思路是相同的。
2.4.2 一致性的粒度
一个核心可以以各种粒度执行加载和存储操作,通常范围从 1 到 64 字节。从理论上讲,一致性可以在最细的加载 / 存储粒度上实现。然而,在实践中,一致性通常是在缓存块的粒度上维护的。也就是说,硬件以缓存块为基础来强制实施一致性。在实践中,SWMR 不变量很可能是,对于任何内存块,要么存在单个写者,要么存在若干读者。在典型系统中,不可能出现一个核心正在写入一个块的第一个字节,而另一个核心正在写入该块内的另一个字节的情况。尽管缓存块粒度很常见,并且在本入门教程的其余部分我们也假定是这种情况,但应该注意的是,已经有一些协议在更细或更粗的粒度上维护一致性。
侧边栏:类似内存一致性的缓存一致性定义
——————————————————————————————————————————
我们偏好的一致性定义是从实现的角度来定义它的 —— 指定了关于不同核心对内存位置的访问权限以及核心之间传递的数据值的硬件强制不变量。
还存在另一类从程序员的角度来定义一致性的定义,类似于内存一致性模型如何指定从体系结构上可见的加载和存储操作的顺序。
一种类似一致性的指定一致性的方法与顺序一致性的定义相关。顺序一致性(SC)是我们将在第 3 章深入讨论的一种内存一致性模型,它规定系统必须以一种尊重每个线程的程序顺序的全序方式来执行所有线程对所有内存位置的加载和存储操作。每个加载操作获取在该全序中最近的存储操作的值。与顺序一致性定义类似的一致性定义是,一个一致的系统必须以一种尊重每个线程的程序顺序的全序方式来执行所有线程对单个内存位置的加载和存储操作。这个定义突出了文献中一致性和一致性之间的一个重要区别:一致性是在每个内存位置的基础上指定的,而一致性是针对所有内存位置指定的。值得注意的是,任何满足 SWMR 和数据值不变量(再结合一个不会对任何特定位置的访问进行重新排序的流水线)的一致性协议也保证满足这种类似一致性的一致性定义。(然而,反之不一定成立。)
另一个 [1, 2] 关于一致性的定义用两个不变量来定义一致性:(1)每个存储操作最终对所有核心都可见;(2)对同一内存位置的写入操作是按顺序执行的(即所有核心以相同的顺序观察到这些操作)。IBM 在 Power 架构 [4] 中也持有类似的观点,部分原因是为了便于实现这样一种情况:一个核心的一系列存储操作可能已经到达了某些核心(这些核心的加载操作可以看到这些值),但还没有到达其他核心。不变量 2 与我们前面描述的类似一致性的定义等价。与不变量 2(这是一个安全性不变量,即不能发生不好的事情)不同,不变量 1 是一个活性不变量(即好的事情最终必须发生)。
另一个由 Hennessy 和 Patterson [3] 指定的一致性定义由三个不变量组成:(1)一个核心对内存位置 A 的加载操作获取该核心之前对 A 的存储操作的值,除非在这期间另一个核心对 A 进行了存储操作;(2)如果另一个核心对 A 的存储操作 S 与该加载操作 “在时间上足够分开”,并且在 S 和该加载操作之间没有发生其他存储操作,那么对 A 的加载操作获取另一个核心对 A 的存储操作 S 的值;(3)对同一内存位置的存储操作是按顺序执行的(与前一个定义中的不变量 2 相同)。与前一个定义一样,这组不变量既包含了安全性又包含了活性。
—————————————————————————————————————————————
2.4.3 一致性在何时相关
无论我们选择哪种一致性定义,它都只在某些情况下相关,架构师必须清楚它在何时适用,何时不适用。我们现在讨论两个重要的问题。
・一致性适用于所有保存来自共享地址空间的块的存储结构。这些结构包括一级数据缓存、二级缓存、共享末级缓存(LLC)和主内存。这些结构还包括一级指令缓存和转换后备缓冲器(TLB)。
・一致性对程序员来说不是直接可见的。相反,处理器流水线和一致性协议共同执行一致性模型 —— 并且只有一致性模型对程序员是可见的。