为什么这个`std :: atomic_thread_fence`工作

时间:2018-01-18 08:30:22

标签: c++ c++11 concurrency atomic memory-barriers

首先,我想列出一些与此有关的问题,如果我错了,请纠正我。

  1. x86中的MFENCE可确保完全屏障
  2. 顺序一致性阻止了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(没有围栏)
    4. 否则仍有可能重新排序。

      根据@ LWimsey的评论,我在这里犯了一个错误,如果LOADSTORE都是memory_order_seq_cst,则没有重新排序。 Alex可能表示使用非原子或非SC的情况。

    5. std::atomic_thread_fence(memory_order_seq_cst)始终会产生全屏障

      根据Alex's answer。因此,我始终可以将asm volatile("mfence" ::: "memory")替换为std::atomic_thread_fence(memory_order_seq_cst)

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

    6. 现在我来到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()System.out.println(new File("/tmp/test 1/").exists()); 显然这里没有足够的内存屏障,或者这个声明与第3点有点类似?

3 个答案:

答案 0 :(得分:3)

所以我的主要问题是_Atomic_exchange_4(&_Guard, 0, memory_order_seq_cst);如何创建完全障碍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而言已足够。 lock指令对WC存储器中MOVNTDQA加载的命令排序不严格,而MFENCE对此命令的排序不严格。

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

使用lock add dword [esp], 0之类的指令替代mfence是一种众所周知的技术。 (并且在某些CPU上性能更好。)此MSVC代码是相同的想法,但是它代替了对栈指针所指向的任何对象的无操作,而是对虚拟变量执行了xchg < / strong>。实际位置无关紧要,但是性能最好的选择是仅由当前内核访问并且已在缓存中处于高温的缓存行。

使用所有内核将争夺访问的static共享变量是最糟糕的选择。这样的代码太糟糕了!不必与其他内核在同一缓存行中进行交互来控制此内核在其自己的L1d缓存中的操作顺序。这完全是傻瓜。 MSVC显然仍在其std::atomic_thread_fence()的实现中使用了这种可怕的代码,即使对于保证mfence可用的x86-64,也是如此。 (Godbolt with MSVC 19.14

如果您正在seq_cst 商店,则选择mov + mfence(gcc这样做)或在商店用单个xchg来设置屏障(clang和MSVC可以这样做,因此代码生成很好,没有共享的虚拟变量)。


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

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

C ++保证使用完全不同的模型进行订购,在该模型中,从发布存储中获取看到值的负载与之“同步”,并保证C ++源代码中的后续操作在发布存储之前可以查看代码中的所有存储。

它还保证即使在不同对象之间,所有所有 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上,有加载获取和存储释放指令,它们实际上具有顺序释放的语义,这与x86加载/存储“仅”是常规释放不同。 (因此,AArch64 seq_cst不需要单独的屏障;微体系结构可能会延迟耗尽存储缓冲区,除非/直到执行加载获取,而仍然没有将存储释放提交给L1d缓存。)其他ISA通常需要完整的屏障指令以在seq_cst存储之后清空存储缓冲区。

当然,与seq_cst加载或存储操作不同,甚至AArch64都需要针对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将留下一个“便携式” ISO C ++程序,该程序具有数据争用UB,因此不能保证任何事情。

(尽管请注意,滚动您自己的原子应该至少使用volatile而不只是障碍,以确保编译器不会产生多个负载,即使您避免了明显的问题,即从负载中提升负载Who's afraid of a big bad optimizing compiler?)。


始终记住,实现的作用必须至少 与ISO C ++保证的一样强。往往最终变得更强。

答案 1 :(得分:2)

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

ARM上的情况大不相同,问题中的代码段对此进行了演示。

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

答案 2 :(得分:0)

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

C ++线程防护被称为std::atomic_thread_fence的原因是:它们仅与std::atomic<>个对象有关具有定义的功能

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

  

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

它确实有效,但仅针对其他std::memory_order_seq_cst操作。