您已经拥有了关于语言律师部分的答案。但我想回答一个相关问题,即如何理解为什么在使用LL/SC进行RMW原子操作的可能CPU架构上,这是可能的。
C++11禁止这种重排序是没有意义的:在某些CPU架构中,这将需要存储-加载障碍。
实际上,在PowerPC上使用真正的编译器可能是可能的,因为它们将C++11内存顺序映射到汇编指令的方式。
在PowerPC64上,具有acq_rel交换和获取加载(使用指针参数而不是静态变量)的函数使用gcc6.3 -O3 -mregnames
编译如下。 这是来自C11版本的,因为我想查看MIPS和SPARC的clang输出,而且Godbolt的clang设置适用于C11 <atomic.h>
,但在使用-target sparc64
时对于C++11 <atomic>
失败。
#include <stdatomic.h>
long foo(_Atomic long *a, _Atomic int *b) {
atomic_exchange_explicit(b, 1, memory_order_acq_rel);
return atomic_load_explicit(a, memory_order_acquire);
}
(源代码和汇编 在Godbolt上提供MIPS32R6、SPARC64、ARM 32和PowerPC64的支持。)
foo:
lwsync # with seq_cst exchange this is full sync, not just lwsync
# gone if we use exchage with mo_acquire or relaxed
# so this barrier is providing release-store ordering
li %r9,1
.L2:
lwarx %r10,0,%r4 # load-linked from 0(%r4)
stwcx. %r9,0,%r4 # store-conditional 0(%r4)
bne %cr0,.L2 # retry if SC failed
isync # missing if we use exchange(1, mo_release) or relaxed
ld %r3,0(%r3) # 64-bit load double-word of *a
cmpw %cr7,%r3,%r3
bne- %cr7,$+4 # skip over the isync if something about the load? PowerPC is weird
isync # make the *a load a load-acquire
blr
isync
不是存储-加载屏障;它只需要先前的指令在本地完成(从乱序部分的核心中退役)。它不等待存储缓冲区被刷新,因此其他线程可以看到较早的存储。
因此,作为交换的一部分的SC (stwcx.
)存储可以坐在存储缓冲区中,并在随后的纯获取加载之后成为全局可见的。实际上,另一个Q&A已经问过这个问题,答案是我们认为这种重新排序是可能的。是否`isync`可以防止CPU PowerPC上存储-加载重排序?
如果纯负载是seq_cst
,PowerPC64 gcc将在ld
之前放置一个sync
。使exchange
seq_cst
不能防止重排序。请记住,C++11仅保证SC操作的单个总顺序,因此对于C++11来保证它,交换和加载都需要是SC。
所以,PowerPC在原子操作方面的C++11到汇编映射有些不寻常。大多数系统在存储器上放置更重的屏障,允许seq-cst加载更加便宜或只在一侧有屏障。我不确定这是否对于PowerPC著名的弱内存排序是必需的,或者是否存在其他选择。
https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html展示了各种架构上的一些可能实现。它提到了ARM的多个替代方案。
在AArch64上,对于线程1的问题的原始C++版本,我们会得到以下结果:
thread1():
adrp x0, .LANCHOR0
mov w1, 1
add x0, x0, :lo12:.LANCHOR0
.L2:
ldaxr w2, [x0] @ load-linked with acquire semantics
stlxr w3, w1, [x0] @ store-conditional with sc-release semantics
cbnz w3, .L2 @ retry until exchange succeeds
add x1, x0, 8 @ the compiler noticed the variables were next to each other
ldar w1, [x1] @ load-acquire
str w1, [x0, 12] @ r1 = load result
ret
重新排序无法在此处进行,因为AArch64获取加载与释放存储交互以提供顺序一致性,而不仅仅是普通的acq/rel。释放存储不能与后续获取加载重新排序。(它们可以与后续的普通加载重新排序,在纸上和可能在某些真实硬件中。如果避免在释放存储后立即使用获取加载,则AArch64 seq_cst可能比其他ISA便宜。但不幸的是,这使得acq/rel比x86更糟糕。这通过ARMv8.3-A
LDAPR修复,这是一个只有获取没有顺序获取的负载。它允许较早的存储器(甚至STLR)与其重新排序。因此,您只获得acq_rel,允许StoreLoad重新排序但不允许其他重新排序。(它也是ARMv8.2-A的可选功能)。)
在同样或替代具有普通释放LL/SC原子操作的机器上,很容易看出acq_rel无法阻止在交换的LL和SC之后但在不同缓存行上的后续加载变得全局可见。
如果像x86一样使用单个事务实现
exchange
,那么加载和存储在内存操作的全局顺序中是相邻的,那么肯定没有后续操作可以与
acq_rel
交换重排序,并且它基本上等同于
seq_cst
。
但是,LL / SC不必是真正的原子事务,即可为该位置提供RMW原子性。
实际上,一个单独的asm
swap
指令可以具有松散或acq_rel语义。SPARC64需要
membar
指令围绕其
swap
指令,因此与x86的
xchg
不同,它本身不是seq-cst。(与PowerPC相比,SPARC具有非常好/易读的指令助记符。好吧,基本上任何东西都比PowerPC更易读。)
因此,对C++11来说,要求这样做是没有意义的:它会损害在CPU上不需要存储-加载屏障的实现。
r1==0 && r2 == 0
,因为 acquire-release(和 consume-release)是成对出现的。只有一个线程获取(和释放)每个原子变量,所以下面的 relaxed load 可能会看到“旧”的值。 - Caleth