伪共享不是 bug,也不会触发 TSan 报告 data race,但它会让 4 线程比 1 线程还慢——根本原因就一个:两个线程写的变量,物理上挤在同一个 cache line 里。

伪共享不是 bug,也不会触发 TSan 报告 data race,但它会让 4 线程比 1 线程还慢——根本原因就一个:两个线程写的变量,物理上挤在同一个 cache line 里。
alignas(64) 是最直接有效的起点
现代 x86-64 CPU 的 cache line 普遍是 64 字节,alignas(64) 能强制变量起始地址对齐到 64 字节边界,极大降低跨行风险。但要注意:
-
alignas(64)只控制起始地址,不保证整个对象不跨行;如果结构体本身超过 64 字节(比如含多个std::atomic和数组),仍可能和下一个成员共享缓存行 - 在数组场景下,
alignas(64) struct Counter { int64_t val; } arr[1024];会导致内存占用翻倍甚至更多,得看是否可接受 - 嵌入式或老平台(如某些 ARMv7)
cache line可能是 32 字节,硬写64会浪费空间
优先用 std::hardware_destructive_interference_size 替代魔法数字
这个 C++17 常量本意就是提供“安全隔离尺寸”,但它在旧版 GCC(
- 先检查是否可用:
#ifdef __cpp_lib_hardware_interference_size - 不可用时,回退到
constexpr size_t cache_line_size = 64;或根据目标平台手动设为 32/64/128 - 不要直接写
alignas(std::hardware_destructive_interference_size)而不做判断,否则编译失败
结构体内字段之间也要防挤占
即使整个结构体用 alignas(64),内部字段若没隔离,仍可能互相污染。典型反例:
立即学习“C++免费学习笔记(深入)”;
struct Counter {
std::atomic<int64_t> a;
std::atomic<int64_t> b; // 和 a 极可能落在同一 cache line
};正确做法是让高频写字段各自独占缓存行:
- 用填充字节隔离:
char padding[64 - sizeof(std::atomic<int64_t>)];</int64_t> - 或拆成独立对齐结构:
alignas(64) std::atomic<int64_t> a;</int64_t>和alignas(64) std::atomic<int64_t> b;</int64_t>(注意:这两个变量不能是同一结构体的普通成员,否则对齐失效) - 更稳妥的是把每个写入点封装为独立对齐类型,比如
struct alignas(64) PaddedAtomic { std::atomic<int64_t> v; };</int64_t>
thread_local 比对齐更省事,但适用场景有限
如果变量天然属于单线程生命周期(比如每个线程私有的计数器、临时缓冲区),thread_local 是零成本解法——它从根源上避免跨核访问,完全绕开缓存一致性协议。
- 适用于:线程启动时初始化、全程不跨线程传递、无需全局聚合的场景
- 不适用:需要多线程协作更新的共享状态(如无锁队列的
m_head/m_tail),这类必须靠对齐+隔离 - 注意
thread_local初始化开销(首次访问时构造),高频短命线程可能有影响
真正难处理的从来不是单个变量,而是数组里连续排布的计数器、环形缓冲区里紧挨着的 head/tail 原子变量——这些地方哪怕差一个字节没对齐,性能就掉进坑里。别依赖编译器自动优化,该 pad 就 pad,该 align 就 align,用 perf 或 VTune 验证是否真分开了。


















