Lea*_*ros 4 c++ x86 assembly lock-free stdatomic
为什么是std::atomic的 store:
std::atomic<int> my_atomic;
my_atomic.store(1, std::memory_order_seq_cst);
Run Code Online (Sandbox Code Playgroud)
在xchg请求具有顺序一致性的商店时执行?
从技术上讲,具有读/写内存屏障的普通商店不应该足够吗?相当于:
_ReadWriteBarrier(); // Or `asm volatile("" ::: "memory");` for gcc/clang
my_atomic.store(1, std::memory_order_acquire);
Run Code Online (Sandbox Code Playgroud)
我明确地谈论x86和x86_64.商店有隐含的获取围栏.
Pet*_*des 11
mov-store + mfence和xchg是在x86上实现顺序一致性存储的有效方法.带内存 的隐式lock前缀xchg使其成为完整的内存屏障,就像x86上的所有原子RMW操作一样.(不幸的是,对于其他用例,x86不提供放松或acq_rel原子增量的方法,只提供seq_cst.)
平原mov是不够的; 它只有发布语义,而不是顺序发布.(与AArch64的stlr指令不同,后者执行顺序发布存储.这个选择显然是由于C++ 11将seq_cst作为默认的内存排序.但AArch64的正常存储要弱得多;放松不发布.)参见Jeff Preshing的文章获取/释放语义,并注意常规版本允许在以后的操作中重新排序.(如果发布商店正在释放锁定,那么以后的内容似乎可以在关键部分内发生.)
不同CPU 之间mfence和之间存在性能差异xchg,可能在热缓存和冷缓存以及竞争与非竞争情况下.和/或许多操作的吞吐量在同一个线程中背靠背而不是单独一个,并且允许周围的代码与原子操作重叠执行.
在英特尔Skylake硬件上,mfence阻止独立ALU指令的无序执行,但xchg没有.(参见我的测试asm +结果在这个SO答案的底部).英特尔的手册并不要求它那么强大; 只有lfence记录这样做.但作为一个实现细节,对于Skylake上的周围代码的无序执行来说,这是非常昂贵的.
我还没有测试过其他的CPU,这可能是因为错误修复了错误SKL079,SKL079 MOVNTDQA来自WC内存可能会通过早期的MFENCE说明.错误的存在基本上证明了SKL曾经能够在MFENCE之后执行指令.如果他们通过使MFENCE在微代码中更强大来修复它,我会不会感到惊讶,这是一种直接的工具方法,显着增加了对周围代码的影响.
我只测试了在L1d缓存中缓存行很热的单线程情况.(当它在内存中冷却时,或者当它在另一个核心上处于修改状态时). xchg不得不加载先前的值,从而对内存中的旧值创建"错误"依赖.但mfence强制CPU等待,直到先前的存储提交到L1d,这也需要缓存线到达(并处于M状态).所以他们在这方面可能大致相同,但英特尔mfence迫使一切都要等待,而不仅仅是负载.
AMD的优化手册推荐xchg用于原子seq-cst存储.我认为英特尔推荐mov+ mfence,gcc使用,但英特尔的编译器也在xchg这里使用.
当我测试时,我在Skylake上的吞吐量xchg比在相同位置的单线程循环中的mov+ 更好mfence.有关详细信息,请参阅Agner Fog的微指南和指令表,但他并没有花太多时间在锁定操作上.
有关C++ 11 seq-cst,请参阅Godbolt编译器资源管理器上的gcc/clang/ICC/MSVC输出.my_atomic = 4;当SSE2可用时, 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另一个有效选项(gcc -m32当MFENCE不可用时使用).堆栈的底部通常已经处于M状态的缓存中.缺点是如果在那里存储本地,则会引入延迟.(如果它只是一个返回地址,返回地址预测通常非常好,因此延迟ret读取它的能力不是很大的问题.)因此lock or dword [rsp-4], 0在某些情况下值得考虑.(gcc确实考虑过它,但是还原它,因为它使得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的一些勘误表,内容涉及movntdqa来自WC内存的传输,并通过了之前的locked指令.(SKYLAKE微架构,它在哪里的对面mfence,而不是lock说是一个问题,但是不像SKL,有在微没有修复版的说明.这也许可以解释为什么Linux的仍然使用mfence它的mb()驱动程序,如果任何事情的任何使用NT负载从复制回视频RAM或其他东西,但不能让读取发生,直到早期商店可见.)
在Linux 4.14中,smp_mb()使用mb().如果可用,则使用mfence,否则使用lock addl $0, 0(%esp).
__smp_store_mb(存储+内存屏障)使用xchg(并且在以后的内核中不会更改).
在Linux 4.15中,smb_mb()使用lock; addl $0,-4(%esp)或%rsp代替使用mb().(内核即使在64位中也不使用红区,因此-4可能有助于避免本地变量的额外延迟).
mb()驱动程序使用它来命令访问MMIO区域,但smp_mb()在为单处理器系统编译时变为无操作.更改mb()风险更大,因为它更难测试(影响驱动程序),并且CPU具有与锁定和mfence相关的勘误.但无论如何,mb()如果可用的话,使用mfence,否则lock addl $0, -4(%esp).唯一的变化是-4.
#if defined(CONFIG_X86_PPRO_FENCE)比现代硬件实现的x86-TSO模型更弱排序的内存模型定义的内容之外,没有任何变化.x86和x86_64.商店有隐含的获取围栏
你的意思是释放,我希望. my_atomic.store(1, std::memory_order_acquire);不会编译,因为只写原子操作不能获取操作.另请参阅Jeff Preshing关于获取/发布语义的文章.
要么
asm volatile("" ::: "memory");
不,这只是一个编译器障碍; 它阻止所有编译时重新排序,但不会阻止运行时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
Run Code Online (Sandbox Code Playgroud)
使用发布围栏不够强大(它和发布商店都可以延迟超过以后的负载,这与说释放围栏不会让以后的负载提前发生的情况相同).然而,一个发布 - 获取围栏可以解决这个问题,保持以后的负载不会发生,而不是自己能够与发布存储重新排序.
相关:Jeff Preshing关于栅栏的文章与发布操作不同.
但请注意,根据C++ 11规则,seq-cst是特殊的:只保证seq-cst操作具有单个全局/总顺序,所有线程都同意这些顺序.因此,使用较弱的顺序+围栏来模拟它们可能在C++抽象机器上通常不完全相同,即使它在x86上也是如此.(在x86上,所有商店都有一个总订单,所有内核都同意.另请参见全局隐形加载指令:加载可以从商店缓冲区获取数据,因此我们无法确定加载+存储的总订单.)