首先,我想列出我对此的一些理解,如果我错了,请指正。
MFENCE
可确保完全隔离这是根据Wikipedia。
std::memory_order_seq_cst
不保证阻止STORE-LOAD重新排序。这是根据Alex's answer进行的,“对于较早的商店,负载可能会重新排序到不同的位置”(对于x86),并且不会始终添加mfence。
std::memory_order_seq_cst
是否指示顺序一致性?根据第2/3点,对我来说似乎不正确。 std::memory_order_seq_cst
仅在以下情况下指示顺序一致性MFENCE
添加到LOAD
或STORE
否则仍可能有重新订购。
根据@LWimsey的评论,我在这里犯了一个错误,如果
LOAD
和STORE
均为memory_order_seq_cst
,则无需重新排序。 Alex可能指出了使用非原子或非SC的情况。 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
似乎在原子函数和篱笆函数之间的用法有很大不同。 现在,我进入实现
std::atomic_thread_fence
的MSVC 2015标准库的头文件中的此代码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点? 最佳答案
这将编译为带有存储目标的xchg
指令。就像mfence
一样,这是一个完整的内存屏障(耗尽了存储缓冲区)。
在此之前和之后使用编译器障碍,也可以防止围绕它进行编译时重新排序。因此,可以防止任何方向的重新排序(对原子和非原子C++对象的操作),从而使其足以执行ISO C++ atomic_thread_fence(mo_seq_cst)
promise 的所有操作。
对于比seq_cst弱的顺序,只需要一个编译器屏障。 x86的硬件内存排序模型是程序顺序+具有存储转发功能的存储缓冲区。这对于acq_rel
足够强大,而编译器不会发出任何特殊的asm指令,而只是阻止编译时重新排序。 https://preshing.com/20120930/weak-vs-strong-memory-models/
脚注1 :足够用于std::atomic的目的。通过lock
指令对WC存储器中MOVNTDQA加载的排序不严格,而对MFENCE的排序不严格。
mfence
blocks OoO exec like lfence
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这样做,因此代码生成很好,没有共享的虚拟变量)。这个问题的大部分早期部分(陈述“事实”)似乎是错误的,并且包含一些误解或误导性内容,甚至没有错。
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重新排序之类的概念基于以下模型:
就此模型而言,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
的非原子操作进行一些重新排序,并且仍然符合标准。永远是一个很强的词。仅当涉及
std::atomic
加载或存储操作时,ISO C++才能保证任何内容。 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++保证的作用一样强。这通常最终变得更强大。