为什么这个“std::atomic_thread_fence”起作用

cal*_*vin 4 c++ x86 memory-barriers stdatomic

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

  1. aMFENCE在x86中可以保证全屏障
  2. 顺序一致性可防止 STORE-STORE、STORE-LOAD、LOAD-STORE 和 LOAD-LOAD 重新排序

    这是根据维基百科的说法。

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

    这是根据Alex 的回答,“负载可能会通过早期存储重新排序到不同位置”(对于 x86),并且 mfence 不会总是被添加。

    a是否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的评论,我在这里犯了一个错误,如果 和LOAD都是STOREmemory_order_seq_cst则没有重新排序。Alex 可能指出使用非原子或非 SC 的情况。

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

    这是根据Alex的回答。所以我总是可以替换asm volatile("mfence" ::: "memory")std::atomic_thread_fence(memory_order_seq_cst)

    这对我来说很奇怪,因为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
    }
Run Code Online (Sandbox Code Playgroud)

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

Pet*_*des 5

所以我的主要问题是如何_Atomic_exchange_4(&_Guard, 0, memory_order_seq_cst);创建一个完整的屏障 MFENCE

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

在此之前和之后存在编译器障碍,也可以防止围绕它的编译时重新排序。因此,(原子和非原子 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 加载可能不像lockMFENCE 那样由 ed 指令严格排序。

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

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

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

如果您正在执行 seq_cst store,您的选项是mov+ mfence(gcc 执行此操作)或使用单个执行存储屏障xchg(clang 和 MSVC 执行此操作,因此代码生成很好,没有共享虚拟变量)。


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

std::memory_order_seq_cst不保证防止 STORE-LOAD 重新排序。

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

它还保证即使跨不同对象,所有seq_cst 操作也有一个总顺序。(较弱的顺序允许线程在全局可见之前重新加载自己的存储,即存储转发。这就是为什么只有 seq_cst 必须耗尽存储缓冲区。它们还允许 IRIW 重新排序。在 不同线程中对不同位置的两个原子写入是否总是会发生?其他线程以相同的顺序看到?

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

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

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

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

当然,与加载或存储操作不同,即使是 AArch64 也需要针对seq_cst 栅栏的完整屏障指令。seq_cst


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

实际上是的。

所以我总是可以替换asm volatile("mfence" ::: "memory")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++ 程序,因此无法保证任何内容。

(尽管请注意,滚动您自己的原子应该至少volatile使用,而不仅仅是屏障,以确保编译器不会发明多个加载,即使您避免了将加载提升到循环之外的明显问题。 谁害怕大优化编译器不好?)。


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


归档时间:

查看次数:

2212 次

最近记录:

3 年,10 月 前