Archipelago

Lec 25缓存一致性

讲义完成更新于 2026-09-29
后续

当前课程暂无后续已收录内容。

Lec 25 缓存一致性

TL;DR

  • 一致性两大条件:写入传播(所有核心最终可见写入)与写入串行化(全局看到一致的写入更新顺序)。
  • 监听 vs 目录协议:Snooping 依靠共享广播总线作为全序串行点(适合中小核);Directory 依靠分布式目录节点点对点中转(可扩展至大规模多核)。
  • 状态机优化演进:从朴素 VI、MSI(解决读写权限)演进至 MESI(引入独占 E 状态实现私有写静默升级),以及消除伪共享(False Sharing)的软件填充优化。

多核私有缓存与一致性挑战

在现代微处理器中,往往会有多个核心,每个核心都有一个私有缓存(以提高加载/存储性能),但是,主存是所有核心共享的。因此核心经常通过与主内存的交互来进行通信。因此,我们需要确保操作看起来像是在一个共享内存上进行。

多核私有缓存与共享主存引发的数据不一致示例

问题出现在,如下示例。0 号核心通过加载指令获取 0xA 地址上的值为 2,接着 2 号核心向 0xA 地址设置值为 3,此时 0 号核心上私有缓存的 0xA 的值是过时的。 解决办法就是,通过一个缓存一致性协议来控制缓存的内容,避免出现过期缓存行,比如,核心 2 在写入 3 之前将核心 0 的副本无效化(invalidate)。

缓存一致性协议

缓存一致性协议必须实现两个规则:

  1. 写入传播(Write propagation): 写入操作最终都会被所有的处理器可视
  2. 写入串行(Write serialization): 同个位置的写入是串行的(即所有处理器看到的都是相同的顺序写入)

如何实现写入传播?

  • 无效化写入协议(Write-invalidate protocol):执行写入操作之前使无效化所有其他缓存的副本
  • 写入更新协议(Write-update protocol): 执行写入操作之后更新所有的缓存副本

如何实现写入串行?

  • Snooping-based protocol(基于监听协议): 所有的缓存都通过一个总线来监听彼此的动作。
    • 中小规模多核 CPU(消费级 CPU 为主)
  • Directory-based protocol(基于目录协议):缓存目录跟踪私有缓存和串行请求的内容。
    • 大规模多核(DB、超算,因为 snooping 在规模大时不再高效)
  • Tips: 类似电话窃听 vs 邮局,前者就像一群人监听同个线路,一旦有人说话,其他人能听到;后者向邮局(目录)在管理所有的信件(缓存块),谁要发、谁时候、谁当前持有,都记录在案。

Snooping-Based 一致性

基于共享总线监听的 Snooping 一致性微架构

在单核视角(我们平时学习的)如果这个地址在本地缓存里(Hit),就直接用缓存中的数据。如果缓存里没有这个地址(Miss),就去主存 fetch 一份来填充缓存,这并没有考虑其他核心的影响。 基于监听的缓存(Snoopy Cache)还必须“监听”其他核心对共享内存地址的访问行为(尤其是写操作)并做出对应的动作。

总线提供了串行点:

  • 首先它是广播,完全有序,即总线上出现的事务是有确定顺序的
  • 每个 CPU 的缓存控制器就像一位监听者,会“偷听”其他核心对内存的操作
  • 缓存控制器的职责
    • 响应本地处理器的请求
      • 比如处理器执行 x = 3,控制器要判断能否直接在缓存里完成,是否需要获取独占权
    • 监听总线上的事务
      • 如果其他人写了我缓存的数据,我要标记失效(Invalid);
      • 如果别人读了,我可能得把缓存状态从 Modified 降到 Shared。
    • 发起新的总线事务
      • 当处理器要写一个变量时,如果当前状态不足以写(如是 Shared),控制器需要广播一个 “Request for ownership”。
      • 当处理器读取未缓存的数据时,会发出“Read Miss”请求。

缓存控制器响应本地请求与总线事务的状态协同

总得来说,实现 Snooping-based 一致性,需要实现一个 FSM,画出状态转移图,动作

VI 协议

最简单的缓存一致性协议: Valid / Invalid 状态机(FSM)。状态说明:

  • Invalid:当前缓存行是无效的,不能被处理器使用
  • Valid:当前缓存行是有效的,可以被处理器使用(只读)

