没错,LFENCE 和 SFENCE 在普通代码中没有用,因为 x86 对常规存储的获取/释放语义使它们变得多余,除非您使用其他特殊指令或内存类型。
对于正常的无锁代码而言,唯一重要的栅栏是来自 locked 指令或慢速 MFENCE 的完整栅栏(包括 StoreLoad)。对于顺序一致性存储,首选xchg,而不是mov+mfence。 Are loads and stores the only instructions that gets reordered? 因为它更快。
Does `xchg` encompass `mfence` assuming no non-temporal instructions?(是的,即使 带有 NT 指令,只要没有 WC 内存。)
Jeff Preshing 的Memory Reordering Caught in the Act 文章是对 Bartosz 的帖子谈到的同一案例的更易于阅读的描述,您需要像 MFENCE 这样的 StoreLoad 屏障。只有 MFENCE 可以;你不能用 SFENCE + LFENCE 构造 MFENCE。 (Why is (or isn't?) SFENCE + LFENCE equivalent to MFENCE?)
如果您在阅读您发布的链接后有任何疑问,请阅读 Jeff Preshing 的其他博客文章。他们让我对这个主题有了很好的理解。 :) 虽然我认为我在 Doug Lea 的页面中发现了关于 SFENCE/LFENCE 通常是无操作的花絮。 Jeff 的帖子没有考虑 NT 加载/存储。
相关:When should I use _mm_sfence _mm_lfence and _mm_mfence(我的答案和@BeeOnRope 的答案很好。我写这个答案的时间比那个答案早得多,所以这个答案的一部分显示了我几年前的经验不足。我在那里的答案考虑了 C++ 内在函数和C++ 编译时内存顺序,这和 x86 asm 运行时内存顺序完全不一样。但你还是不要_mm_lfence()。)
SFENCE 仅在使用 movnt(非临时)流存储,或使用类型设置为正常回写以外的类型的内存区域时才相关。或者clflushopt,这有点像一个弱订单商店。 NT 存储绕过缓存以及弱排序。 x86's normal memory model is strongly ordered,除了 NT 存储、WC(写入组合)内存和 ERMSB 字符串操作(见下文)。
LFENCE 仅对具有弱排序负载的内存排序有用,这非常很少。 (或者对于在 NT 存储之前常规加载的 LoadStore 排序?)
来自 WB 内存的 NT 负载 (movntdqa) 为 still strongly ordered,即使在假设的未来 CPU 上也不会忽略 NT 提示;在 x86 上进行弱排序加载的唯一方法是从弱排序内存 (WC) 中读取,然后我认为只能使用 movntdqa。这在“正常”程序中不会偶然发生,因此您只需在映射视频 RAM 或其他东西时担心这一点。
(lfence 的主要用例根本不是内存排序,而是用于序列化指令执行,例如用于 Spectre 缓解或使用 RDTSC。请参阅Is LFENCE serializing on AMD processors? 和“链接问题”侧边栏。)
C++ 中的内存排序,以及它如何映射到 x86 asm
几周前我对此感到好奇,并针对最近的一个问题发布了一个相当详细的答案:
Atomic operations, std::atomic<> and ordering of writes。我提供了很多关于 C++ 内存模型与硬件内存模型的链接。
如果您使用 C++ 编写,使用 std::atomic<> 是告诉编译器您有什么排序要求的好方法,因此它不会在编译时重新排序您的内存操作。您可以并且应该在适当的情况下使用较弱的发布或获取语义,而不是默认的顺序一致性,因此编译器根本不必在 x86 上发出任何屏障指令。它只需要保持操作的源顺序。
在 ARM 或 PPC 或带有 movnt 的 x86 等弱排序架构上,您需要在写入缓冲区和设置标志以指示数据已准备好之间的 StoreStore 屏障指令。此外,读取器在检查标志和读取缓冲区之间需要一个 LoadLoad 屏障指令。
不算 movnt,x86 已经在每个加载之间设置了 LoadLoad 屏障,在每个存储之间设置了 StoreStore 屏障。 (LoadStore 排序也得到保证)。 MFENCE 是所有 4 种屏障,包括 StoreLoad,这是 x86 默认情况下唯一不做的屏障。 MFENCE 确保加载不会使用其他线程看到您的存储并可能进行自己的存储之前的旧预取值。 (同时也是 NT 存储排序和加载排序的障碍。)
有趣的事实:x86 lock-prefixed 指令也是完整的内存屏障。它们可以在旧的 32 位代码中用作 MFENCE 的替代品,这些代码可能在不支持它的 CPU 上运行。 lock add [esp], 0 否则为空操作,并在内存上执行读/修改/写循环,这很可能在 L1 缓存中很热,并且已经处于 MESI 一致性协议的 M 状态。
SFENCE 是 StoreStore 屏障。在 NT 存储之后为后续存储创建发布语义很有用。
LFENCE 几乎总是与内存屏障无关,因为唯一的弱序负载
LoadLoad 和 also a LoadStore barrier。 (loadNT / LFENCE / storeNT 防止存储在加载之前变得全局可见。我认为如果加载地址是长依赖链的结果,或者缓存中丢失的另一个加载的结果,这可能会在实践中发生。)
ERMSB 字符串操作
有趣的事实 #2(感谢 @EOF):来自 ERMSB (Enhanced rep movsb/rep stosb on IvyBridge and later) 的存储是弱排序的(但不是缓存绕过)。 ERMSB 建立在常规快速字符串操作(自 PPro 以来一直存在的 rep stos/movsb 的微编码实现的广泛存储)之上。
英特尔在其软件开发人员手册第 1 卷的第 7.3.9.3 节中记录了 ERMSB 存储“可能出现乱序执行”的事实。他们还说
"顺序相关代码应写入离散信号量变量
在任何字符串操作之后以允许看到正确排序的数据
由所有处理器“
他们没有提到在 rep movsb 和存储到 data_ready 标志之间需要任何屏障指令。
按照我的阅读方式,rep stosb / rep movsb 之后有一个隐含的 SFENCE(至少是字符串数据的栅栏,可能不是其他运行中的弱排序 NT 存储)。无论如何,措辞意味着对标志/信号量的写入在所有字符串移动写入之后变得全局可见,因此在使用快速字符串操作填充缓冲区的代码中不需要 SFENCE / LFENCE然后写入一个标志,或者在读取它的代码中。
(LoadLoad 排序总是发生,所以你总是按照其他 CPU 使其全局可见的顺序看到数据。即使用弱排序存储写入缓冲区不会改变其他线程中的加载仍然是强排序的事实.)
总结:使用普通存储写入一个标志,指示缓冲区已准备好。 不要让读者只检查用 memset/memcpy 写入的块的最后一个字节。
我还认为 ERMSB 商店会阻止任何以后的商店通过它们,所以如果您使用 movNT,您仍然只需要 SFENCE。即rep stosb 作为一个整体具有发布语义。较早的说明。
有一个 MSR 位可以被清除以禁用 ERMSB,以便新服务器运行旧二进制文件,这些旧二进制文件将“数据就绪”标志作为rep stosb 或rep movsb 的一部分写入。 (在这种情况下,我猜你会得到旧的快速字符串微码,它可能使用有效的缓存协议,但确实使所有存储按顺序显示给其他内核)。