cal*_*vin 4 c++ x86 memory-barriers stdatomic
首先我想谈一下我对此的一些理解,如有错误请指正。
MFENCE
在x86中可以保证全屏障顺序一致性可防止 STORE-STORE、STORE-LOAD、LOAD-STORE 和 LOAD-LOAD 重新排序
这是根据维基百科的说法。
std::memory_order_seq_cst
不保证防止 STORE-LOAD 重新排序。
这是根据Alex 的回答,“负载可能会通过早期存储重新排序到不同位置”(对于 x86),并且 mfence 不会总是被添加。
a是否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的回答。所以我总是可以替换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 点有点相似?
所以我的主要问题是如何
_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 加载可能不像lock
MFENCE 那样由 ed 指令严格排序。
mfence
会阻止 OoO exec,例如lfence
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 重新排序等概念基于以下模型:
就该模型而言, 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 次 |
最近记录: |