伪共享是多线程中不同CPU核心频繁读写同一cache line内不同变量,触发缓存一致性协议反复无效化整行,导致性能骤降;典型表现为atomic计数器吞吐异常、perf显示高cache-misses。

什么是伪共享,为什么它会让多线程变慢
伪共享(False Sharing)不是代码写错了,而是多个线程在不同 CPU 核心上,频繁读写**同一 cache line 里不同变量**,导致缓存一致性协议反复无效化整块 64 字节 cache line。结果是:变量本身不共享,但硬件强制“共享”了——性能掉得毫无征兆。
典型现象:std::atomic<int></int> 计数器在多线程下吞吐量远低于预期,perf 显示大量 LLC-misses 或 cache-references 异常高;用 valgrind --tool=cachegrind 能看到同一 cache line 被反复标记为 dirty。
关键点:只要两个变量地址差小于 64 字节(主流 x86_64 cache line 大小),又分别被不同核上的线程写入,就可能中招。
如何定位伪共享:从 perf 到手动对齐验证
别猜,先测。Linux 下最直接的方式是用 perf 抓硬件事件:
立即学习“C++免费学习笔记(深入)”;
perf stat -e cycles,instructions,cache-references,cache-misses,L1-dcache-loads,L1-dcache-load-misses ./your_program
如果 cache-misses 占 cache-references 比例超过 15%~20%,且线程数增加后吞吐不升反降,就要怀疑伪共享。
更准的方法是用 pahole 查结构体内存布局,确认热点变量是否挤在同一 cache line:
pahole -C YourStruct your_binary
常见陷阱:
-
std::vector<:atomic>></:atomic>连续存放——每个std::atomic<int></int>只占 4 字节,但默认不 padding,相邻几个原子变量大概率落在同一 cache line - 调试时加
std::cout 会掩盖问题(IO 同步开销远大于伪共享),导致误判“没性能问题” - 用
std::this_thread::sleep_for降低竞争强度,也会让伪共享效应消失,不能作为无问题的依据
怎么避免:对齐 + 分离 + 缓存友好布局
核心思路就一条:让会被不同线程写的变量,物理上不在同一 cache line。不是靠锁,也不是靠减少访问,是靠内存布局控制。
实操建议:
- 用
alignas(64)强制变量独占 cache line:struct alignas(64) PaddedCounter { std::atomic<int> value{0}; };注意:alignas(64)作用于类型或变量声明,不是成员;对单个std::atomic<int></int>变量加alignas(64)是合法且有效的 - 结构体内部填充:如果必须把多个计数器塞进一个 struct,用
char pad[64 - sizeof(std::atomic<int>)]</int>隔开,但不如alignas清晰可靠 - 数组场景慎用
std::vector:改用std::unique_ptr<paddedcounter></paddedcounter>或静态数组,避免 vector 内部连续分配放大伪共享风险 - 避免跨 cache line 的“看似安全”操作:比如
struct { char a; std::atomic<int> b; }</int>——a和b很可能同属一行,若a被另一线程写(哪怕只是日志标记位),也会拖累b
std::hardware_destructive_interference_size 是摆设吗
它是 C++17 引入的常量,理论上代表“应避免放在同一 cache line 的最小间隔”,值通常是 64。但它不是银弹:
- 部分平台返回 0(如某些嵌入式或旧编译器),
alignas(std::hardware_destructive_interference_size)会编译失败 - 即使值正确,也不能替代对实际访问模式的分析——两个只读变量放一起完全没问题,而一个读一个写就危险
- Clang/GCC 对
alignas的实现有细微差异,GCC 在类成员上有时忽略alignas(需升级到 11+),稳妥做法是对整个结构体而非单个成员加对齐 - 真正复杂的是 NUMA 系统:不同 socket 的 cache line 交互代价更高,此时光靠对齐不够,还需考虑线程绑核(
pthread_setaffinity_np)和内存分配节点(numactl)
伪共享的麻烦在于它藏得深、测不出来就以为没问题、一上线流量上来才爆发。最保险的做法,是在设计阶段就给高频写的共享状态预留 cache line 边界,而不是等性能毛刺出现再回头改内存布局。



















