提问者:小点点

为什么具有顺序一致性的std::atomic存储使用XCHG?


为什么std::atomicstore

std::atomic<int> my_atomic;
my_atomic.store(1, std::memory_order_seq_cst);

当请求具有顺序一致性的存储时执行xchg

从技术上讲,具有读/写内存屏障的普通存储难道不就足够了吗?相当于:

_ReadWriteBarrier(); // Or `asm volatile("" ::: "memory");` for gcc/clang
my_atomic.store(1, std::memory_order_acquire);

我明确地说是x86


共1个答案

匿名用户

mov-storemgridxchg都是在x86上实现顺序一致性存储的有效方法。带有内存的xchg上的隐式lock前缀使其成为一个完整的内存屏障,就像x86上的所有原子RMW操作一样。

(x86的内存排序规则本质上使全屏障效应成为任何原子RMW的唯一选择:它同时是一个加载和一个存储,以全局顺序粘在一起。原子性要求加载和存储不被仅仅通过将存储排队到存储缓冲区中来分离,因此必须将其耗尽,负载侧的加载-加载排序要求它不重新排序。)

简单的mov是不够的;它只有释放语义学,没有顺序释放。(不像AArch64的stlr指令,它确实做了一个顺序释放存储,不能用后来的ldar顺序获取加载重新排序。这种选择显然是由C 11将seq_cst作为默认内存排序引起的。但是AArch64的普通存储要弱得多;放松不是释放。)

请参阅Jeff Presing关于获取/释放语义学的文章,并注意常规释放存储(如mov或除xchg之外的任何非锁定x86内存目标指令)允许对后续操作进行重新排序,包括获取加载(如mov或任何x86内存源操作数)。例如,如果释放存储释放了锁,那么以后的事情似乎发生在临界区内是可以的。

在不同的CPU上,mgridxchg之间存在性能差异,可能在热缓存与冷缓存以及竞争与非竞争的情况下。和/或在同一个线程中连续执行许多操作的吞吐量与单独执行一个操作的吞吐量,以及允许周围代码与原子操作重叠执行。

请参阅https://shipilev.net/blog/2014/on-the-fence-with-dependencies的实际基准测试mgridvs.lock addl 0美元,-8(%rsp)vs.(%rsp)作为一个完整的屏障(当你还没有商店要做的时候)。

在Intel Skylake硬件上,mgrid阻止独立ALU指令的乱序执行,但xchg不会。(请参阅SO答案底部的我的测试asm结果)。Intel的手册不要求它那么强大;只有lgrid记录了这样做。但是作为实现细节,在Skylake上乱序执行周围代码非常昂贵。

我没有测试过其他CPU,这可能是WC内存中的勘误SKL079、SKL079 MOVNTDQA可能通过早期MFENCE指令的微码修复的结果。勘误表的存在基本上证明了SKL过去能够在MFENCE之后执行指令。如果他们通过使MFENCE在微码中变得更强来修复它,我不会感到惊讶,这是一种明显增加对周围代码影响的钝器方法。

我只测试了L1d缓存中缓存线是热的单线程情况。(不是当它在内存中是冷的,或者当它在另一个内核上处于修改状态时。)xchg必须加载以前的值,对内存中的旧值创建“错误”依赖。但是mgrid强制CPU等待,直到以前的存储提交到L1d,这也要求缓存线到达(并且处于M状态)。所以在这方面它们可能大致相同,但是Intel的mgrid强制所有东西等待,而不仅仅是加载。

AMD的优化手册推荐xchg用于原子seq-cst存储。我以为英特尔推荐了旧gcc使用的movmgrid,但是英特尔的编译器在这里也使用xchg

当我测试时,我在Skylake上为xchg获得了比在同一位置重复的单线程循环中为movmgrid更好的吞吐量。有关一些详细信息,请参阅Agner Fog的microarch指南和说明表,但他并没有在锁定操作上花费太多时间。

