False Sharing(伪共享) 是多核编程中一个极其隐蔽的性能杀手。它发生在这种场景:两个完全无关的变量,因为恰好被编译器或分配器放到了同一个 Cache Line(缓存行) 中,导致本应并行的多线程操作变成了事实上串行的“总线拉锯战”。
1. 为什么会发生 False Sharing?
理解这个问题的关键在于:CPU 缓存同步的最小单位不是字节,而是缓存行(通常为 64 字节)。
-
同住一屋: 假设线程 A 只需要操作变量
X,线程 B 只需要操作变量Y。但由于X和Y在内存中紧挨着,它们被同时载入了同一个缓存行。 -
MESI 惩罚: * 核心 1 修改了
X,根据 MESI 协议,它必须将核心 2 对应的整个缓存行设为 Invalid (无效) 。- 核心 2 想要修改
Y时,发现缓存行失效,必须重新从主内存(或核心 1 的缓存)拉取数据。
- 核心 2 想要修改
-
结果: 尽管
X和Y逻辑上毫无关系,但两个核心会为了争夺这块缓存行的“修改权”而频繁打架,产生巨大的总线开销。
2. 在锁设计中如何避免?
在设计高性能锁(如自旋锁或无锁队列)时,避免伪共享是进入“工业级”门槛的标志。常见策略如下:
A. 缓存行对齐(Padding / Alignment)
这是最直接的方法:在两个频繁修改的变量之间插入“无用”的填充数据,强行将它们推到不同的缓存行。
-
手动填充: 在 C/C++ 中增加无用的数组。
C++
struct MyLock { volatile int lockA; long long padding[8]; // 填充 64 字节,确保 lockB 另起一行 volatile int lockB; }; -
语言特性支持: * C++11: 使用
alignas(64)。- Java 8+: 使用
@Contended注解(需要开启 JVM 参数)。 - Swift: 使用
managedBuffer或手动对齐分配。
- Java 8+: 使用
B. 空间换时间(Array Padding)
在处理锁数组(如 Striped Lock)时,如果数组元素是连续的 int,竞争会非常严重。
- 方案: 不要直接定义
int locks[16],而是定义一个结构体,让每个结构体的大小刚好等于一个缓存行(64 Bytes)。
C. 局部化数据(Local Copy)
尽可能让每个线程操作自己私有的副本,只在最后阶段进行合并(Reduce)。
- Per-CPU 变量: Linux 内核中广泛使用,每个核心拥有独立的计数器,彻底消除 MESI 协议在核心间的通信损耗。
3. 实战案例:Disruptor 框架
高性能并发框架 LMAX Disruptor 之所以快,很大程度上归功于它对缓存行的极致利用。它的 Sequence 类通过在左右两边各填充 7 个 long 变量(共 56 字节),配合自带的一个 long 变量(8 字节),完美占据了一个 64 字节的缓存行,确保无论这个序列号如何高频更新,都不会干扰到旁边的变量。
总结
- 现象: 无关变量“躺枪”,导致缓存频繁失效。
- 本质: MESI 协议以缓存行为单位进行颗粒度控制的副作用。
- 对策: 对齐(Alignment)与填充(Padding),让核心变量“独占一屋”。