内存序与原子操作深入 演示
三种内存序(relaxed / acquire-release / seq_cst)在 x86-64 上生成的机器指令对比 + 真实竞争下的吞吐实测。
文件
| 文件 | 作用 |
|---|---|
asm-demo.cpp | 8 个 noinline 函数覆盖 store/load/fetch_add 的 relaxed/release/acquire/seq_cst 组合 |
perf.cpp | 4 线程各 5M 次 fetch_add,对比 relaxed / acq_rel / seq_cst 吞吐 |
spinlock.cpp | atomic_flag 手写自旋锁 vs std::mutex(futex)对比 |
构建与运行
bash
make asm # 反汇编:各内存序的机器指令
make perf # 吞吐对比(4 线程竞争)
make spinlock # 自旋锁 vs 互斥锁实测数据(2026-08-27,AMD EPYC 7K62 / GCC 10.2.1 / x86-64)
反汇编(-O2)
| 操作 | relaxed | release/acquire | seq_cst |
|---|---|---|---|
| store | movl $0x2a,(%rip) | movl $0x2a,(%rip)(与 relaxed 相同) | mov $0x2a,%eax + xchg %eax,(%rip) |
| load | mov (%rip),%eax | mov (%rip),%eax(与 relaxed 相同) | mov (%rip),%eax(与 relaxed 相同) |
| fetch_add | lock addl $0x1,(%rip) | — | lock addl $0x1,(%rip)(与 relaxed 相同) |
结论:x86 是 TSO 强序架构——load 天然带 acquire、store 天然带 release,所以 acquire/release 在 x86 上是零指令;只有 seq_cst 的 store 需要 xchg 提升到全局序;RMW 操作(fetch_add)无论什么内存序都必须 lock 前缀原子执行。
吞吐(4 线程 × 5M fetch_add,ns/op)
| 内存序 | 耗时 |
|---|---|
| relaxed | 10.92 |
| acq_rel | 11.98 |
| seq_cst | 11.88 |
三种内存序吞吐几乎相同(差异 <10%)——与反汇编结论一致:x86 上内存序差异不产生额外指令,瓶颈在 lock 前缀本身。
自旋锁 vs 互斥锁(4 线程 × 200K 临界区,ns/临界区)
| 锁 | 耗时 |
|---|---|
| atomic_flag 自旋锁 | 86.52 |
| std::mutex(futex) | 53.23 |
反直觉的结果:低竞争场景下 std::mutex 比自旋锁快。原因:互斥锁未竞争时走 futex 用户态快速路径,几乎零开销;而自旋锁在临界区外忙等,多个线程在同一个缓存行上反复 test_and_set,造成缓存行颠簸(cache line ping-pong)。自旋锁的优势只在"临界区极短且竞争低"时(省去 futex 系统调用)才能体现。
复现
bash
scp -r demos/cpp-expert/atomics-deep/ chzhuo@<server>:~/atomic_demo/
ssh chzhuo@<server> "bash ~/atomic_demo/run-remote.sh"