【问题标题】:Does the Intel Memory Model make SFENCE and LFENCE redundant?英特尔内存模型是否使 SFENCE 和 LFENCE 变得多余?
【发布时间】:2015-12-18 17:46:10
【问题描述】:

英特尔内存模型保证:

  • 商店不会与其他商店重新订购
  • 负载不会与其他负载重新排序

http://bartoszmilewski.com/2008/11/05/who-ordered-memory-fences-on-an-x86/

我看到有人声称由于 Intel 内存模型,SFENCE 在 x86-64 上是冗余的,但从来没有 LFENCE。上述内存模型规则是否使任一指令变得多余?

【问题讨论】:

  • 呃,Store-Load 和 Load-Store 排序呢?
  • @IwillnotexistIdonotexist:MFENCE 是一个 StoreLoad 屏障(以及所有其他 3 种屏障)。是的,你仍然需要它。 :P 我不确定 movNT 加载/存储是否可以显示 LoadStore 重新排序,或者他们是否省略了单独的 LoadStore 屏障指令,假设您通常(总是?)在任何时候需要 LoadStore 屏障时都需要 StoreLoad 屏障。因为它无论如何只影响 movnt 流操作,所以它是特例的特例,没有它 x86 也可以。 :P
  • @PeterCordes In a previous answer,我引用了英特尔 SDM 的完整列表或允许重新排序。但是我在上面的评论中所强调的是,OP 指出,大部分正确的是,不会发生 Load-Load 和 Store-Store 重新排序。然而,这些只是总共四种可能性中的两种(Load-Store 和 Store-Load 是另外两种组合),并且那些其他可能性可能发生,因此需要mfence/sfence/lfence
  • @IwillnotexistIdonotexist:哦,有趣,所以 LFENCE 也是一个 Load-Store 屏障,因为在加载/lfence 之前,以后的存储不能全局可见。我假设通常movnt 加载/存储以这种方式重新排序,很可能是加载地址在存储之后才可用。我没有仔细观察 LFENCE,并认为它只是一个 LoadLoad 障碍。

标签: assembly optimization x86 atomic memory-barriers


【解决方案1】:

没错,LFENCE 和 SFENCE 在普通代码中没有用,因为 x86 对常规存储的获取/释放语义使它们变得多余,除非您使用其他特殊指令或内存类型。

对于正常的无锁代码而言,唯一重要的栅栏是来自 locked 指令或慢速 MFENCE 的完整栅栏(包括 StoreLoad)。对于顺序一致性存储,首选xchg,而不是mov+mfenceAre 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&lt;&gt; 是告诉编译器您有什么排序要求的好方法,因此它不会在编译时重新排序您的内存操作。您可以并且应该在适当的情况下使用较弱的发布或获取语义,而不是默认的顺序一致性,因此编译器根本不必在 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 stosbrep movsb 的一部分写入。 (在这种情况下,我猜你会得到旧的快速字符串微码,它可能使用有效的缓存协议,但确实使所有存储按顺序显示给其他内核)。

【讨论】:

  • 不仅movnt 的内存排序较弱。 memcpy/strcpy 指令 (rep[ne] movs[b/w/d/q]) 也可以。
  • @EOF:谢谢,我不知道!奇怪的是 insn ref 手册没有提到这一点,只有 Vol1 手册。我用我对文档所说的解释更新了我的答案:rep movsb 之后有一个隐式 StoreStore 屏障(用于字符串数据),所以你只需要单独编写你的数据就绪标志(而不是作为最后一个字节字符串操作)。
  • @EOF:不仅仅是movntrep[ne] movs[b/w/d/q];但是(可能)访问内存的每条指令;鉴于内存排序模型可以通过将 PAT/页表或 MTRR 配置为“写组合”(而不是“写回”)访问的内存来削弱。
  • @Brendan:我假设了普通操作系统(如 Linux)中用户进程的上下文。您可以假设您的所有页面都是 WB,除非您使用特定于操作系统的特殊页面来映射任何其他页面。对于大多数用途,WB 内存的性能比任何其他类型的内存都要好得多。不过,有趣的一点;确实,WC内存是弱序的。
  • @PeterCordes: 如果假设是正确的,那么假设是好的,但即使这样,至少要知道有些情况下这些假设是不正确的(例如,设备驱动程序与内存映射的 IO 区域通信) ,如视频显示内存)。
猜你喜欢
  • 2013-04-10
  • 1970-01-01
  • 2013-12-18
  • 2015-02-22
  • 2015-02-20
  • 1970-01-01
  • 1970-01-01
  • 2021-10-19
相关资源
最近更新 更多