为什么std::atomic
的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和x86_64。商店具有隐式获取围栏的地方。
mov
-store +mfence
和xchg
都是在 x86 上实现顺序一致性存储的有效方法。具有内存的xchg
上的隐式lock
前缀使其成为完整的内存屏障,就像 x86 上的所有原子 RMW 操作一样。
(x86的内存排序规则基本上使这种全屏障效应成为任何原子RMW的唯一选择:它同时是负载和存储,以全局顺序粘在一起。 原子性要求加载和存储不通过仅将存储排队到存储缓冲区中来分离,因此必须将其排出,而加载端的负载-负载排序要求它不重新排序。
简单的mov
是不够的;它只有发布语义,没有顺序发布。 (与 AArch64 的stlr
指令不同,该指令确实执行顺序发布存储,该存储不能随着以后的ldar
顺序获取加载重新排序。 这种选择显然是出于 C++11 将seq_cst作为默认内存排序的动机。 但是 AArch64 的正常存储要弱得多;放松不释放。
请参阅 Jeff Preshing关于获取/发布语义的文章,并注意常规发布存储(如mov
或 xchg 以外的任何非锁定 x86 内存目标指令)允许使用以后的操作重新排序,包括获取加载(如 mov 或任何 x86 内存源操作数)。 例如,如果发布存储正在释放锁,那么以后的事情似乎发生在关键部分内是可以的。
不同 CPU 上的mfence
和xchg
之间存在性能差异,可能在热缓存与冷缓存以及争用与非争用情况下存在性能差异。 和/或在同一线程中背靠背与单独执行许多操作的吞吐量,以及允许周围的代码与原子操作重叠执行。
https://shipilev.net/blog/2014/on-the-fence-with-dependencies 有关mfence
与lock addl $0, -8(%rsp)
vs.(%rsp)
作为一个完整的障碍(当你还没有商店要做时)。
在英特尔 Skylake 硬件上,mfence
阻止独立 ALU 指令的无序执行,但xchg
不会。 (请参阅我的测试 asm + 此 SO 答案底部的结果)。 英特尔的手册不要求它那么强大;只有lfence
记录这样做。 但作为实现细节,在 Skylake 上无序执行周围代码的成本非常高。
我还没有测试过其他CPU,这可能是对勘误表SKL079的微码修复的结果,WC内存中的SKL079 MOVNTDQA可能会通过早期的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的微拱指南和说明表,但他不会花太多时间在锁定操作上。
请参阅 Godbolt 编译器资源管理器上的 gcc/clang/ICC/MSVC 输出,了解 C++11 seq-cstmy_atomic = 4;
gcc在 SSE2 可用时使用mov
+mfence
。 (使用-m32 -mno-sse2
让 GCC 也使用xchg
)。 其他 3 个编译器都更喜欢默认调优的xchg
,或者znver1
(Ryzen)或skylake
。Linux 内核使用xchg
进行__smp_store_mb()
。
更新:最近的 GCC(如 GCC10)更改为像其他编译器一样将xchg
用于 seq-cst 存储,即使 SSE2 formfence
可用。
另一个有趣的问题是如何编译atomic_thread_fence(mo_seq_cst);
。 显而易见的选项是mfence
,但lock or dword [rsp], 0
是另一个有效选项(当 MFENCE 不可用时,gcc -m32
使用)。 堆栈的底部在 M 状态下的缓存中通常已经很热。 缺点是,如果本地存储在那里,则会引入延迟。 (如果它只是一个退货地址,退货地址预测通常非常好,所以延迟ret
读取它的能力不是什么大问题。 因此,在某些情况下lock or dword [rsp-4], 0
可能值得考虑。 (海湾合作委员会确实考虑过,但因为它让瓦尔格林德不高兴而恢复了它。 这是在人们知道即使mfence
可用时它可能比mfence
更好之前。
所有编译器当前都使用mfence
作为独立屏障(如果可用)。 这些在 C++11 代码中很少见,但需要对真正的多线程代码真正最有效的研究,这些代码在无锁通信的线程中进行了真正的工作。
但是多源建议使用堆栈的lock add
作为屏障而不是mfence
,因此Linux内核最近切换到将其用于x86上的smp_mb()
实现,即使SSE2可用。
有关一些讨论,请参阅 https://groups.google.com/d/msg/fa.linux.kernel/hNOoIZc6I9E/pVO3hB5ABAAJ,包括提及 HSW/BDW 的一些勘误表,该勘误表涉及从 WC 内存传递早期lock
ed 指令movntdqa
加载。 (与Skylake相反,在那里它被mfence
而不是lock
ed指令,这是一个问题。 但与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
可能有助于避免本地 var 的额外延迟)。驱动程序使用
mb()
来订购对 MMIO 区域的访问,但在为单处理器系统编译时smp_mb()
变为无操作。 更改mb()
风险更大,因为它更难测试(影响驱动程序),并且 CPU 具有与锁定与 mfence 相关的勘误表。 但无论如何,mb()
使用mfence(如果可用),否则lock addl $0, -4(%esp)
. 唯一的变化是-4
.在 Linux 4.16 中,除了删除
#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
使用发布围栏不够强大(它和发布存储都可能延迟到稍后的加载之后,这与说释放围栏不能阻止后期加载提前发生是一回事)。 但是,发布获取围栏可以解决问题,防止以后的加载提前发生,并且本身无法与发布存储区重新排序。
相关:Jeff Preshing关于围栏不同于发布操作的文章。
但请注意,根据 C++11 规则,seq-cst 是特殊的:只有 seq-cst 操作才能保证具有所有线程都同意看到的单个全局/总顺序。 因此,使用较弱的顺序 + 栅栏模拟它们在C++抽象机器上通常可能并不完全等效,即使在 x86 上也是如此。 (在 x86 上,所有存储都有一个所有内核都同意的总订单。 另请参阅全局不可见的加载说明:加载可以从存储缓冲区获取其数据,因此我们不能真正说加载 + 存储的总订单。