1.4CPU缓存一致性

一、为什么会有缓存一致性问题?

1. CPU 缓存的层次结构

现代 CPU 为了弥补「CPU 运算速度」和「主存读写速度」之间的巨大差距(CPU 比主存快约 100 倍),引入了多级缓存架构:

  • L1 缓存:每个 CPU 核心私有,分为指令缓存和数据缓存,容量约 32KB~64KB,访问延迟约 1ns
  • L2 缓存:每个 CPU 核心私有,容量约 256KB~512KB,访问延迟约 3ns
  • L3 缓存:所有 CPU 核心共享,容量约几 MB 到几十 MB,访问延迟约 15ns
  • 主存(内存):所有核心共享,容量几十 GB,访问延迟约 100ns

核心问题 :L1、L2 是每个核心私有的,当多个核心同时操作同一个主存地址的数据时,每个核心的私有缓存中都会保存该数据的副本。如果一个核心修改了自己缓存中的副本,其他核心的缓存副本仍然是旧值,就会导致数据不一致。

2. 一个简单的不一致例子

假设主存中有一个全局变量 x = 0,两个 CPU 核心 Core1 和 Core2 同时操作它:

  1. Core1 读取 x,将 x 所在的 64 字节数据块从主存加载到自己的 L1 缓存,此时 Core1 缓存中 x=0
  2. Core2 也读取 x,同样将 x 加载到自己的 L1 缓存,此时 Core2 缓存中 x=0
  3. Core1 将 x 修改为 1,并更新到自己的 L1 缓存(此时还未写回主存)
  4. Core2 读取 x,由于自己的 L1 缓存中已经有 x 的副本,直接返回旧值 0

这就出现了严重的问题:Core1 已经把 x 改成了 1,但 Core2 看到的还是 0,两个核心对同一个变量的认知不一致。

二、缓存一致性的基础:缓存行(Cache Line)

CPU 缓存不是按单个字节管理的,而是按缓存行(Cache Line) 为单位进行数据传输和管理,主流 CPU 的缓存行大小是 64 字节

也就是说:

  • 即使你只修改 1 个字节,CPU 也会把该字节所在的整个 64 字节缓存行从主存读到缓存
  • 当缓存行被修改后,CPU 也会把整个 64 字节的缓存行写回主存

缓存行是缓存一致性协议的最小操作单位,所有一致性协议都是围绕「缓存行的状态同步」设计的。

三、主流缓存一致性协议:MESI 协议

MESI 是目前 x86 架构 CPU 使用最广泛的缓存一致性协议,它是一种基于总线监听的协议:所有核心都监听总线上的消息,根据消息更新自己缓存行的状态。

1. MESI 的四个状态

每个缓存行都有一个状态标记,共 4 种状态,这也是 MESI 名称的由来:

表格

状态 全称 含义
M Modified(修改态) 该缓存行的数据被当前核心修改过,与主存不一致;只有当前核心持有最新副本,其他核心的副本都是无效的;当该缓存行被替换出缓存时,必须先写回主存
E Exclusive(独占态) 该缓存行的数据与主存一致只有当前核心持有该副本,其他核心都没有;此时修改该缓存行无需通知其他核心,直接修改后状态变为 M
S Shared(共享态) 该缓存行的数据与主存一致有多个核心同时持有该副本;此时修改该缓存行必须先通知其他核心将副本置为无效
I Invalid(无效态) 该缓存行的数据无效,不能使用;要访问该数据必须重新从主存或其他核心的有效副本读取

2. MESI 协议的工作流程

我们还是用前面的变量 x(初始值 0)为例,完整演示 MESI 协议的状态流转:

步骤 1:Core1 首次读取 x
  • Core1 发送Read请求到系统总线
  • 主存和其他核心监听总线,主存返回 x 所在的缓存行
  • Core1 将缓存行加载到自己的 L1 缓存,状态设为E(独占)(因为只有它持有该副本)
步骤 2:Core2 首次读取 x
  • Core2 发送Read请求到总线
  • Core1 监听到总线的 Read 请求,发现自己持有该缓存行的 E 态副本,于是将自己的副本发送给 Core2,同时主存也返回该缓存行
  • Core1 和 Core2 的缓存行状态都变为S(共享)(现在两个核心都持有副本)
步骤 3:Core1 修改 x 为 1
  • Core1 发现自己的缓存行是 S 态,不能直接修改,于是发送Read For Ownership(RFO,读并独占)请求到总线
  • RFO 请求的作用是:通知其他核心将该缓存行置为无效,同时获取该缓存行的独占权
  • Core2 监听到 RFO 请求,将自己的缓存行状态设为I(无效),并返回确认消息
  • Core1 收到所有核心的确认后,将自己的缓存行状态改为M(修改),然后将 x 的值改为 1
  • 此时主存中的 x 仍然是 0,只有 Core1 的缓存中有最新值
步骤 4:Core2 再次读取 x
  • Core2 发现自己的缓存行是 I 态,发送Read请求到总线
  • Core1 监听到 Read 请求,发现自己持有该缓存行的 M 态副本,于是先将缓存行写回主存,然后将副本发送给 Core2
  • Core1 和 Core2 的缓存行状态都变回S(共享),主存中的 x 也更新为 1