VI 协议双状态转换有限状态机

  • 系统启动后,缓存行默认是 Invalid
  • Invalid →\rightarrow Valid ,处理器会首先发起 LOAD(读)请求,缓存控制器发出 BusRd 总线读事务,从主存加载数据到缓存。
  • Valid 状态下,处理器可以直接读取该缓存行(命中),不需要初始化读事务了。但如果处理器发出写请求,则需要广播通知, 缓存控制器发出 BusWr 写事务
  • Valid →\rightarrow Invalid,总线上其他核心发出 BusWr 到这个地址,本地缓存控制器监听到“别人要写我缓存的地址”, 则该状态由 Valid 转换为 Invalid
  • InValid 状态下, 本地缓存没有有效的数据,如果本地处理器要写一个地址,我们发起一个总线写事务(BusWr),直接把数据写入主存,并保持缓存行为 Invalid,不缓存写入的数据

为了便于理解,结合上述的规则去理解梳理下图过程发生的事。

VI 协议下读写序列总线事务交互时序

这个简单 VI 缓存一致性协议有什么问题?

Solution: 即使根本没有其他缓存也缓存了这地址,写操作始终需要广播(总线写事务),带宽消耗大。□

MSI 协议

保持一致性: 在一致性内存系统中,所有的加载(load)和存储(store)操作都可以被排列成一个全局顺序。然而,当多个缓存中存在某个地址的副本时,这种一致性可能会被破坏。为了保证内存一致性,需要满足两个条件:一是任意时刻只能有一个缓存对某个地址拥有写权限;二是在执行写操作之后,不能有任何缓存保留该地址的陈旧副本

MSI 协议解释。每个缓存行维持 MSI 状态:

  • I (Invalid,无效)—— 缓存中没有该地址
  • S(Shared,共享) —— 该缓存持有此地址的数据,但其他缓存也可能持有相同副本,因此只能读取
  • M(Modified,已修改)—— 仅该缓存持有此地址的最新数据。因此能够被,因此可读可写;其他缓存中的副本(如果有)已被置为 Invalid

MSI 协议状态机

MSI 协议三状态转移图与总线读写事件

  • 整体上看,越往上的权限越大,越往下权限越小
  • 系统启动后,默认是 Invalid 状态。
  • 在 Invalid 状态下, 如果处理器发出读请求,则缓存控制器发出 BusRd,转到 Shared 状态,表示我正在读该位置的数据,但没有修改;如果处理器发出写请求,则缓存控制器发出 BusRdX(bus read exclusive),转到 Modified 状态,语义是我想单独持有这个位置的副本到我的缓存行;
  • 在 Shared 状态下,处理器若继续读该地址,保持在 Shared 状态;若发出写请求,则发出 BusRdX 并转为 Modified;若监听到总线上对该地址的 BusRdX 请求,则本地对应缓存行需失效(Invalidate),转为 Invalid
  • 处于 Modified 状态时,若监听到总线上有 BusRd 请求,则触发 BusWB 写回事务,将缓存内容写回主存并转为 Shared;若监听到 BusRdX,则失效转为 Invalid;若处理器继续读写该地址,则直接命中缓存并响应,无需发出总线事务

同样,结合下面示例检验对协议的理解。

MSI 协议在多核读写交错下的状态追踪示例

MSI 协议在既定条件下,可以让缓存服务于处理器写操作,在不需要更新主存,即主存可以有过时数据。如果另一个核心(比如 Core 1)要读取这个地址,而该地址处于 Core 0 的缓存中且是 Modified 状态,由 Core 0 的缓存来“提供最新值”,而不是从主存读取。

但是问题是主存可能同时回应这个读请求,由于主存和缓存都在监听,主存并不知道自己的数据已经过时。

MESI 协议

一个观察,就是“读取后修改并写入 Read-modify-write”的操作序列非常常见,具体来说对私有数据执行Read-modify-write 需要在总线上发起 2 个事务(BusRd + BusRdX),即便没有其他核用到这个地址。为了解决问题,MSI 协议的优化: 增加独占状态(Exclusive State),用以提高 read-write 私有数据的性能

每个缓存行维护的状态:

  • Invalid:所有缓存行的初始状态是无效的,表示 tag 和 data 不包含最新的信息。这对应于我们原始缓存实现中的有效位设置为 0。
  • Exclusive:unmodified exclusive 。当一个核心发出 BusRd 请求,而没有其他缓存拥有该数据副本时,它会拿到 E 状态(不是 S 状态),表示数据是独占的,数据与主存是一致的(干净 clean),这对应于我们原始缓存实现中的有效位设置为 1。当处理器随后对该地址进行写操作时,“静默升级”,不会占用总线带宽。即不需要再发 BusRdX,直接在本地将 E → M,即修改数据 → 独占 + 脏(dirty)
  • Modified(修改):Modified Exclusive 。这对应于我们原始缓存实现中的脏位和有效位都设置为 1。
  • Shared(共享):当其他缓存也可能拥有相同未修改的内存数据副本时,缓存行状态为共享。

