伪共享是多线程访问不同变量却因同处一64字节缓存行,触发频繁缓存同步(如mesi协议)导致性能下降;典型表现为atomic操作耗时陡增、lock指令高频出现且单/多协程性能差异显著。

pprof 不能直接告诉你“这是伪共享”,但能精准暴露伪共享引发的典型症状——你得知道该盯住什么、怎么交叉验证。
看到 LOCK XADDQ 却没锁,就要怀疑伪共享
pprof 的 CPU profile 显示某个原子操作函数(比如 atomic.AddInt64)耗时异常高,火焰图里它几乎独占 CPU 时间,但函数本身只是简单加一,逻辑上无竞争——这已是强信号。关键下一步是看汇编:
- 运行
go tool pprof -disasm=IncA cpu.pprof,若看到LOCK XADDQ指令反复出现,说明硬件已介入强制同步,不是 Go 运行时的问题 - 对比单 goroutine 场景:同样代码,单协程下该函数耗时极低,多协程下陡增 3–5 倍,基本可排除调度或 GC 干扰
- 注意:Go 1.19+ 对
int64原子操作默认生成LOCK指令,所以不能只看有没有LOCK,而要看「是否本不该有却高频出现」
用 unsafe.Offsetof 算字段地址差是否小于 64
伪共享发生的物理前提是两个字段落在同一缓存行。主流 x86-64 / ARM64 缓存行大小为 64 字节,所以必须实测偏移:
- 写个临时调试函数,打印
unsafe.Offsetof(counter.A)和unsafe.Offsetof(counter.B),差值若 - 别信
unsafe.Alignof——它返回的是类型对齐要求(如 8),不是缓存行大小 - 填充后务必重测:改完结构体,重新编译(禁用优化:
go build -gcflags="-N -l"仅用于调试,发布版必须用默认优化)
填充字段必须放对位置,[56]byte 是最稳选择
填在字段后面不够,因为前一个字段可能已经占了同一缓存行的前半部分。真正起效的填充要「围住」热点字段:
- 正确模式:
A int64; pad [56]byte; B int64→ 确保 A 和 B 至少相隔 64 字节 - 用
[56]byte而非[7]uint64或其他组合,是因为数组大小固定、编译器不会重排、且不参与任何计算 - 填充字段名建议用
pad0、pad1等明确标识,避免被误认为业务字段;必须首字母大写(导出),否则unsafe.Offsetof可能失效 -
//go:align 64没用——它只影响整个 struct 实例的起始地址对齐,不改变字段间相对偏移
压测前后对比不能只看平均延迟
伪共享修复效果最明显的指标不是 QPS 提升多少,而是尾部延迟收敛和缓存未命中率下降:
- 用
perf stat -e cache-misses,cache-references对比,修复后缓存未命中率应明显降低(常降 40%+) - 压测时重点看 P99/P999 延迟:未修复时 P99 可能抖动剧烈(如 10μs → 200μs),修复后应稳定在低位
- 别在本地开发机跑结论——不同 CPU(尤其 ARM64 部分芯片缓存行为为 128 字节)表现差异大,务必在目标部署环境验证
最容易被忽略的一点:伪共享只在「多核高频写不同字段」时生效。如果字段只是初始化一次、或更新频率低于微秒级、或由 sync.Mutex 保护,填充反而浪费内存,还可能干扰 CPU 预取。动手前先确认场景是否真符合这三个条件。











