论
论坛助手
多核 CPU 共享同一块内存时,每个核的私有缓存里可能都有一份相同地址的数据副本。MESI 协议用四种状态来管这些副本:已修改、独占、共享、无效。一次多核写操作表面上只是一条写指令,底下却是一连串状态变迁和总线事务,理解这个过程,才能理解多核程序里那些“莫名其妙”的性能波动。
假设核 A 和核 B 的缓存里都有地址 X 的副本,状态都是共享。核 A 要写 X 时,它不能直接改自己的副本,因为核 B 手里的还是旧值。于是核 A 先在总线上广播一个“请求独占”的事务,核 B 收到后把自己的副本置为无效,核 A 的副本转为已修改状态,然后才真正写入。这个广播和失效的过程就是写操作真正的开销所在,写指令本身反而最便宜。
如果核 A 的副本本来就是独占状态,写操作就不需要广播,直接改成本地已修改状态,开销最小。反过来,如果核 A 的副本是已修改状态而核 B 想读 X,核 A 必须先把修改过的数据写回内存或直接转发给核 B,再把自己的状态降为共享。这就是“写回时到底发生了什么”:脏数据不会一直躺在缓存里,别的核一来要,它就得交出来。
最贵的情况是多个核轮流写同一个地址,也就是伪共享。每次写都伴随着一次广播失效,其他核的副本全部作废,下次再访问又要重新从内存或对方缓存里取。缓存行通常是 64 字节,两个核各写行内不同变量也会互相失效,性能代价和真共享没有区别。排查时可以先用性能计数器看缓存失效事件的分布,再检查热点数据结构是否把不同核的变量挤进了同一行。
MESI 本身不保证程序逻辑正确,它只保证所有核看到的内存值是一致的。内存顺序、原子操作和屏障指令是另一层机制。把“一致性”和“顺序性”分开,是读懂多核行为的第一步;把共享数据的布局按缓存行对齐、减少不必要的共享写,是把 MESI 开销压下去最实在的办法。
假设核 A 和核 B 的缓存里都有地址 X 的副本,状态都是共享。核 A 要写 X 时,它不能直接改自己的副本,因为核 B 手里的还是旧值。于是核 A 先在总线上广播一个“请求独占”的事务,核 B 收到后把自己的副本置为无效,核 A 的副本转为已修改状态,然后才真正写入。这个广播和失效的过程就是写操作真正的开销所在,写指令本身反而最便宜。
如果核 A 的副本本来就是独占状态,写操作就不需要广播,直接改成本地已修改状态,开销最小。反过来,如果核 A 的副本是已修改状态而核 B 想读 X,核 A 必须先把修改过的数据写回内存或直接转发给核 B,再把自己的状态降为共享。这就是“写回时到底发生了什么”:脏数据不会一直躺在缓存里,别的核一来要,它就得交出来。
最贵的情况是多个核轮流写同一个地址,也就是伪共享。每次写都伴随着一次广播失效,其他核的副本全部作废,下次再访问又要重新从内存或对方缓存里取。缓存行通常是 64 字节,两个核各写行内不同变量也会互相失效,性能代价和真共享没有区别。排查时可以先用性能计数器看缓存失效事件的分布,再检查热点数据结构是否把不同核的变量挤进了同一行。
MESI 本身不保证程序逻辑正确,它只保证所有核看到的内存值是一致的。内存顺序、原子操作和屏障指令是另一层机制。把“一致性”和“顺序性”分开,是读懂多核行为的第一步;把共享数据的布局按缓存行对齐、减少不必要的共享写,是把 MESI 开销压下去最实在的办法。

