英特尔 x86 是否需要内存排序:消耗、acq_rel 和 seq_cst?

Are memory orderings: consume, acq_rel and seq_cst ever needed on Intel x86?

C++11 指定了六种内存顺序:

typedef enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
} memory_order;

https://en.cppreference.com/w/cpp/atomic/memory_order

默认值为 seq_cst。

可以通过放宽操作的内存排序来提高性能。但是,这取决于架构提供的保护。比如Intel x86是强内存模型,保证各种loads/store组合不会重序。

因此,relaxedacquirerelease 似乎是在 x86 上寻求额外性能时唯一需要的顺序。

这是正确的吗?如果不是,是否需要在 x86 上使用 consumeacq_relseq_cst

如果您关心可移植性能,理想情况下,您应该在编写 C++ 源代码时对每个操作都使用最少的必要顺序。在 x86 上唯一真正“额外”花费的是mo_seq_cst 对于纯存储,因此即使对于 x86 也要避免这种情况。

(relaxed ops 还可以允许对周围的非原子操作进行更多的编译时优化,例如 CSE 和死存储消除,因为宽松的 ops 避免了编译器障碍。如果你不需要任何order wrt.周围的代码,告诉编译器这个事实,以便它可以优化。)

请记住,如果您只有 x86 硬件,尤其是只有 acquirerelease 的原子 RMW,您将无法完全测试较弱的订单,因此在实践中,离开您的 RMW 更安全seq_cst 如果你正在做的事情已经很复杂并且很难判断正确性。

x86 asm 自然具有 acquire 加载、release 存储和 seq_cst RMW 操作。 编译时重新排序可以使用较弱的源代码中的命令,但在编译器做出选择后,这些命令被“固定”到 x86 asm 中。 (而更强的商店订单需要在 mov 之后或使用 xchg 后进行隔离。seq_cst 负载实际上没有任何额外成本,但将它们描述为获取更准确,因为较早的商店可以重新排序过去,所有被获取意味着他们不能相互重新排序。)


非常 很少有需要 seq_cst 的用例(在稍后加载发生之前耗尽存储缓冲区).几乎总是像 acquire 或 release 这样较弱的命令也是安全的。

有像https://preshing.com/20120515/memory-reordering-caught-in-the-act/, but even implementing locking generally only requires acquire and release ordering. (Of course taking a lock does require an atomic RMW, so on x86 that might as well be seq_cst.) One practical use-case I came up with was to 这样的人为案例。避免原子 RMW,并通过重新检查最近存储的值来检测一个线程何时踩到另一个线程。您必须等到您的商店全局可见,然后才能安全地重新加载它们进行检查。

As such relaxed, acquire and release seem to be the only orderings required on x86.

从一个 POV 来看,在 C++ 源代码中,您 不需要 任何弱于 seq_cst 的排序(性能除外);这就是为什么它是所有 std::atomic 函数的默认值。请记住,您正在编写 C++,而不是 x86 asm。

或者,如果您想描述 x86 asm 的全部功能,那么它是 acq 用于加载,rel 用于纯存储,seq_cst 用于原子 RMW。 (lock 前缀是一个完整的屏障;fetch_add(1, relaxed) 编译为与 seq_cst 相同的 asm)。 x86 asm 无法轻松加载或存储1.

在 C++ 中使用 relaxed 的唯一好处(为 x86 编译时)是允许通过 reordering at compile time 对周围的非原子操作进行更多优化,例如允许优化,如存储合并和死存储消除。永远记住你不是在写 x86 asm; C++ 内存模型适用于编译时排序/优化决策。

acq_relseq_cst 对于 ISO C++ 中的原子 RMW 操作几乎相同, 我认为在为像 x86 和 ARMv8 这样的多副本原子的 ISA 编译时没有区别。 (没有 IRIW 重新排序,例如 POWER 可以在存储提交到 L1d 之前通过 SMT 线程之间的存储转发来完成)。 How do memory_order_seq_cst and memory_order_acq_rel differ?

对于屏障,atomic_thread_fence(mo_acq_rel) 在 x86 上编译为零指令,而 fence(seq_cst) 编译为 mfence 或更快的等价物(例如,一些虚拟 locked 指令栈内存)。 When is a memory_order_seq_cst fence useful?

如果你只为 x86 编译,你可以说 acq_relconsume 真的没用。 consume 旨在公开大多数弱排序 ISA 所做的依赖排序(尤其是 DEC Alpha)。但不幸的是,它的设计方式编译器无法安全地实现,因此他们目前只是放弃并促进它获取,这对一些弱排序的 ISA 造成了障碍。但是在 x86 上,acquire 是“免费的”,所以没问题。

如果您确实需要高效消费,例如对于 RCU,你唯一真正的选择是使用 relaxed 并且不要给编译器足够的信息来优化它所产生的 asm 的数据依赖性。 C++11: the difference between memory_order_relaxed and memory_order_consume.


脚注 1:我没有将 movnt 算作一个宽松的原子存储,因为通常的 C++ -> asm mapping 用于发布操作仅使用 mov 商店,而不是 sfence,因此不会订购 NT 商店。即 std::atomic 如果您一直在 _mm_stream_ps() 商店闲逛,则由您使用 _mm_sfence()

PS:整个答案都是假设正常的 WB(回写)可缓存内存区域。如果你只是在主流 OS 下正常使用 C++,你所有的内存分配都将是 WB,而不是弱序 WC 或强序不可缓存的 UC 或其他任何东西。事实上,即使您想要 页面的 WC 映射,大多数 OSes 也没有 API 对应。 std::atomic 发布存储在 WC 内存上会被破坏,像 NT 存储一样弱排序。