为什么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的。商店有隐含的获取围栏。
答案 0 :(得分:10)
mov
- store + mfence
和xchg
都是在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,并注意常规版本允许重新排序以后的操作。 (如果发布商店正在发布锁定,那么以后的内容似乎可以在关键部分内发生。)
mfence
和xchg
在不同的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.14,mb()
使用smp_mb()
。如果可用则使用mfence,否则使用mb()
。
lock addl $0, 0(%esp)
(商店+内存屏障)使用__smp_store_mb
(在以后的内核中不会发生变化)。
In Linux 4.15,xchg
使用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)
。
-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}}:加载可以从商店缓冲区中获取数据,因此我们无法真正说出那里的数据。装货+商店的总订单。)