所以我的主要问题是如何_Atomic_exchange_4(&_Guard, 0, memory_order_seq_cst);
创建一个完整的屏障 MFENCE
This compiles to an xchg
instruction with a memory destination. This is a full memory barrier (draining the store buffer) exactly1 like 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/ https://preshing.com/20120930/weak-vs-strong-memory-models/
脚注1:完全足以满足 std::atomic 的目的。来自 WC 内存的弱有序 MOVNTDQA 加载可能不会严格排序lock
MFENCE 的编辑说明。
- x86 上哪个写屏障更好:lock+addl 或 xchgl? https://stackoverflow.com/questions/4232660/which-is-a-better-write-barrier-on-x86-lockaddl-or-xchgl/52910647#52910647
-
lock xchg 与 mfence 具有相同的行为吗? https://stackoverflow.com/questions/40409297/does-lock-xchg-have-the-same-behavior-as-mfence- 与 std::atomic 目的相同,但对于使用 WC 内存区域的设备驱动程序可能存在一些细微的差异。以及性能差异。尤其在 Skylake 上mfence阻止 OoO exec 类似lfence https://stackoverflow.com/questions/50494658/are-loads-and-stores-the-only-instructions-that-gets-reordered
- 为什么 LOCK 在 x86 上是完全屏障? https://stackoverflow.com/questions/60332591/why-is-lock-a-full-barrier-on-x86
x86 上的原子读-修改-写 (RMW) 操作只能通过lock
前缀,或xchg有记忆 https://www.felixcloutier.com/x86/xchg即使机器代码中没有锁定前缀,情况也是如此。带锁前缀的指令(或带有 mem 的 xchg)始终是完整的内存屏障。
使用类似的指令lock add dword [esp], 0
作为替代品mfence
是一项众所周知的技术。 (并且在某些 CPU 上性能更好。)这个 MSVC 代码是相同的想法,但它不是对堆栈指针指向的任何内容执行无操作,而是执行xchg
在虚拟变量上。实际上它在哪里并不重要,但是仅由当前核心访问并且在缓存中已经很热的缓存线是性能的最佳选择。
Using a static
所有核心都将争夺访问权限的共享变量是最糟糕的选择;这段代码太糟糕了!无需与其他核心相同的高速缓存行交互来控制该核心对其自己的 L1d 高速缓存的操作顺序。这完全是疯了。 MSVC 显然仍然在其实现中使用这个可怕的代码std::atomic_thread_fence()
,即使对于 x86-64,其中mfence
保证可用。 (Godbolt 与 MSVC 19.14 https://godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAM1QDsCBlZAQwBtMQBGAFlICsupVs1qhkAUgBMAISnTSAZ0ztkBPHUqZa6AMKpWAVwC2tLgFZSW9ABk8tTADljAI0zEQADlIAHVAsLqtHqGJuY%2BfgF0tvZORq7uXkoqanQMBMzEBMHGppwWSZiqgWkZBNGOLm6eiumZ2aF5NaXlsfGeAJSKqAbEyBwA5FIAzHbIhlgA1OJDOsxERngSQ9jiAAwAgmvrAG6oeOgTzhnEeG4Q7VMA7LIbE3cTCgToICBzqAvIAPoECMSYzOhPlQtH0II9niAjJgjCQAJ6fEhYYifJQAR0%2ByEe7WmN02lwAIlstrt9g8iH8UcgwU8Xm8PtMdHYCNNsFNJAA2bYZC7ia5be4TLnEKZDfETTiSIY4okE/qdVggfpmfqkUz9VYq1CKnRyOQPbq9TBsoacFUERUa9qdBD/JGUToAaxAQ24ADoAJwednu7hmTjey4SrwK/rcFVqjWkLX9FUKECrUjm9Vy0hwWBINBGbx4dhkCgQTPZ3MoVifewAd0%2B7N4VBzBDccYgzgtKucdgysMVptImah9AA8rRWJ3k6QsEYRMB2C2x3g/kVtpg46PMAAPQoGetdlVM5Qz1h4ZzEDt6LDbxMnIzbzo0ehMNgcHj8QTCUQoXUyISHuOQTqobwpLQy4ALT9pIsbKIUgGaNo9S5JY2gtJU7icOE/iAXBgi%2BOhgRIXEVSoQURSpLUWT6DkghEYBJSZHhbSEaRmEMc0dgVPhKGdAoBp9Fw8qKsqqoztGq7VhMRgKNsyCCpw7qujwEwQAA6gAkg42AXBAuCECQxqoRMehZjmbi6RcOoyHIZotp0EAZu8RZuOQlCFkZ7iLMgMmobWrD1sQjbNqOba0B2569loBCDsOM7jpO06jvg85qIuy6RmuG5bv03a7iGkYHkeJ4YAM3YEJe16kLejAsNOT4CEMQiTu%2B5mfrlP7nFGAGBCB/ZDBBySBDBujkQ0CE2KxrQEWhkRBIN8HYZNdHjVRxSMdNlGQcRtA0WUo3IZRy0hPBjwsTEO2cJx3GPnxSrhkJiqrl6wGiW54qyasckKVp5K6aQ%2Bl2S5xpDKZH7SJZyZWqQTpDJIrpmKs7JmJIlzcHkPBDOyQiKmGgmjtGsbxomVmpjAiAoL9uaOQWpNVMA7q1V5Pl%2BTOgXBRlKqhQOQ4jpG0WiLFXNzlBeBJTOqXIJuhU7vQe6jrlx7ELCp7ixeeBXizN50BVD5cLwAiSHVb4SI18jNfAf7tXQnXdYoa3QRAVhMcN80oRNGErahs2AY7q29SRpT24tPu0dt7G7b7rtNIHx3B6dXQ9Dx0chgJEaard92icAyBSbJQzvfgn3DHpBn2cKwySIDhsg5a1q2lUrUQ1DMNwwjSN%2BtwqPo6G13Y4quMJkmlft%2BBWORjj%2BOg50i6%2BX13BAA)
如果你正在做 seq_cststore,你的选择是mov
+mfence
(海湾合作委员会这样做)或做商店and障碍与单一xchg
(clang 和 MSVC 这样做,所以代码生成很好,没有共享虚拟变量)。
这个问题的大部分早期部分(陈述“事实”)似乎都是错误的,并且包含一些误解或被误导的事情,甚至没有错。
std::memory_order_seq_cst
不保证防止 STORE-LOAD 重新排序。
C++ 使用完全不同的模型来保证顺序,其中获取加载从发布存储中看到的值与其“同步”,并且 C++ 源代码中的后续操作保证可以看到发布存储之前代码中的所有存储。
它还保证总订单为allseq_cst 操作甚至可以跨不同的对象。 (较弱的顺序允许线程在其自己的存储变得全局可见之前重新加载,即存储转发。这就是为什么只有 seq_cst 必须耗尽存储缓冲区。它们还允许 IRIW 重新排序。其他线程是否总是以相同的顺序看到对不同线程中不同位置的两个原子写入? https://stackoverflow.com/questions/27807118/will-two-atomic-writes-to-different-locations-in-different-threads-always-be-see/50679223#50679223)
StoreLoad 重新排序等概念基于以下模型:
- 所有核心间通信都是通过将存储提交到缓存一致的共享内存
- 重新排序发生在一个核心内部对缓存的访问之间。例如通过存储缓冲区延迟存储可见性,直到稍后加载(如 x86 允许)。 (除非核心可以通过商店转发尽早看到自己的商店。)
就该模型而言,seq_cst 确实需要在 seq_cst 存储和稍后的 seq_cst 加载之间的某个时刻耗尽存储缓冲区。实现这一目标的有效方法是设置全面的屏障afterseq_cst 存储。 (而不是在每次 seq_cst 加载之前。便宜的加载比便宜的存储更重要。)
在像 AArch64 这样的 ISA 上,有加载获取和存储释放指令,它们实际上具有顺序释放语义,这与 x86 加载/存储“仅”常规释放不同。 (因此 AArch64 seq_cst 不需要单独的屏障;微体系结构可以延迟耗尽存储缓冲区,除非/直到执行加载获取,同时仍然有存储释放未提交到 L1d 缓存。)其他 ISA 通常需要完整的屏障在 seq_cst 存储之后耗尽存储缓冲区的指令。
当然,即使是 AArch64 也需要完整的屏障指令seq_cst
fence,不同于seq_cst
加载或存储手术.
std::atomic_thread_fence(memory_order_seq_cst)
总是产生一个完整的屏障
实际上是的。
所以我可以随时更换asm volatile("mfence" ::: "memory")
with std::atomic_thread_fence(memory_order_seq_cst)
实际上是的,但理论上,实现可能允许对非原子操作进行一些重新排序std::atomic_thread_fence
并且仍然符合标准。Always这是一个非常强烈的词。
ISO C++ 仅在以下情况下保证任何内容std::atomic
涉及加载或存储操作。 GNU C++ 可以让你推出自己的原子操作asm("" ::: "memory")
编译器障碍 (acq_rel) 和asm("mfence" ::: "memory")
全面壁垒。将其转换为 ISO C++ signal_fence 和 thread_fence 将留下一个具有数据争用 UB 的“可移植”ISO C++ 程序,因此无法保证任何内容。
(尽管请注意,滚动你自己的原子应该使用至少volatile https://stackoverflow.com/questions/4557979/when-to-use-volatile-with-multi-threading/58535118#58535118,而不仅仅是障碍,以确保编译器不会发明多个加载,即使您避免了将加载提升到循环之外的明显问题。谁害怕一个糟糕的优化编译器? https://lwn.net/Articles/793253/).
永远记住,实现所做的事情必须是at least与 ISO C++ 所保证的一样强大。这往往最终会变得更强大。