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

时间:2018-03-05 09:59:37

标签: c++ assembly x86 lock-free stdatomic

为什么std::atomic's store

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&amp; x86_64的。商店有隐含的获取围栏。

1 个答案:

答案 0 :(得分:10)

mov - store + mfencexchg都是在x86上实现顺序一致性存储的有效方法。隐式lock带有内存的xchg上的前缀使其成为完整的内存屏障,就像x86上的所有原子RMW操作一样。 (不幸的是,对于其他用例,x86没有提供放松或acq_rel原子增量的方法,只有seq_cst。)

普通mov是不够的;它只有发布语义,而不是顺序发布。 (与AArch64的stlr指令不同,后者执行顺序发布存储。这个选择显然是由于C ++ 11将seq_cst作为默认的内存顺序。但AArch64的正常存储是更弱;放松不释放。)请参阅Jeff Preshing's article on acquire / release semantics,并注意常规版本允许重新排序以后的操作。 (如果发布商店正在发布锁定,那么以后的内容似乎可以在关键部分内发生。)

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

在英特尔Skylake硬件上, mfence阻止独立ALU指令的无序执行,但xchg。 (See my test asm + results in the bottom of this SO answer)。英特尔的手册并不要求它强大;只记录lfence来做到这一点。但作为一个实现细节,对Skylake周围代码的无序执行来说非常昂贵。

我还没有测试过其他CPU,这可能是a microcode fix for erratum SKL079 的结果, SKL079来自WC内存的MOVNTDQA可以通过早期 MFENCE说明。错误的存在基本上证明了SKL曾经能够在MFENCE之后执行指令。如果他们通过在微代码中使MFENCE更强大来修复它,我会不会感到惊讶,这是一种直接的工具方法,显着增加了对周围代码的影响。

我只测试了L1d缓存中缓存行热的单线程情况。 (当它在内存中冷却时,或者当它在另一个核心上处于修改状态时)。xchg必须加载先前的值,创建一个&#34; false&#34;依赖于内存中的旧值。但mfence强制CPU等待,直到先前的存储提交到L1d,这也需要缓存线到达(并处于M状态)。所以他们在这方面可能大致相同,但英特尔的mfence迫使所有事情等待,而不仅仅是加载。

AMD的优化手册建议原型seq-cst商店使用xchg 。我认为英特尔建议{g}使用mov + mfence,但英特尔编译器也在这里使用xchg

当我测试时,我在Skylake上获得xchg的吞吐量比在同一位置的单线程循环中的mov + mfence更高。有关详细信息,请参阅Agner Fog's microarch guide and instruction tables,但他并没有在锁定操作上花费太多时间。

当SSE2可用时,请参阅gcc/clang/ICC/MSVC output on the Godbolt compiler explorer了解C ++ 11 seq-cst my_atomic = 4; gcc使用mov + mfence。 (使用-m32 -mno-sse2让gcc也使用xchg。其他3个编译器都喜欢xchg默认调整,或者znver1(Ryzen)或skylake

Linux内核对xchg使用__smp_store_mb()

所以看来gcc应该使用xchg,除非他们有一些其他人不知道的基准测试结果。

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

当所有编译器可用时,它们会使用mfence作为独立屏障。这些在C ++ 11代码中很少见,但是对于真正的多线程代码实际上最有效的内容需要更多的研究,这些代码在无锁通信的线程内进行实际工作。

但是多个来源建议使用lock add作为障碍而不是mfence ,因此Linux内核最近切换到将其用于smp_mb()在x86上实现,即使SSE2可用。

请参阅https://groups.google.com/d/msg/fa.linux.kernel/hNOoIZc6I9E/pVO3hB5ABAAJ进行一些讨论,包括提及HSW / BDW的一些勘误表,其中涉及来自WC内存的movntdqa次加载,并通过了之前的lock指令。 (与Skylake相反,它是mfence而不是lock指令,这是一个问题。但与SKL不同,微码中没有修复。这可能是Linux仍然使用{{}的原因。 1}}对于驱动程序的mfence,如果有任何东西使用NT加载从视频RAM或其他东西复制回来但不能让读取发生,直到早期商店可见。)

  • In Linux 4.14mb()使用smp_mb()。如果可用则使用mfence,否则使用mb()

    lock addl $0, 0(%esp)(商店+内存屏障)使用__smp_store_mb(在以后的内核中不会发生变化)。

  • In Linux 4.15xchg使用smb_mb()lock; addl $0,-4(%esp),而非使用%rsp。 (内核即使在64位中也不使用红区,因此mb()可以帮助避免本地变量的额外延迟。

    驱动程序使用

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

  • In Linux 4.16,除了删除为现代硬件实现的x86-TSO模型更弱序的内存模型定义内容的-4之外没有任何变化。
  

x86&amp; x86_64的。商店有隐含的获取围栏

你的意思是发布,我希望如此。 #if defined(CONFIG_X86_PPRO_FENCE)不会编译,因为只写原子操作不能获取操作。另请参阅Jeff Preshing's article on acquire/release semantics

  

my_atomic.store(1, std::memory_order_acquire);

不,这只是一个编译障碍;它会阻止compile-time reordering之间的所有runtime StoreLoad reordering,但不会阻止Jeff Preshing's article on fences being different from release operations,即直到稍后才缓存存储,并且直到稍后加载后才会出现在全局顺序中。 (StoreLoad是x86允许的唯一一种运行时重新排序。)

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

asm volatile("" ::: "memory");

使用发布围栏不够强大(它和发布商店都可以延迟到以后的负载,这就像说释放围栏不会让以后的负载不能及早发生) 。然而,一个发布 - 获取栅栏可以解决这个问题,保持以后的负载不会发生,并且本身不能重新排序。

相关:Globally Invisible load instructions

但请注意,根据C ++ 11规则,seq-cst是特殊的:只保证seq-cst操作具有单个全局/总顺序,所有线程都同意这些顺序。因此,使用较弱的顺序+围栏来模拟它们可能在C ++抽象机器上通常不完全相同,即使它在x86上也是如此。 (在x86上,所有商店都有一个总订单,所有内核都同意。另请参阅{{3}}:加载可以从商店缓冲区中获取数据,因此我们无法真正说出那里的数据。装货+商店的总订单。)