MESI 协议 FSM

MESI 协议四状态有限状态机与静默升级路径

  • 系统启动后,默认是 Invalid 状态。
  • 在 Invalid 状态下, 如果处理器发出读请求,则缓存控制器发出 BusRd,如果有其他 sharer,则转到 Shared 状态,而如果没有其他 sharer,则转到 Exclusive 状态;如果处理器发出写请求,则缓存控制器发出 BusRdX(bus read exclusive),转到 Modified 状态
  • 在 Exclusive 状态下,如果控制器发出写请求,此时缓存控制器不需要发出 BusRdX 事务,可以直接转到 Modified 状态。而如果是读请求则继续待在 E 状态; 如果监听到有其他 sharer 加入该内存位置的读事务,则会转为 Shared 状态
  • 在 Shared 状态下,处理器若继续读该地址,保持在 Shared 状态;若发出写请求,则发出 BusRdX 并转为 Modified;若监听到总线上对该地址的 BusRdX 请求,则本地对应缓存行需失效(Invalidate),转为 Invalid
  • 处于 Modified 状态时,若监听到总线上有 BusRd 请求,则触发 BusWB 写回事务,将缓存内容写回主存并转为 Shared;若监听到 BusRdX,则失效转为 Invalid;若处理器继续读写该地址,则直接命中缓存并响应,无需发出总线事务

MOESI 协议

MOESI 在 MESI 基础上增加了 Owned (O) 状态,共五种状态:

  • Owned (O):
    • 当前缓存持有最新数据,但其他缓存可能有 S 状态的副本(与 M 不同,M 要求其他缓存必须无效)。
    • 允许直接向其他缓存提供数据,而无需写回内存,减少内存访问。

MESIF 协议

在 MOESI 中,如果多个缓存持有 S 状态的副本,当另一个核心请求该数据时,所有 S 状态的缓存都可能响应(取决于具体实现),导致总线争用(bus contention)。

MESIF 的解决方案:引入 F 状态,规定只有一个缓存(F 状态)能响应共享数据请求,其他 S 状态缓存不响应,类似“数据代理”, 减少总线上的冗余响应。

Directory-based 一致性

基于分布式目录的 Directory-based 一致性网络架构

随着核数规模的增大, 会趋近于使用基于目录缓存一致性协议,这里不陷入细节,只是探讨下其主要思想。所有一致性操作都通过目录中转,目录记录每个缓存块的状态及其所在私有缓存的位置,避免了广播开销;同时目录也作为冲突请求的全序点,使系统可以在非全序网络(unordered network)中维持一致性。

此类系统中通常涉及三个处理器角色:本地节点(请求发起处)、主节点(目标地址所在的内存位置)、远程节点(缓存了该地址的数据,无论是共享还是独占)。

每个处理器核有自己的私有缓存,以及负责部分内存区域,I/O,以及目录,目录记录着这部分内存发生了什么事情,谁由记录着缓存这片内存的东西。 当本地节点需要请求某个区域内存时,它会直接找到负责的处理器核,而不是通过广播。

缓存一致性的实践经验

伪共享

缓存行通常包含多个字(word),通常是 64 字节,一个 word 要么是 4 要么是 8 字节,索引包含多个变量或者数组元素。缓存一致性协议是以整个缓存行为单位来维护的(而不是单个字)。

伪共享现象中多核交替写入同缓存行引起的乒乓效应

假设处理器 P1 写 wordiword_i,而处理器 P2 写wordkword_k,并且两个字都在同个缓存行地址,会发生什么? 出现 ping-pong 效应,严重影响性能。

如果你想避免这种情况,可以用 结构体填充(padding) 或 alignas() 等方式强制变量分布在不同缓存行。

我的理解

缓存一致性(Cache Coherence)与内存一致性模型(Memory Consistency Model)是两个正交但紧密耦合的维度。缓存一致性协议仅保证“对单一内存地址的访问具有全局一致的写入串行化”;而内存一致性模型则定义了“对不同内存地址的访问在跨核视角下的可见顺序(如 TSO 或 Weak Ordering)”。硬件一致性协议(如 MESI)通过缓存行失效与独占所有权传递,在微架构底层维持了单地址视图的串行因果律,但多地址间的乱序重排仍必须依赖编译期与运行期的内存屏障指令来协调。