步骤 5:Core2 修改 x 为 2
  • 流程和步骤 3 完全一致:Core2 发送 RFO 请求→Core1 将缓存行置为 I→Core2 的缓存行变为 M→修改 x 为 2

3. MESI 的核心消息类型

核心之间通过总线发送以下 5 种核心消息来同步缓存行状态:

  • Read:请求读取某个缓存行
  • Read For Ownership(RFO):请求读取并独占某个缓存行(用于修改共享态的缓存行)
  • Invalidate:通知其他核心将指定缓存行置为无效
  • Invalidate Acknowledge:确认收到 Invalidate 消息
  • Writeback:将修改后的缓存行写回主存

四、缓存一致性带来的性能问题:伪共享(False Sharing)

伪共享是缓存一致性协议最常见的性能陷阱,也是面试高频考点。

1. 什么是伪共享?

多个独立的变量被放在同一个缓存行中,而不同的 CPU 核心同时修改这些不同的变量时,会导致缓存行频繁地在核心之间失效和同步,即使这些变量之间没有任何逻辑关联,也会互相影响,严重降低性能。

2. 伪共享的例子

假设我们有两个全局变量ab,都是 int 类型(4 字节),它们在内存中是连续存储的,刚好位于同一个 64 字节的缓存行中:

cpp 复制代码
// 伪共享场景:a和b在同一个缓存行
struct Data {
    int a; // Core1只修改a
    int b; // Core2只修改b
};

此时:

  1. Core1 修改 a:发送 RFO 请求,让 Core2 的缓存行置为 I
  2. Core2 修改 b:发送 RFO 请求,让 Core1 的缓存行置为 I
  3. 如此反复,缓存行在两个核心之间来回失效和同步,性能会下降几十倍

本质:两个核心修改的是不同的变量,但因为它们在同一个缓存行,缓存一致性协议会把整个缓存行当作一个整体来处理,导致不必要的同步开销。

3. 伪共享的解决方法:缓存行填充

解决伪共享的核心思路是:让每个变量独占一个缓存行,避免多个变量共享同一个缓存行。

方法 1:手动填充字节

在变量前后填充足够的字节,让每个变量的大小刚好等于缓存行大小(64 字节):

c

运行

cpp 复制代码
// 解决伪共享:每个变量独占一个缓存行
struct Data {
    int a;
    char padding1[60]; // 4 + 60 = 64字节,填满一个缓存行
    int b;
    char padding2[60];
};
方法 2:编译器对齐指令

使用编译器提供的对齐指令,让变量自动对齐到缓存行边界:

  • C/C++:__attribute__((aligned(64)))
  • Java:@sun.misc.Contended(需要 JVM 参数-XX:-RestrictContended开启)

Java 示例:

java

运行

cpp 复制代码
// Java中解决伪共享
@Contended
public class Data {
    volatile int a;
    volatile int b;
}

五、扩展:其他缓存一致性协议

MESI 协议有很多变种,不同 CPU 架构会做优化:

  • MOESI 协议 :在 MESI 基础上增加了Owned(拥有态),允许核心直接将自己的 M 态缓存行共享给其他核心,无需先写回主存,AMD 处理器使用该协议
  • MESI-F 协议 :在 MESI 基础上增加了Forward(转发态),优化了共享态缓存行的读取性能,Intel 处理器使用该协议

六、缓存一致性与内存屏障的区别

很多人会混淆这两个概念,它们解决的是完全不同的问题:

  • 缓存一致性:解决「多个 CPU 核心的缓存副本不一致」的问题,由硬件自动实现,保证「一个核心修改的数据,其他核心最终能看到」
  • 内存屏障:解决「CPU 指令重排序」和「数据可见性时机」的问题,由软件显式调用,保证「修改操作对其他核心的可见性顺序」

简单来说:缓存一致性是硬件提供的基础保证,内存屏障是软件对硬件行为的显式控制。

总结

  1. 缓存一致性问题的根源是多核 CPU 的私有缓存架构,多个核心同时操作同一主存数据时会出现副本不一致
  2. 缓存行是缓存管理的最小单位(64 字节),所有一致性协议都围绕缓存行状态同步设计
  3. MESI 协议通过 4 种状态(M/E/S/I)和总线监听机制,实现了缓存行的一致性
  4. 伪共享是缓存一致性带来的主要性能问题,通过缓存行填充让每个变量独占一个缓存行可以解决
  5. 缓存一致性是硬件自动实现的,和内存屏障是不同层面的概念
相关推荐
周末也要写八哥3 小时前
Eclipse 2024全流程网盘下载与安装配置教程详解
java·ide·eclipse
来恩10034 小时前
JSTL的标签库种类
java·开发语言
图像僧4 小时前
vs2019中的属性页使用说明
java·开发语言·jvm
武子康4 小时前
Java-03 深入浅出 MyBatis 增删改查与映射配置详解
java·后端
静心观复4 小时前
.puml文件是什么,怎么用
java
YOU OU4 小时前
SpringBoot 日志
java·开发语言
江南十四行4 小时前
并发编程(二)
java·开发语言
qingfeng154155 小时前
企业微信 API 自动化开发指南:从消息回调到智能运营实战
java·开发语言·python·自动化·企业微信
Tirzano5 小时前
超大型组和用户缓存redis
redis·缓存·哈希算法