为什么这个 std::atomic_thread_fence 有效

Why does this `std::atomic_thread_fence` work

首先我想列出一些我对此的理解,如有错误请指正。

  1. a MFENCE在x86中可以保证全barrier
  2. 顺序一致性防止 STORE-STORE、STORE-LOAD、LOAD-STORE 和 LOAD-LOAD 的重新排序

    这是根据Wikipedia.

  3. std::memory_order_seq_cst 不保证防止 STORE-LOAD 重新订购。

    这是根据 ,"Loads May Be Reordered with Earlier Stores to Different Locations"(对于 x86),mfence 不会总是添加。

    std::memory_order_seq_cst是否表示顺序一致性?根据第 2/3 点,这对我来说似乎不正确。 std::memory_order_seq_cst仅在

    时表示顺序一致性
    1. 至少一个明确的 MFENCE 添加到 LOADSTORE
    2. LOAD(无围栏)和LOCK XCHG
    3. LOCK XADD ( 0 ) 和 STORE(无围栏)

    否则仍有可能重新订购。

    根据@LWimsey的评论,我这里弄错了,如果LOADSTORE都是memory_order_seq_cst,则不会重新排序。 Alex 可能会指出使用非原子或非 SC 的情况。

  4. std::atomic_thread_fence(memory_order_seq_cst) 总是生成一个完整的屏障

    这是根据 Alex's answer。所以我总是可以用 std::atomic_thread_fence(memory_order_seq_cst)

    替换 asm volatile("mfence" ::: "memory")

    这对我来说很奇怪,因为memory_order_seq_cst似乎在原子函数和栅栏函数之间的用法有很大差异。

现在我在 MSVC 2015 的标准库的头文件中找到这段代码,它实现了 std::atomic_thread_fence

inline void _Atomic_thread_fence(memory_order _Order)
    {   /* force memory visibility and inhibit compiler reordering */
 #if defined(_M_ARM) || defined(_M_ARM64)
    if (_Order != memory_order_relaxed)
        {
        _Memory_barrier();
        }

 #else
    _Compiler_barrier();
    if (_Order == memory_order_seq_cst)
        {   /* force visibility */
        static _Uint4_t _Guard;
        _Atomic_exchange_4(&_Guard, 0, memory_order_seq_cst);
        _Compiler_barrier();
        }
 #endif
    }

所以我的主要问题是 _Atomic_exchange_4(&_Guard, 0, memory_order_seq_cst); 如何创建一个完整的屏障 MFENCE,或者实际上做了什么来启用类似 MFENCE 的等效机制,因为 _Compiler_barrier() 显然这里对于完整的内存屏障是不够的,或者这个语句的作用有点类似于第 3 点?

仅仅因为 C++ 栅栏被实现为生成特定的汇编级栅栏,并且通常需要生成一个,并不意味着您可以四处寻找内联 asm 并用 C++ 指令替换显式 asm 栅栏!

C++ 线程栅栏被称为 std::atomic_thread_fence 是有原因的:它们有一个仅与 std::atomic<> 对象相关的已定义函数 .

您绝对不能使用这些来对正常(非原子)内存操作进行排序。

std::memory_order_seq_cst makes no guarantee to prevent STORE-LOAD reorder.

仅针对 其他std::memory_order_seq_cst 操作。

听起来原子 STORE/LOAD 操作的 x86 实现利用了 strongly-ordered asm memory model of the x86 architecture. See also C/C++11 mappings to processors

ARM 上的情况非常不同,问题中的代码片段演示了这一点。

Herb Sutter 在 CPPCON 2014 上做了精彩的演讲:https://www.youtube.com/watch?v=c1gO9aB9nbs

So my major question is how can _Atomic_exchange_4(&_Guard, 0, memory_order_seq_cst); create a full barrier MFENCE

这将编译为具有内存目标的 xchg 指令。这是一个完整的内存屏障(耗尽存储缓冲区),与 mfence.

完全一样1

由于之前和之后的编译器障碍,编译时围绕它重新排序也被阻止了。因此,(对原子和非原子 C++ 对象的操作)在任一方向上的所有重新排序都被阻止,使其足够强大以执行 ISO C++ atomic_thread_fence(mo_seq_cst) 承诺的所有事情。


对于比seq_cst弱的命令,只需要一个编译器屏障。 x86 的硬件内存排序模型是程序顺序 + 具有存储转发功能的存储缓冲区。这对于 acq_rel 来说已经足够强大了,而无需编译器发出任何特殊的 asm 指令,只是阻止编译时重新排序。 https://preshing.com/20120930/weak-vs-strong-memory-models/


脚注 1:完全符合 std::atomic 的目的。从 WC 内存中弱排序的 MOVNTDQA 加载可能不像 MFENCE 那样严格按照 locked 指令排序。

  • Which is a better write barrier on x86: lock+addl or xchgl?
  • - equivalent for std::atomic purposes, but some minor differences that might matter for a device driver using WC memory regions. And perf differences. Notably on Skylake where mfence blocks OoO exec like lfence

