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

问题出现在,如下示例。0号核心通过加载指令获取0xA地址上的值为2,接着2号核心向0xA地址设置值为3,此时0号核心上私有缓存的0xA的值是过时的。 解决办法就是,通过一个缓存一致性协议来控制缓存的内容,避免出现过期缓存行,比如,核心2在写入3之前将核心0的副本无效化(invalidate)。
Outline
- 缓存一致性协议
- 基于监听的缓存一致性
- 基于目录的缓存一致性
- 缓存一致性的实践经验
缓存一致性协议
缓存一致性协议必须实现两个规则:
- 写入传播(Write propagation): 写入操作最终都会被所有的处理器可视
- 写入串行(Write serialization): 同个位置的写入是串行的(即所有处理器看到的都是相同的顺序写入)
如何实现写入传播?
- 无效化写入协议(Write-invalidate protocol):执行写入操作之前使无效化所有其他缓存的副本
- 写入更新协议(Write-update protocol): 执行写入操作之后更新所有的缓存副本
如何实现写入串行?
- Snooping-based protocol(基于监听协议): 所有的缓存都通过一个总线来监听彼此的动作。
- 中小规模多核CPU(消费级CPU为主)
- Directory-based protocol(基于目录协议):缓存目录跟踪私有缓存和串行请求的内容。
- 大规模多核(DB、超算,因为 snooping 在规模大时不再高效)
- Tips: 类似电话窃听 vs 邮局,前者就像一群人监听同个线路,一旦有人说话,其他人能听到;后者向邮局(目录)在管理所有的信件(缓存块),谁要发、谁时候、谁当前持有,都记录在案。
Snooping-Based 一致性

在单核视角(我们平时学习的)如果这个地址在本地缓存里(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:当前缓存行是有效的,可以被处理器使用(只读)

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

这个简单VI缓存一致性协议有什么问题?
Solution: 即使根本没有其他缓存也缓存了这地址,写操作始终需要广播(总线写事务),带宽消耗大。□
MSI 协议
保持一致性: 在一致性内存系统中,所有的加载(load)和存储(store)操作都可以被排列成一个全局顺序。然而,当多个缓存中存在某个地址的副本时,这种一致性可能会被破坏。为了保证内存一致性,需要满足两个条件:一是任意时刻只能有一个缓存对某个地址拥有写权限;二是在执行写操作之后,不能有任何缓存保留该地址的陈旧副本
MSI协议解释。每个缓存行维持MSI状态:
- I (Invalid,无效)—— 缓存中没有该地址
- S(Shared,共享) —— 该缓存持有此地址的数据,但其他缓存也可能持有相同副本,因此只能读取
- M(Modified,已修改)—— 仅该缓存持有此地址的最新数据。因此能够被,因此可读可写;其他缓存中的副本(如果有)已被置为Invalid
MSI协议状态机

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

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

- 系统启动后,默认是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 一致性

随着核数规模的增大, 会趋近于使用基于目录缓存一致性协议,这里不陷入细节,只是探讨下其主要思想。所有一致性操作都通过目录中转,目录记录每个缓存块的状态及其所在私有缓存的位置,避免了广播开销;同时目录也作为冲突请求的全序点,使系统可以在非全序网络(unordered network)中维持一致性。
此类系统中通常涉及三个处理器角色:本地节点(请求发起处)、主节点(目标地址所在的内存位置)、远程节点(缓存了该地址的数据,无论是共享还是独占)。
每个处理器核有自己的cache,以及负责部分内存区域,I/O,以及目录,目录记录着这部分内存发生了什么事情,谁由记录着缓存这片内存的东西。 当本地节点需要请求某个区域内存时,它会直接找到负责的处理器核,而不是通过广播。
缓存一致性的实践经验
伪共享
缓存行通常包含多个字(word),通常是64字节,一个word要么是4要么是8字节,索引包含多个变量或者数组元素。缓存一致性协议是以整个缓存行为单位来维护的(而不是单个字)。

假设处理器P1 写
如果你想避免这种情况,可以用 结构体填充(padding) 或 alignas() 等方式强制变量分布在不同 cache line