【问题标题】:Why does this `std::atomic_thread_fence` work为什么这个`std::atomic_thread_fence`有效
【发布时间】:2018-06-27 06:17:21
【问题描述】:

首先我想列出我对此的一些理解,如果我错了,请纠正我。

  1. x86 中的 MFENCE 可以确保完全屏障
  2. Sequential-Consistency 防止 STORE-STORE、STORE-LOAD、LOAD-STORE 和 LOAD-LOAD 重新排序

    这是根据Wikipedia.

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

    这是根据Alex's answer,“加载可能会与早期存储重新排序到不同的位置”(对于 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 点?

【问题讨论】:

  • 关于您的第 3 点“std::memory_order_seq_cst 不保证防止 STORE-LOAD 重新排序”.. 它确实保证了这一点,但前提是这两个操作都被标记为这样.
  • @LWimsey 你的意思是如果我使用atomic_store(memory_order_seq_cst )atomic_load(memory_order_seq_cst ),就不会有重新排序。但是,如果我使用atomic_store(memory_order_release)atomic_load(memory_order_acquire),那么我应该在其中任何一个上添加MFENCE,以避免STORE-LOAD 重新排序?
  • 是的,如果您在storeload 上都使用seq_cst,则所有线程将按此顺序观察这两个操作。在两者之间插入atomic_thread_fence(seq_cst) 也是如此(您可以/不应该真正插入MFENCE,将其留给编译器)。
  • @calvin 这实际上取决于您是否谈论相同的内存位置。如果您执行x.store(1, memory_order_release); x.load(memory_order_acquire);,则不需要栅栏(尽管这样的构造很值得怀疑,因此您可能意味着它们位于不同的内存位置)。
  • @LWimsey 1) 所有线程?哪些线程? 2)什么和什么之间的围栏?其他线程必须使用栅栏吗?

标签: c++ x86 memory-barriers stdatomic


【解决方案1】:

所以我的主要问题是_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 来说已经足够了。 locked 指令从 WC 内存中加载的弱排序 MOVNTDQA 可能不像 MFENCE 那样严格排序。

x86 上的原子读-修改-写 (RMW) 操作只能使用lock 前缀或xchg with memory,即使机器代码中没有锁定前缀也是如此。带锁前缀的指令(或带有 mem 的 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 存储,您的选择是 mov+mfence(gcc 执行此操作)或执行存储 单个xchg(clang 和 MSVC 这样做,所以 codegen 很好,没有共享的 dummy var)。


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

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

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

它还保证了 all seq_cst 操作的总顺序,即使跨不同的对象也是如此。 (较弱的订单允许线程在全局可见之前重新加载自己的存储,即存储转发。这就是为什么只有 seq_cst 必须耗尽存储缓冲区。它们还允许 IRIW 重新排序。Will two atomic writes to different locations in different threads always be seen in the same order by other threads?

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

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

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

在像 AArch64 这样的 ISA 上,有 load-acquire 和 store-release 指令实际上具有顺序发布语义,这与 x86 加载/存储“仅”定期发布不同。 (所以 AArch64 seq_cst 不需要单独的屏障;微架构可能会延迟耗尽存储缓冲区,除非/直到加载获取执行,而仍然有一个存储释放尚未提交到 L1d 缓存。)其他 ISA 通常需要一个完整的屏障在 seq_cst 存储之后耗尽存储缓冲区的指令。

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


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

实际上是的。

所以我总是可以用std::atomic_thread_fence(memory_order_seq_cst)替换asm volatile("mfence" ::: "memory")

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

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

(尽管请注意,滚动您自己的原子应该使用at least volatile,而不仅仅是障碍,以确保编译器不会发明多个负载,即使您避免了将负载提升出循环的明显问题。@ 987654330@)。


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

【讨论】:

  • 有什么理由更喜欢 XCHG 和完全静态的存储持续时间变量(实际上没有存储在堆栈上)?我怀疑这是由于编码最小(没有LOCK 前缀,简单的地址获取)。所以在后面的 MSVC 中改成LOCK CMPXCHG 是没有用的,虽然不是很有害。
  • 我知道他们也不使用mfence,因为他们在没有mfence的CPU上运行(至少理论上曾经运行过),所以即使mfence有更好的性能,他们可能不会用它
  • @AlexGuteniev:哦,天哪,我只浏览了代码,只看到了我期望看到的内容。不是实际存在的疯狂代码 (static),这要糟糕得多,并且会在不同的核心之间产生争用,从而造成障碍。 mfence 是 x86-64 的基线,因此这将是一个选项,并且比必须将 xchg 的 reg 归零的代码更小。对于某些 CPU,您可能仍会选择虚拟 xchg 或其他锁定操作作为更有效的屏障。但是带有 stack 变量的虚拟 xchg 完全可以,如果有必要阻止它进行优化,可能会使其成为 volatile
  • @AlexGuteniev:当您使用调整选项进行编译时,GCC 使用lock inc 表示原子++,该选项调整CPU,其中inc 在寄存器上完全没问题。例如-mtune=haswellgodbolt.org/z/fWMhMB。不幸的是,即使在未锁定的情况下,它也使用内存目标inc,这会在 Haswell 上花费额外的 uop。 INC instruction vs ADD 1: Does it matter?.
  • 我提请了 Boost.Atomic 维护者的注意,今天他提出了a commit。最有趣的部分是在此链接中反对 lock or [esp], 0 的解释:shipilev.net/blog/2014/on-the-fence-with-dependencies。简而言之,在寄存器溢出的情况下可能存在错误的数据依赖关系。
【解决方案2】:

听起来原子存储/加载操作的 x86 实现利用了strongly-ordered asm memory model of the x86 architecture。另见C/C++11 mappings to processors

在 ARM 上的情况非常不同,问题中的代码 sn-p 演示了这一点。

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

【讨论】:

【解决方案3】:

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

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

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

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

它只是只针对其他std::memory_order_seq_cst操作。

【讨论】:

  • 但是交换如何与其他原子同步,特别是与另一个原子的std::memory_order_seq_cst 负载(在 MSVC 中是普通负载)?而且我看到它被替换为InterlockedCompareExhcange,所以没有交换工作?
  • "但是交换如何与其他原子同步" 具体来说:交换是如何完成的?
  • 我的意思是,这个atomic_thread_fence(seq_cst) 是通过交换实现的。 atomic<T>::load(seq_cst)) 是通过简单的加载实现的,只有编译器屏障。它们是如何同步的?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-02-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-29
  • 2020-08-21
  • 2021-07-24
相关资源
最近更新 更多