以下是一些替代方案的比较:
内联汇编,如@luke h的回答。
-
__sync_bool_compare_and_swap(): a GNU extension, gcc/clang/ICC-only, 已弃用, 编译器将发出 CMPXCHG16B 指令的伪函数至少与 -mcx16 一起
李>
atomic_compare_exchange_weak() / strong:在 C++11 中执行 atomic<> 的 C11 伪函数。对于 GNU,这不会在 gcc 7 及更高版本中发出CMPXCHG16B,而是调用libatomic(因此必须链接到)。动态链接的libatomic 将根据 CPU 的能力决定使用哪个版本的函数,并且在 CPU 能够CMPXCHG16B 的机器上,它将使用它。
显然clang 将仍将CMPXCHG16B 内联为atomic_compare_exchange_weak() 或strong
我没有尝试过机器语言,但是看看 (2) 的反汇编,它看起来很完美,我看不出 (1) 怎么能打败它。 (我对 x86 知之甚少,但编写了很多 6502。)此外,如果可以避免它,有很多建议永远不要使用汇编,并且至少可以通过 gcc/clang 避免它。所以我可以把 (1) 划掉。
这是 gcc 版本 9.2.1 20190827 (Red Hat 9.2.1-1) (GCC) 中 (2) 的代码:
Thread 2 "mybinary" hit Breakpoint 1, MyFunc() at myfile.c:586
586 if ( __sync_bool_compare_and_swap( &myvar,
=> 0x0000000000407262 <MyFunc+904>: 49 89 c2 mov %rax,%r10
0x0000000000407265 <MyFunc+907>: 49 89 d3 mov %rdx,%r11
(gdb) n
587 was, new ) ) {
=> 0x0000000000407268 <MyFunc+910>: 48 8b 45 a0 mov -0x60(%rbp),%rax
0x000000000040726c <MyFunc+914>: 48 8b 55 a8 mov -0x58(%rbp),%rdx
(gdb) n
586 if ( __sync_bool_compare_and_swap( &myvar,
=> 0x0000000000407270 <MyFunc+918>: 48 c7 c6 00 d3 42 00 mov $0x42d300,%rsi
0x0000000000407277 <MyFunc+925>: 4c 89 d3 mov %r10,%rbx
0x000000000040727a <MyFunc+928>: 4c 89 d9 mov %r11,%rcx
0x000000000040727d <MyFunc+931>: f0 48 0f c7 8e 70 04 00 00 lock cmpxchg16b 0x470(%rsi)
0x0000000000407286 <MyFunc+940>: 0f 94 c0 sete %al
然后对现实世界的算法进行 (2) 和 (3) 的锤击测试,我没有看到真正的性能差异。即使在理论上,(3) 也只有一个额外的函数调用的开销和 libatomic 包装函数中的一些工作,包括一个关于 CAS 是否成功的分支。
(使用惰性动态链接,对 libatomic 函数的第一次调用实际上将运行一个 init 函数,该函数使用 CPUID 来检查您的 CPU 是否有cmpxchg16b。然后它将更新 PLT 存根跳过的 GOT 函数指针, 所以未来的调用将直接转到使用lock cmpxchg16b 的libat_compare_exchange_16_i1。名称中的i1 后缀来自GCC's ifunc mechanism 用于功能多版本控制;如果您在没有cmpxchg16b 支持的CPU 上运行它,它会将共享库函数解析为使用锁定的版本。)
在我的真实世界的锤子测试中,函数调用开销因无锁机制保护的功能占用的 CPU 量而丢失。因此,我认为没有理由使用 __sync 函数,这些函数是特定于编译器的并且不推荐用于引导。
这是每个 .compare_exchange_weak() 调用的 libatomic 包装器的程序集,从单步执行我的 Fedora 31 上的程序集。如果使用 -fno-plt 编译,callq *__atomic_compare_exchange_16@GOTPCREL(%rip) 将内联到调用者中,避免PLT 并在程序启动时而不是在第一次调用时尽早运行 CPU 检测。
Thread 2 "tsquark" hit Breakpoint 2, 0x0000000000403210 in
__atomic_compare_exchange_16@plt ()
=> 0x0000000000403210 <__atomic_compare_exchange_16@plt+0>: ff 25 f2 8e 02 00 jmpq *0x28ef2(%rip) # 0x42c108 <__atomic_compare_exchange_16@got.plt>
(gdb) disas
Dump of assembler code for function __atomic_compare_exchange_16@plt:
=> 0x0000000000403210 <+0>: jmpq *0x28ef2(%rip) # 0x42c108 <__atomic_compare_exchange_16@got.plt>
0x0000000000403216 <+6>: pushq $0x1e
0x000000000040321b <+11>: jmpq 0x403020
End of assembler dump.
(gdb) s
Single stepping until exit from function __atomic_compare_exchange_16@plt,
...
0x00007ffff7fab250 in libat_compare_exchange_16_i1 () from /lib64/libatomic.so.1
=> 0x00007ffff7fab250 <libat_compare_exchange_16_i1+0>: f3 0f 1e fa endbr64
(gdb) disas
Dump of assembler code for function libat_compare_exchange_16_i1:
=> 0x00007ffff7fab250 <+0>: endbr64
0x00007ffff7fab254 <+4>: mov (%rsi),%r8
0x00007ffff7fab257 <+7>: mov 0x8(%rsi),%r9
0x00007ffff7fab25b <+11>: push %rbx
0x00007ffff7fab25c <+12>: mov %rdx,%rbx
0x00007ffff7fab25f <+15>: mov %r8,%rax
0x00007ffff7fab262 <+18>: mov %r9,%rdx
0x00007ffff7fab265 <+21>: lock cmpxchg16b (%rdi)
0x00007ffff7fab26a <+26>: mov %r9,%rcx
0x00007ffff7fab26d <+29>: xor %rax,%r8
0x00007ffff7fab270 <+32>: mov $0x1,%r9d
0x00007ffff7fab276 <+38>: xor %rdx,%rcx
0x00007ffff7fab279 <+41>: or %r8,%rcx
0x00007ffff7fab27c <+44>: je 0x7ffff7fab288 <libat_compare_exchange_16_i1+56>
0x00007ffff7fab27e <+46>: mov %rax,(%rsi)
0x00007ffff7fab281 <+49>: xor %r9d,%r9d
0x00007ffff7fab284 <+52>: mov %rdx,0x8(%rsi)
0x00007ffff7fab288 <+56>: mov %r9d,%eax
0x00007ffff7fab28b <+59>: pop %rbx
0x00007ffff7fab28c <+60>: retq
End of assembler dump.
使用 (2)不想指望他们安装正确的。我个人在源代码中下载了一个并错误地构建了它,因此 16 字节交换最终使用互斥锁:灾难。
我没试过 (4)。或者更确切地说,我开始在代码上出现太多警告/错误,gcc 没有评论就通过了,以至于我无法在预算时间内编译它。
请注意,虽然选项 2、3 和 4 看起来是相同的代码或几乎相同的代码应该可以工作,但实际上这三个选项都有很大不同的检查和警告,即使您有三个中的一个编译正常且没有警告在-Wall 中,如果您尝试其他选项之一,您可能会收到更多警告或错误。 __sync* 伪函数没有详细记录。 (事实上,文档只提到了 1/2/4/8 字节,而不是它们适用于 16 字节。同时,它们“有点”像函数模板一样工作,但你看不到模板,而且它们似乎对是否第一个和第二个 arg 类型实际上是同一类型,而 atomic_* 不是。)简而言之,比较 2、3 和 4 并不是您可能猜到的 3 分钟工作。