为什么具有顺序一致性的 std::原子存储使用 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和x86_64。商店具有隐式获取围栏的地方。

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

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

简单的mov是不够的;它只有发布语义,没有顺序发布。 (与 AArch64 的stlr指令不同,该指令确实执行顺序发布存储,该存储不能随着以后的ldar顺序获取加载重新排序。 这种选择显然是出于 C++11 将seq_cst作为默认内存排序的动机。 但是 AArch64 的正常存储要弱得多;放松不释放。

请参阅 Jeff Preshing关于获取/发布语义的文章,并注意常规发布存储(如mov或 xchg 以外的任何非锁定 x86 内存目标指令)允许使用以后的操作重新排序,包括获取加载(如 mov 或任何 x86 内存源操作数)。 例如,如果发布存储正在释放锁,那么以后的事情似乎发生在关键部分内是可以的。


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

https://shipilev.net/blog/2014/on-the-fence-with-dependencies 有关mfencelock 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 内存传递早期locked 指令movntdqa加载。 (与Skylake相反,在那里它被mfence而不是locked指令,这是一个问题。 但与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 上,所有存储都有一个所有内核都同意的总订单。 另请参阅全局不可见的加载说明:加载可以从存储缓冲区获取其数据,因此我们不能真正说加载 + 存储的总订单。

相关内容

  • 没有找到相关文章

最新更新