以下是一些比较的替代方案:
内联汇编,例如@luke h的回答。
__sync_bool_compare_and_swap()
:一个 GNU 扩展,gcc/clang/ICC-only,不推荐使用的伪函数,编译器将CMPXCHG16B
至少发出一条指令-mcx16
atomic_compare_exchange_weak()
/ :在 C++11 中strong
执行的 C11 伪函数。atomic<>
对于 GNU,这不会CMPXCHG16B
在 gcc 7 及更高版本中发出 a ,而是调用libatomic
(因此必须链接到)。动态链接libatomic
将根据 CPU 的能力决定使用哪个版本的功能,并且在 CPU 能够使用的机器上CMPXCHG16B
使用它。
显然铿锵声仍将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 函数的第一次调用实际上将运行一个使用 CPUID 来检查您的 CPU 是否具有cmpxchg16b
.直接libat_compare_exchange_16_i1
使用lock cmpxchg16b
.i1
名称中的后缀来自GCC 的 ifunc函数多版本控制机制;如果你在没有cmpxchg16b
支持的 CPU 上运行它,它会将共享库函数解析为使用锁定的版本。)
在我的真实世界的锤子测试中,函数调用开销在无锁机制保护的功能占用的 CPU 量中丢失了。因此,我认为没有理由使用特定于编译器且不推荐使用的,__sync
函数。
这是每个 调用的 libatomic 包装器的程序集.compare_exchange_weak()
,从单步执行我的 Fedora 31 上的程序集。如果使用 编译-fno-plt
,acallq *__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) 的唯一好处是,如果您的机器没有附带libatomic
(旧版 Red Hat 的情况)并且您没有能力要求系统管理员提供此功能或不想指望他们安装了正确的。我个人在源代码中下载了一个并错误地构建了它,因此 16 字节交换最终使用互斥锁:灾难。
我没试过(4)。或者更确切地说,我开始在代码上出现太多警告/错误,gcc 没有评论就通过了,以至于我无法在预算时间内编译它。
请注意,虽然选项 2、3 和 4 看起来是相同的代码或几乎相同的代码应该可以工作,但实际上这三个选项都有很大不同的检查和警告,即使您有三个中的一个编译良好且没有警告-Wall
,您如果您尝试其他选项之一,可能会收到更多警告或错误。__sync*
伪函数没有得到很好的记录。(事实上,文档只提到了 1/2/4/8 字节,而不是它们适用于 16 字节。同时,它们“有点”像函数模板一样工作,但你看不到模板,而且它们似乎对是否第一个和第二个 arg 类型实际上是相同的类型,但实际上atomic_*
并非如此。)简而言之,您可能猜到比较 2、3 和 4 并不是 3 分钟的工作。