如果您关心可移植性能,理想情况下,您应该为每个操作编写具有最低必要顺序的 C++ 源代码。 在 x86 上真正需要“额外”成本的唯一东西是 mo_seq_cst纯存储,所以即使对于 x86,也要避免这种情况。
(relaxed ops 还可以允许对周围的非原子操作进行更多的编译时优化,例如 CSE 和死存储消除,因为宽松的 ops 避免了编译器障碍。如果您不需要任何命令 wrt.around代码,告诉编译器这个事实,以便它可以优化。)
请记住,如果您只有 x86 硬件,尤其是只有 acquire 或 release 的原子 RMW,则无法完全测试较弱的订单,因此在实践中,如果您将 RMW 保留为 seq_cst 会更安全'正在做任何已经复杂且难以推理正确性的事情。
x86 asm 自然有acquire 加载、release 存储和seq_cst RMW 操作。 编译时重新排序可以在源代码中使用较弱的顺序,但在编译器完成它之后选择,这些被“确定”到 x86 asm 中。 (更强大的商店订单需要在mov 或使用xchg 之后设置mfence。seq_cst 加载实际上没有任何额外成本,但将它们描述为获取更准确,因为早期的商店可以重新订购它们,并且全部被获取意味着它们不能相互重新排序。)
非常很少有需要seq_cst 的用例(在以后的加载发生之前耗尽存储缓冲区)。几乎总是像获取或释放这样较弱的命令也是安全的。
有https://preshing.com/20120515/memory-reordering-caught-in-the-act/这样的人为情况,但即使实现锁定通常也只需要获取和释放顺序。 (当然,获取锁确实需要一个原子 RMW,所以在 x86 上也可能是 seq_cst。)我想出的一个实际用例是 have multiple threads set bits in an array。避免原子 RMW,并通过重新检查最近存储的值来检测一个线程何时踩到另一个线程。您必须等到您的商店在全球范围内可见,然后才能安全地重新加载它们以进行检查。
因此,relaxed、acquire 和 release 似乎是 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_rel 和 seq_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_rel 和 consume 真的没用。 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_sfence() 如果你一直在搞乱 _mm_stream_ps() 商店。
PS:整个答案都是假设正常的 WB(回写)可缓存内存区域。如果您只是在主流操作系统下正常使用 C++,那么您所有的内存分配都将是 WB,而不是弱序 WC 或强序不可缓存 UC 或其他任何东西。事实上,即使您想要页面的 WC 映射,大多数操作系统也没有用于此的 API。而std::atomic release 存储会在 WC 内存上被破坏,像 NT 存储一样弱排序。