不能像store一样在x86上放宽原子fetch_add重新排序,稍后加载?

该程序有时会打印 00,但如果我注释掉 a.store 和 b.store 并取消注释 a.fetch_add 和 b.fetch_add ,它们执行完全相同的操作,即都设置 a=1,b=1 的值,我从不得到00。(在 x86-64 Intel i3 上测试,使用 g++ -O2)

我是不是遗漏了什么,或者按照标准“00”永远不会出现?

这是带有普通商店的版本,可以打印00。

// g++ -O2 -pthread axbx.cpp  ; while [ true ]; do ./a.out  | grep "00" ; done
#include<cstdio>
#include<thread>
#include<atomic>
using namespace std;
atomic<int> a,b;
int reta,retb;

void foo(){
        //a.fetch_add(1,memory_order_relaxed);
        a.store(1,memory_order_relaxed);
        retb=b.load(memory_order_relaxed);
}

void bar(){
        //b.fetch_add(1,memory_order_relaxed);
        b.store(1,memory_order_relaxed);
        reta=a.load(memory_order_relaxed);
}

int main(){
        thread t[2]{ thread(foo),thread(bar) };
        t[0].join(); t[1].join();
        printf("%d%dn",reta,retb);
        return 0;
}

下面从不打印 00

// g++ -O2 -pthread axbx.cpp  ; while [ true ]; do ./a.out  | grep "00" ; done
#include<cstdio>
#include<thread>
#include<atomic>
using namespace std;
atomic<int> a,b;
int reta,retb;

void foo(){
        a.fetch_add(1,memory_order_relaxed);
        //a.store(1,memory_order_relaxed);
        retb=b.load(memory_order_relaxed);
}

void bar(){
        b.fetch_add(1,memory_order_relaxed);
        //b.store(1,memory_order_relaxed);
        reta=a.load(memory_order_relaxed);
}

int main(){
        thread t[2]{ thread(foo),thread(bar) };
        t[0].join(); t[1].join();
        printf("%d%dn",reta,retb);
        return 0;
}

也看看这个多线程原子 ab 为 memory_order_relaxed 打印 00

回答

标准允许00,但你永远不会在 x86 上得到它(没有编译时重新排序)。在 x86 上实现原子 RMW 的唯一方法涉及一个lockprefix,它是一个“完全屏障”,它对于 seq_cst 来说足够强大。

在 C++ 术语中,原子 RMW 在为 x86 编译时被有效地提升为 seq_cst。(只有在确定了可能的编译时排序之后——例如,非原子加载/存储可以通过宽松的 fetch_add 重新排序/组合,其他宽松的操作也是如此,以及使用获取或释放操作的单向重新排序。虽然编译器较少可能会相互重新排序原子操作,因为它们不会组合它们,这样做是编译时重新排序的主要原因之一。)

事实上,大多数编译器a.store(1, mo_seq_cst)通过 using xchg(它有一个隐式lock前缀)来实现,因为它比现代 CPU 上的mov+快mfence,并且将 0 变成 1lock add作为对每个对象的唯一写入是完全相同的。有趣的事实:只需存储和加载,您的代码将编译为与https://preshing.com/20120515/memory-reordering-caught-in-the-act/相同的 asm ,因此此处的讨论适用。


ISO C++ 允许整个松散的 RMW 以松散的负载重新排序,但普通编译器不会在编译时无缘无故地这样做。(DeathStation 9000 C++ 实现可以/将会)。因此,您终于找到了在不同的 ISA 上进行测试很有用的案例。原子 RMW(或它的一部分)在运行时重新排序的方式在很大程度上取决于 ISA。


需要重试循环来实现 fetch_add的LL/SC机器(例如 ARM,或ARMv8.1 之前的 AArch64)可能能够真正实现可以在运行时重新排序的宽松 RMW,因为任何比宽松更强大的东西都需要障碍。(或获取/发布指令的版本,如AArch64 ldaxr / stlxrvs. ldxr/ stxr)。因此,如果relaxed 和acq 和/或rel 之间存在asm 差异(有时seq_cst 也不同),则可能需要差异并防止某些运行时重新排序。

在 AArch64 上,即使是单指令原子操作也能真正放松;我没有调查过。大多数弱排序的 ISA 传统上都使用 LL/SC 原子,所以我可能只是将它们混为一谈。

在 LL/SC 机器中,LL/SC RMW 的存储端甚至可以与以后的负载分开重新排序,除非它们都是 seq_cst。 出于排序的目的,原子读-修改-写是一种操作还是两种操作?


要实际看到00,两个加载都必须在 RMW 的存储部分在另一个线程中可见之前发生。是的,我认为 LL/SC 机器中的硬件重新排序机制与重新排序普通商店非常相似。


以上是不能像store一样在x86上放宽原子fetch_add重新排序,稍后加载?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>