Ole*_*eev 18 c++ atomic memory-barriers language-lawyer stdatomic
我在以下代码中有关于操作顺序的问题:
std::atomic<int> x;
std::atomic<int> y;
int r1;
int r2;
void thread1() {
y.exchange(1, std::memory_order_acq_rel);
r1 = x.load(std::memory_order_relaxed);
}
void thread2() {
x.exchange(1, std::memory_order_acq_rel);
r2 = y.load(std::memory_order_relaxed);
}
Run Code Online (Sandbox Code Playgroud)
鉴于std::memory_order_acquirecppreference页面上的描述(https://en.cppreference.com/w/cpp/atomic/memory_order),
具有此内存顺序的加载操作会对受影响的内存位置执行获取操作:在此加载之前,不能对当前线程中的读取或写入进行重新排序.
很明显,r1 == 0 && r2 == 0在跑步thread1和thread2同时之后永远不会有结果.
但是,我在C++标准中找不到任何措辞(现在查看C++ 14草案),这保证了两个宽松的加载不能与获取 - 释放交换重新排序.我错过了什么?
编辑:正如评论中所建议的那样,实际上可以使r1和r2都等于零.我已经更新了程序以使用load-acquire,如下所示:
std::atomic<int> x;
std::atomic<int> y;
int r1;
int r2;
void thread1() {
y.exchange(1, std::memory_order_acq_rel);
r1 = x.load(std::memory_order_acquire);
}
void thread2() {
x.exchange(1, std::memory_order_acq_rel);
r2 = y.load(std::memory_order_acquire);
}
Run Code Online (Sandbox Code Playgroud)
现在是有可能得到两个和r1以及r2同时执行后等于0 thread1和thread2?如果没有,哪些C++规则阻止了这个?
LWi*_*sey 11
该标准没有根据如何使用特定排序参数围绕原子操作排序操作来定义C++内存模型.相反,对于获取/发布排序模型,它定义了正式关系,例如"synchronize-with"和"before-before",用于指定如何在线程之间同步数据.
N4762,§29.4.2 - [atomics.order]
对原子对象M执行释放操作的原子操作A与对M执行获取操作的原子操作B同步,并且从由A开头的释放序列中的任何副作用获取其值.
在§6.8.2.1-9中,该标准还规定,如果存储A与负载B同步,则在A线程之前排序的任何内容"发生在B之后的任何顺序之前".
在第二个示例(第一个甚至更弱)中没有建立"synchronize-with"(以及因此发生在线程之间)关系,因为缺少运行时关系(检查来自负载的返回值).
但即使你确实检查了返回值,它也没有用,因为exchange操作实际上并没有"释放"任何东西(即在这些操作之前没有对内存操作进行排序).Neiter做原子加载操作'获取'任何东西,因为在加载后没有操作排序.
因此,根据标准,两个示例(包括0 0)中的负载的四种可能结果中的每一种都是有效的.实际上,标准给出的保证并不比memory_order_relaxed所有操作都强.
如果要在代码中排除0 0结果,则必须使用所有4个操作std::memory_order_seq_cst.这保证了所涉及的操作的单一总订单.
您已经对此的语言律师部分有了答案。但是我想回答一个相关的问题,即如何理解为什么在使用LL/SC for RMW atomics的可能的 CPU 架构上的 asm 中这可能是可能的。
C++11 禁止这种重新排序是没有意义的:在这种情况下,它需要一个存储加载屏障,而某些 CPU 架构可以避免这种情况。
考虑到 PowerPC 上的真实编译器将 C++11 内存顺序映射到 asm 指令的方式,这实际上可能是可能的。
在 PowerPC64 上,具有 acq_rel 交换和获取加载(使用指针 args 而不是静态变量)的函数编译如下gcc6.3 -O3 -mregnames。这是来自 C11 版本,因为我想查看 MIPS 和 SPARC 的 clang 输出,Godbolt 的 clang 设置适用于 C11,<atomic.h>但<atomic>在使用-target sparc64.
#include <stdatomic.h> // This is C11, not C++11, for Godbolt reasons
long foo(_Atomic long *a, _Atomic int *b) {
atomic_exchange_explicit(b, 1, memory_order_acq_rel);
//++*a;
return atomic_load_explicit(a, memory_order_acquire);
}
Run Code Online (Sandbox Code Playgroud)
(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
Run Code Online (Sandbox Code Playgroud)
isync不是存储加载障碍;它只需要前面的指令在本地完成(从核心的乱序部分退出)。它不会等待刷新存储缓冲区,以便其他线程可以看到较早的存储。
因此stwcx.,作为交换一部分的 SC ( ) 存储可以位于存储缓冲区中,并在跟随它的纯获取加载之后变得全局可见。 事实上,另一个问答已经问过这个问题,答案是我们认为这种重新排序是可能的。 `isync` 是否会阻止在 CPU PowerPC 上进行存储加载重新排序?
如果是纯负载seq_cst,PowerPC64 gcc 会sync在ld. 使得exchange seq_cst它不会阻止重新排序。请记住,C++11 仅保证 SC 操作的单个总顺序,因此交换和加载都需要为 C++11 提供 SC 来保证它。
所以 PowerPC 有一些不寻常的从 C++11 到 asm 的原子映射。大多数系统在 store 上设置了更重的障碍,允许 seq-cst 负载更便宜或只在一侧有障碍。我不确定这是否是 PowerPC 著名的弱内存排序所必需的,或者是否可能有其他选择。
https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html显示了各种架构上的一些可能实现。它提到了 ARM 的多种替代方案。
在 AArch64 上,我们为问题的原始 C++ 版本的 thread1 得到了这个:
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
Run Code Online (Sandbox Code Playgroud)
重新排序不能在那里发生,因为 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 之前变得全局可见。
如果exchange像在 x86 上那样使用单个事务来实现,那么加载和存储在内存操作的全局顺序中是相邻的,那么当然不能用acq_rel交换重新排序以后的操作,它基本上等同于seq_cst.
但是 LL/SC 不必是真正的原子事务才能为该位置提供 RMW 原子性。
事实上,单个 asmswap指令可以具有relaxed 或acq_rel 语义。SPARC64 需要membar围绕其swap指令的指令,因此与 x86 不同xchg,它本身不是 seq-cst。(SPARC 有非常好的/人类可读的指令助记符,尤其是与 PowerPC 相比。基本上任何东西都比 PowerPC 更具可读性。)
因此,C++11 要求它这样做是没有意义的:它会损害 CPU 上的实现,否则它不需要存储加载屏障。
| 归档时间: |
|
| 查看次数: |
780 次 |
| 最近记录: |