关于C 11 seq-cstmy_atomic=4,请参见Goldbolt编译器资源管理器上的gcc/clang/ICC/MSVC输出;当SSE2可用时,gcc使用movmgrid。(使用-m32-mno-sse2让gcc也使用xchg。其他3个编译器都更喜欢默认调整的xchg,或者znver1(Ryzen)或skylake

Linux内核将xchg用于__smp_store_mb()

更新:最近的GCC(如GCC10)更改为像其他编译器一样使用xchg进行seq-cst存储,即使SSE2 formgrid可用。

另一个有趣的问题是如何编译atomic_thread_fence(mo_seq_cst);。显而易见的选项是mgrid,但是lock或dword[rsp],0是另一个有效的选项(并且在MFENCE不可用时由gcc-m32使用)。堆栈的底部通常已经在M状态的缓存中处于热状态。缺点是如果本地存储在那里,则会引入延迟。(如果它只是一个返回地址,则返回地址预测通常非常好,因此延迟ret读取它的能力不是什么大问题。)所以lock或dword[rsp-4],0在某些情况下可能值得考虑。(gcc确实考虑过,但因为它让valgrind不高兴而恢复了它。这是在知道它可能比mgrid更好之前,即使mgrid可用。)

目前,所有编译器都使用mgrid作为独立屏障。这些在C 11代码中很少见,但是对于真正的多线程代码来说,什么是最有效的,还需要更多的研究,因为真正的多线程代码在无锁通信的线程中进行真正的工作。

但是多个源代码建议使用lock add作为堆栈的屏障,而不是mgrid,所以Linux内核最近切换到使用它在x86上的smp_mb()实现,即使SSE2可用。

有关讨论,请参阅https://groups.google.com/d/msg/fa.linux.kernel/hNOoIZc6I9E/pVO3hB5ABAAJ,其中包括提到一些HSW/BDW的勘误表,关于从WC内存加载的movntdqa加载通过早期的locked指令。(与Skylake相反,在早期存储可见之前,不能让读取发生,而是mgrid。)

>

  • 在Linux4.14中,smp_mb()使用mb()。如果可用,则使用mgrid,否则锁定addl 0美元,0(%esp)

    __smp_store_mb(存储内存屏障)使用xchg(这在以后的内核中不会改变)。

    在Linux4.15中,smb_mb()使用lock; addl 0美元,-4(%esp)%rsp,而不是使用mb()。(内核即使在64位中也不使用红区,因此-4可能有助于避免本地变量的额外延迟)。

    驱动程序使用mb()来命令对MMIO区域的访问,但是smp_mb()在为单处理器系统编译时变成了无操作。更改mb()的风险更大,因为它更难测试(影响驱动程序),并且CPU有与lock和mgrid相关的勘误表。但是无论如何,mb()如果可用,则使用mgrid,否则lock addl 0美元,-4(%esp)。唯一的变化是-4

    在Linux4.16中,除了删除#if定义(CONFIG_X86_PPRO_FENCE)之外没有任何变化,它为比现代硬件实现的x86-TSO模型更弱有序的内存模型定义了东西。

    x86

    my_atomic.store(1, std::memory_order_acquire);无法编译,因为只写原子操作不能是获取操作。另请参阅Jeff Presing关于获取/释放语义学的文章。

    asm易失性(":::"内存");

    不,这只是一个编译器障碍;它可以防止所有编译时重新排序,但不能阻止运行时StoreLoad重新排序,即存储直到稍后才被缓冲,并且直到稍后加载后才出现在全局顺序中。(StoreLoad是x86允许的唯一类型的运行时重新排序。)

    无论如何,在这里表达你想要的另一种方式是:

    my_atomic.store(1, std::memory_order_release);        // mov
    // with no operations in between, there's nothing for the release-store to be delayed past
    std::atomic_thread_fence(std::memory_order_seq_cst);  // mfence
    

    使用发布Geofence不够强大(它和发布存储都可能延迟到以后的加载,这与发布Geofence不能阻止以后的加载提前发生是一样的)。然而,发布获取Geofence可以做到这一点,它可以防止以后的加载提前发生,并且本身不能与发布存储一起重新排序。

    相关内容:杰夫·普雷辛关于栅栏不同于释放操作的文章。

    但是请注意,根据C 11规则,seq-cst是特殊的:只有seq-cst操作保证具有所有线程都同意看到的单个全局/总顺序。因此,在C抽象机器上,即使在x86上,用较弱的顺序栅栏模拟它们通常也不完全相同。(在x86上,所有存储都有一个所有内核都同意的总顺序。另请参阅全局不可见加载说明:加载可以从存储缓冲区中获取它们的数据,因此我们不能说加载存储有总顺序。)