“用 atomic 比 Mutex 快”只在一定条件下成立。这篇把同一个计数器在不同并发度下的耗时量出来,看 atomic、CAS 循环、Mutex 各自在哪里变慢,再用伪共享说明慢的真正原因是缓存行。
单位 ns/次 atomic G=1: 4.1 G=2: 10.5 G=4: 26.9 G=8: 53.7 cas G=1: 7.5 G=2: 24.1 G=4: 77.4 G=8: 254.9 mutex G=1: 7.7 G=2: 61.3 G=4: 185.3 G=8: 157.2
无竞争时 atomic 比 Mutex 快一点(4.1 对 7.7 ns),差别不大。8 个 goroutine 争同一个计数器,atomic 慢到 53.7 ns,Mutex 157.2 ns,CAS 循环 254.9 ns——它失败了要重试。三者慢的根源都是这一个缓存行在核之间来回传递。
局部变量 G=1: 0.3 G=8: 0.1
每个 goroutine 先在局部变量里累加,一次性汇总,每次只要 0.1–0.3 ns。更有意思的是伪共享:8 个 goroutine 各用自己的 atomic 变量,逻辑上互不相干,放在相邻内存里却是 57.1 ns,隔开 128 字节后只要 0.8 ns。慢的不是原子指令,是缓存行。
相邻放置(无填充) G=1: 4.0 G=8: 57.1 各占 128 字节 G=1: 3.9 G=8: 0.8
arm64 上 Add 编译成 LDADDALD,CompareAndSwap 编译成 CASALD,前面各有一个对 runtime.arm64HasATOMICS 的检查。另一条分支(没有原子扩展时)我没有展开。
main.add:TEXT main.add(SB), LEAF|NOFRAME|ABIInternal, $0-0 ; NOP ; HINT $0 ; MOVBU runtime.arm64HasATOMICS(SB), R1 ; TBZ $0, R1, 40 ; MOVD $1, R1 ; MOVD $main.v(SB), R2 ; LDADDALD R1, (R2), R0 main.cas:TEXT main.cas(SB), LEAF|NOFRAME|ABIInternal, $0-8 ; NOP ; ADD $1, R0, R1 ; MOVBU runtime.arm64HasATOMICS(SB), R2 ; TBZ $0, R2, 44 ; MOVD $main.v(SB), R2 ; MOVD R0, R27 ; CASALD R27, (R2), R1
计数这类单个变量用 atomic 比 Mutex 省;高并发下最好的办法是别共享——按 goroutine 分片、再汇总。包文档也建议优先用 channel 或 sync 包。以上是这台 8 线程 arm64 机器上的结论,别的核数、别的架构需要自己量。
implementing synchronization algorithms. These functions require great care to be used correctly. Except for special, low-level applications, synchronization is better done with channels or the facilities of the sync package. Share memory by communicating; don't communicate by sharing memory. The swap operation, implemented by the SwapT functions, is the atomic equivalent of: old = *addr *addr = new return old The compare-and-swap operation, implemented by the CompareAndSwapT functions, is the atomic equivalent of:
sync/atomic 文档开头就提醒这些函数要极其小心,除非是低层应用,同步最好用 channel 或 sync 包。