x86 上的原子读取-修改-写入 (RMW) 操作只能使用 lock 前缀,或者 xchg with memory 即使在机器代码中没有锁定前缀也是如此。带锁前缀的指令(或带内存的 xchg)始终是完整的内存屏障。

使用 lock add dword [esp], 0 之类的指令代替 mfence 是一种众所周知的技术。 (并且在某些 CPU 上执行得更好。)这个 MSVC 代码是相同的想法,但它不是对堆栈指针指向的任何内容进行空操作,而是对虚拟对象执行 xchg变量。它实际上在哪里并不重要,但是只有当前核心访问过并且在缓存中已经很热的缓存行是性能的最佳选择。

使用一个static共享变量,所有内核都将争用访问权限是最糟糕的选择;这段代码太糟糕了! 与其他内核相同的高速缓存行交互没有必要控制该内核在其自己的 L1d 高速缓存上的操作顺序。这完全是疯了。 MSVC 显然仍然在其 std::atomic_thread_fence() 的实现中使用这个可怕的代码,即使对于保证 mfence 可用的 x86-64 也是如此。 (Godbolt with MSVC 19.14)

如果你正在做一个 seq_cst store,你的选择是 mov+mfence (gcc 做这个)或者做使用单个 xchg 存储 屏障(clang 和 MSVC 这样做,所以代码生成很好,没有共享虚拟变量)。


这个问题的大部分早期部分(陈述“事实”)似乎是错误的,并且包含一些误解或被误导的事情,他们甚至没有错。

std::memory_order_seq_cst makes no guarantee to prevent STORE-LOAD reorder.

C++ 使用完全不同的模型保证顺序,其中从发布存储中获取值的获取加载与其“同步”,C++ 源代码中的后续操作保证从发布前的代码中看到所有存储商店。

它还保证了 all seq_cst 操作的总顺序,即使在不同的对象之间也是如此。 (较弱的订单允许线程在它们变得全局可见之前重新加载自己的存储,即存储转发。这就是为什么只有 seq_cst 必须耗尽存储缓冲区。它们还允许 IRIW 重新排序。

StoreLoad 重新排序等概念基于以下模型:

  • 所有内核间通信都是通过将存储提交到缓存一致的共享内存
  • 重新排序发生在一个核心内部,在它自己对缓存的访问之间。例如通过存储缓冲区延迟存储可见性,直到 x86 允许的稍后加载之后。 (除了核心可以通过存储转发提前看到自己的存储。)

就此模型而言,seq_cst 确实需要在 seq_cst 存储和稍后的 seq_cst 加载之间的某个点耗尽存储缓冲区.实现这一点的有效方法是在 之后 seq_cst 商店放置一个完整的屏障。 (而不是在每次 seq_cst 加载之前。便宜的加载比便宜的商店更重要。)

在像 AArch64 这样的 ISA 上,有加载获取和存储释放指令,它们实际上具有顺序释放语义,这与 x86 loads/stores 不同,后者是“仅”常规释放。 (因此 AArch64 seq_cst 不需要单独的屏障;微体系结构可能会延迟耗尽存储缓冲区,除非/直到执行加载获取,同时仍有未提交给 L1d 缓存的存储释放。)其他 ISA 通常在 seq_cst store.

之后需要一个完整的屏障指令来耗尽存储缓冲区

当然,与 seq_cst 加载或存储 操作seq_cst fence 不同,即使 AArch64 也需要完整的屏障指令.


std::atomic_thread_fence(memory_order_seq_cst) always generates a full-barrier

实际上是的。

So I can always replace asm volatile("mfence" ::: "memory") with std::atomic_thread_fence(memory_order_seq_cst)

在实践中是的,但理论上,实现可能允许围绕 std::atomic_thread_fence 对非原子操作进行一些重新排序,并且仍然符合标准。 总是是一个很强的词。

ISO C++ 仅在涉及 std::atomic 加载或存储操作时提供任何保证。 GNU C++ 将允许您将自己的原子操作从 asm("" ::: "memory") 编译器障碍(acq_rel)和 asm("mfence" ::: "memory") 完全障碍中推出。将其转换为 ISO C++ signal_fence 和 thread_fence 将留下具有数据争用 UB 的“可移植”ISO C++ 程序,因此无法保证任何事情。

(尽管请注意滚动你自己的原子应该使用 at least volatile, not just barriers, to make sure the compiler doesn't invent multiple loads, even if you avoid the obvious problem of having loads hoisted out of a loop. Who's afraid of a big bad optimizing compiler?)。


永远记住,实现必须至少与 ISO C++ 保证的一样强大。这通常最终会变得更强大。