什么是 MESI 缓存一致性协议?
MESI 是多核 CPU 保证缓存一致性的状态机协议,用四个状态描述每条缓存行(cache line,通常 64 字节): Modified(已修改)、Exclusive(独占)、Shared(共享)、Invalid(无效)。核心规则只有一条: 写之前必须先拿到独占权,其他核的副本会被总线消息置为 Invalid,从而避免两个核同时写同一地址导致数据不一致。
1. 四个状态
| 状态 | 本核有副本 | 与其他核一致 | 内存中是最新值 | 含义 |
|---|---|---|---|---|
| M | 是 | 否(只有我有) | 否(脏) | 我改过,别人都没有 |
| E | 是 | 是(只有我有) | 是 | 独占且干净,可直接改 |
| S | 是 | 是(可能多核共享) | 是 | 只读共享 |
| I | 无效 | — | — | 副本作废,用之前必须重新取 |
2. 状态转移
本地读 本地写
I ───────────────► S/E I ───────────────► M(发 BusRdX,让别人失效)
S ───────────────► S S ───────────────► M(发 BusUpgr,只失效不取数)
E ───────────────► E E ───────────────► M(无总线消息,直接改)
M ───────────────► M M ───────────────► M
其他核读(BusRd) 其他核写(BusRdX)
M ───────────────► S(先回写内存) M ───────────────► I(先回写)
E ───────────────► S E ───────────────► I
S ───────────────► S S ───────────────► I
I ───────────────► I I ───────────────► I
关键的三个总线事务:BusRd(我要读,谁能给我)、BusRdX(我要写,请所有人失效)、
BusUpgr(我要写,但我已有副本,只需你们失效)。E 状态的价值就在这:独占干净时写不需要任何总线通信。
3. 伪共享(false sharing)
缓存以行为单位,两个变量落在同一行(64 字节内),即使逻辑上无关,也会互相把对方的缓存行打无效:
struct { a int64; b int64 } // a、b 在同一 cache line
核0 写 a → 该行变 M
核1 写 b → BusRdX → 核0 的行变 I
核0 再写 a → 又要 BusRdX … 来回 ping-pong,性能暴跌
// 反例:两个 goroutine 各加一个字段,却共享同一缓存行
type Bad struct{ a, b atomic.Int64 }
// 正例:用 padding 把两个字段隔到不同缓存行(64B)
type Padded struct {
a atomic.Int64
_ [56]byte // 8 + 56 = 64,b 落到下一行
b atomic.Int64
}
func BenchmarkFalseSharing(b *testing.B) {
var c Bad
var wg sync.WaitGroup
wg.Add(2)
go func() { for i := 0; i < b.N; i++ { c.a.Add(1) }; wg.Done() }()
go func() { for i := 0; i < b.N; i++ { c.b.Add(1) }; wg.Done() }()
wg.Wait()
}
# 定位伪共享:看 cache line 的 HITM(命中他人已修改行)次数
perf c2c record -a -- sleep 5
perf c2c report --stdio | head -40
真实案例:Java 的 @Contended、Go 的 sync.Pool 里的 pad、Linux 内核的 ____cacheline_aligned,
都是为了把热点变量隔离到独立缓存行。
4. 常见追问
Q:有了 MESI 为什么还需要内存屏障?
MESI 保证的是"缓存行最终一致",但每个核都有 store buffer 和无效队列,写操作可能延迟可见。
volatile/atomic 背后的 mfence、lock 前缀负责在必要时清空 store buffer、等待无效队列处理完,
从而提供顺序保证。一致性(coherence)≠ 内存序(consistency)。
Q:MESI 和 MOESI 差在哪? MOESI 多了 O(Owned)状态:多个核共享脏数据时,由 O 状态的核负责回写内存,避免"一写就要写回内存", AMD 与部分 ARM 实现采用。Intel 主流是 MESI 的变体 MESIF(F 表示 Forward,指定一个核负责响应读)。
Q:为什么缓存行是 64 字节? 空间局部性与总线效率的折中:行太小则标记开销大、总线事务多;行太大则伪共享更严重、传输粒度粗。 64 字节也是主流 DRAM 的突发长度。
Q:MESI 能解决 ABA 或原子性问题吗?
不能。它只保证单地址的读写一致性;复合操作(读-改-写)要靠 lock cmpxchg/CAS 这类原子指令,
MESI 只是让这些指令的失效广播更高效。
小结
MESI 用 M/E/S/I 四态和 BusRd/BusRdX/BusUpgr 三类总线事务,保证"写前独占、写后他核失效"。
它带来一个经典副作用——伪共享:无关变量共享缓存行会引发 ping-pong。
工程对策就是 padding 隔离热点 + perf c2c 定位。答题时把状态表、转移图、伪共享示例讲全即可。