现代多核 CPU 几乎都为每个核心配备了私有缓存(通常是 L1 / L2),并共享更高级别的缓存或内存控制器。 这带来了巨大的性能收益,也引入了一个经典问题:同一份数据的多个副本如何保持一致? 这就是缓存一致性(Cache Coherence)要解决的事。
本文将从 Cache Line 讲起,介绍广泛使用的 MESI 协议,再聊聊并发编程里常见的 False Sharing(伪共享),帮助你在写高性能代码时少踩坑。
一、为什么需要缓存一致性?
假设核心 0 和核心 1 都缓存了地址 0x1000 处的变量 x,当前值为 1。
若核心 0 把 x 改成 2,而核心 1 的缓存里仍是 1,后续读取就会得到过期数据——程序语义被破坏。
一致性协议要保证:对同一内存地址,所有核心观察到的值最终一致,且写操作的可见性符合处理器内存模型的约定。 注意:一致性(Coherence)关注的是同一个地址的多副本; 内存序 / 内存模型(Consistency)关注的是多个地址上读写的全局顺序观感,二者相关但不是一回事。
二、Cache Line:一致性的基本单位
CPU 与缓存、缓存与内存之间,数据几乎从不按「一个字节」搬运,而是按固定大小的块—— Cache Line(缓存行)。在 x86 服务器上,常见大小是 64 字节。
这意味着:哪怕你只修改一个 int(4 字节),硬件也会把整条 64 字节的行标记为脏,
并在需要时把整行写回或在核心间传递。理解这一点,是理解 MESI 和伪共享的前提。
// 概念示意:一条 cache line 可容纳多个变量
// |---- 64 bytes cache line ----|
// | a(4) b(4) c(4) ... padding |
三、MESI 协议
MESI 是一种基于写失效(write-invalidate)的监听(snooping)或目录协议状态机。 每条缓存行在某个核心的私有缓存中,处于以下四种状态之一:
| 状态 | 含义 | 本核能做什么 |
|---|---|---|
| Modified | 已修改,且只在本核缓存中;与内存不一致 | 读 / 写都行;写回前需负责更新内存或转交所有权 |
| Exclusive | 独占干净副本,与内存一致;其他核没有 | 可读;写时可直接变为 M,无需总线广播失效 |
| Shared | 共享干净副本,可能有多个核持有 | 可读;若要写,需先让其他核的副本失效,再进入 M |
| Invalid | 无效,相当于「没有这行」 | 读会触发填充(可能从内存或其他核);写同理 |
典型场景简述:
- 读未命中:若其他核是 M/E,可能从该核拿到数据并都变为 S(或目录协议下的等价状态);否则从内存加载,常进入 E。
- 写 Shared 行:发出失效请求,其他核 S→I,本核变为 M,再写入。
- 写 Exclusive 行:静默升级为 M,无需广播——这就是 E 状态存在的价值。
实际硬件还会扩展出 MOESI、MESIF 等变体(例如 AMD 的 Owner、Intel 的 Forward), 以减少写回带宽或优化共享读,但 MESI 的心智模型已经足够指导大多数软件层面的优化。
四、False Sharing(伪共享)
两个线程分别读写不同变量,但这两个变量落在同一条 Cache Line 上。 从软件看它们毫无共享,从硬件看整行在两个核的缓存间被来回失效、传递—— 性能像「真共享」一样糟糕。这就是伪共享。
struct Counter {
int thread0_count; // 核心 0 频繁 ++
int thread1_count; // 核心 1 频繁 ++
// 两者很可能落在同一 64B cache line → 伪共享
};
// 缓解:对齐 / 填充,让热点字段独占一行
struct alignas(64) PaddedCounter {
int count;
char pad[60];
};
在 Java 里常见 @Contended;在 C/C++ 里用 alignas(64) 或手动 padding。
无谓的伪共享会让「看着很轻量」的计数器在多核上扩展性极差,profiling 时要特别留意。
五、和程序员有什么关系?
- 数据结构布局:高频写的字段不要挤在一行;只读共享的数据可以紧凑存放以利预取。
- 锁与原子变量:一把锁旁边若紧挨着热数据,锁的 bounce 可能拖累整行。
- false sharing 排查:perf c2c、Intel VTune 等工具可以帮你定位跨核的 cache line 争夺。
- 不要过度对齐:每字段都 pad 到 64B 会浪费缓存容量;只对真正的热点竞争点下手。
摸鱼提示:左边「广告」其实是视频窗。文章看累了就加载个 B 站,看懂 MESI 再回来——学习与摸鱼并不矛盾。
六、小结
缓存一致性让多核可以各自缓存同一内存,又通过 MESI 一类协议维持正确性。 Cache Line 是硬件一致性的粒度;忽视它就会撞上伪共享。 写出更快的并发代码,往往不是更多的技巧,而是更清楚数据在缓存里如何移动。
后续可继续阅读:内存屏障与 acquire/release、Store Buffer、以及 Linux 内核中的
smp_mb() 等实现细节。
评论区(演示)