在 x86/x86_64 处理器上使用 LFENCE 指令是不是有意义?

Posted

技术标签:

【中文标题】在 x86/x86_64 处理器上使用 LFENCE 指令是不是有意义?【英文标题】:Does it make any sense to use the LFENCE instruction on x86/x86_64 processors?在 x86/x86_64 处理器上使用 LFENCE 指令是否有意义? 【发布时间】:2013-12-17 10:37:20 【问题描述】:

我经常在互联网上发现 LFENCE 在 x86 处理器中毫无意义,即它什么也不做,所以我们可以完全轻松地使用 SFENCE,因为 MFENCE = SFENCE + @987654328 @ = SFENCE + NOP = SFENCE.

但如果LFENCE 没有意义,那么为什么我们有四种方法可以在 x86/x86_64 中实现顺序一致性:

    LOAD(无围栏)和STORE + MFENCE LOAD(无围栏)和LOCK XCHG MFENCE + LOADSTORE(无围栏) LOCK XADD ( 0 ) 和 STORE (没有围栏)

取自这里:http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html

以及底部第 34 页 Herb Sutter 的表演:https://skydrive.live.com/view.aspx?resid=4E86B0CF20EF15AD!24884&app=WordPdf&wdo=2&authkey=!AMtj_EflYn2507c

如果LFENCE没有做任何事情,那么方法(3)的含义如下:SFENCE + LOAD and STORE (without fence),但是在LOAD之前做SFENCE是没有意义的。即如果LFENCE什么都不做,则方法(3)没有意义。

处理器 x86/x86_64 中的指令 LFENCE 是否有意义?

回答:

1. LFENCE 在下面接受的答案中描述的情况下是必需的。

2. 方法(3)不应单独看待,而应结合前面的命令。例如方法(3):

MFENCE
MOV reg, [addr1]  // LOAD-1
MOV [addr2], reg  //STORE-1

MFENCE
MOV reg, [addr1]  // LOAD-2
MOV [addr2], reg  //STORE-2

我们可以将方法(3)的代码改写如下:

SFENCE
MOV reg, [addr1]  // LOAD-1
MOV [addr2], reg  //STORE-1

SFENCE
MOV reg, [addr1]  // LOAD-2
MOV [addr2], reg  //STORE-2

这里SFENCE 可以防止重新排序 STORE-1 和 LOAD-2。为此,在 STORE-1 命令 SFENCE 刷新 Store-Buffer 之后。

【问题讨论】:

有些指令带有“非临时提示”,不像通常的加载和存储那样有序;我想那些可能会从击剑中受益。 (编辑:这实际上是在您链接的页面上提到的。) 【参考方案1】:

底线 (TL;DR):LFENCE 单独似乎对内存排序毫无用处,但它并不能使 SFENCE 替代 MFENCE。问题中的“算术”逻辑不适用。


这里摘自Intel's Software Developers Manual, volume 3,第 8.2.2 节(2014 年 9 月的版本 325384-052US),与我在another answer 中使用的相同

读取不会与其他读取重新排序。 写入不会与较旧的读取一起重新排序。 对内存的写入不会与其他写入一起重新排序,但以下情况除外: 使用 CLFLUSH 指令执行写入; 使用非临时移动指令(MOVNTI、MOVNTQ、MOVNTDQ、MOVNTPS 和 MOVNTPD)执行的流式存储(写入);和 字符串操作(参见第 8.2.4.1 节)。 可以对不同位置的旧写入重新排序读取,但对同一位置的旧写入则不能。 无法使用 I/O 指令、锁定指令或序列化指令重新排序读取或写入。 读取无法通过早期的 LFENCE 和 MFENCE 指令。 写入无法通过早期的 LFENCE、SFENCE 和 MFENCE 指令。 LFENCE 指令无法通过较早的读取。 SFENCE 指令无法传递较早的写入。 MFENCE 指令无法传递较早的读取或写入。

从这里可以得出:

MFENCE 是一个完整的内存栅栏,适用于所有内存类型的所有操作,无论是否是非临时的。 SFENCE 仅防止重新排序写入(在其他术语中,它是 StoreStore 屏障),并且仅与非临时存储和列为例外的其他指令一起使用。 LFENCE 防止读取与后续读取和写入重新排序(即它结合了 LoadLoad 和 LoadStore 屏障)。但是,前两个项目符号表示 LoadLoad 和 LoadStore 屏障始终存在,没有例外。因此,单独的LFENCE 对内存排序毫无用处。

为了支持最后一个说法,我查看了所有 3 卷英特尔手册中提到 LFENCE 的所有地方,但没有发现任何地方会说 LFENCE 是内存一致性所必需的。甚至MOVNTDQA——迄今为止唯一的非临时加载指令——提到了MFENCE,但没有提到LFENCE


更新:请参阅Why is (or isn't?) SFENCE + LFENCE equivalent to MFENCE? 上的答案以获取以下猜测的正确答案

MFENCE 是否等于其他两个栅栏的“总和”是一个棘手的问题。乍一看,在三个栅栏指令中,只有 MFENCE 提供了 StoreLoad 屏障,即防止对较早写入的读取重新排序。但是正确的答案需要知道的不仅仅是上述规则;也就是说,重要的是所有围栏指令都相对于彼此进行排序。这使得SFENCE LFENCE 序列比单纯的单个效果的联合更强大:这个序列还可以防止 StoreLoad 重新排序(因为加载不能通过 LFENCE,它不能通过 SFENCE,它不能通过存储),因此构成了一个完整的内存栅栏(另请参阅下面的注释 (*))。但请注意,这里的顺序很重要,LFENCE SFENCE 序列没有相同的协同效应。

然而,虽然可以说MFENCE ~ SFENCE LFENCELFENCE ~ NOP,但这并不意味着MFENCE ~ SFENCE。我故意使用等价 (~) 而不是等价 (=) 来强调算术规则不适用于此处。 SFENCE 后跟 LFENCE 的相互影响产生了差异;即使负载没有相互重新排序,也需要LFENCE 以防止使用SFENCE 重新排序负载。

(*) 说MFENCE 比其他两个栅栏的组合更强大仍然可能是正确的。特别是,英特尔手册第 2 卷中对 CLFLUSH 指令的注释说:“CLFLUSH 仅由 MFENCE 指令订购。不保证由任何其他围栏或序列化指令或其他指令订购CLFLUSH 指令。”

(更新,clflush 现在被定义为强排序(就像普通商店一样,所以如果你想稍后阻止加载,你只需要mfence),但clflushopt 是弱的已订购,但可以用sfence 围起来。)

【讨论】:

谢谢,但我不同意“MFENCELFENCE+SFENCE更强。它是所有内存类型上所有操作的完整内存栅栏,无论是非时间性与否。”。我们总是可以写序列MOVNTQSFENCELFENCEMOVNTQMOV [addr], regSFENCELFENCEMOV reg, [addr]用于序列一致性。 IE。 LFENCE+SFENCE - 类似地为所有内存类型的所有操作提供完整的内存栅栏,无论是否是非临时的。 感谢您的评论。它恰好更复杂;我用更多细节重写了答案。 感谢您的澄清,是的,我同意,即如果我们使用SFENCE 而不是MFENCE 来表示顺序一致性,那么会发生什么:MOV [addr1], reg SFENCE MOV reg, [addr2] --> @ 987654375@MOV reg, [addr2]SFENCE-->MOV reg, [addr2]MOV [addr1], regSFENCE。 IE。这是合乎逻辑的,我们重新排序了 Store Load。但它真的会发生吗?如果是这样,那么一定有一个包含命令围栏的机制,这个机制是什么?到处都写着LFENCE 没有做任何事情,即没有这样的机制。有存储缓冲区,但没有加载缓冲区。 新的主要问题可能值得一个单独的 SO 问题。 谢谢!好的,我创建了新的单独问题:***.com/questions/27627969/…【参考方案2】:

考虑以下场景 - 这是推测性加载执行理论上会损害顺序一致性的关键情况

最初 [x]=[y]=0

CPU0:                              CPU1: 
store [x]<--1                      store [y]<--1
load  r1<--[y]                     load r2<--[x]

由于 x86 允许将加载与较早的存储重新排序到不同的地址,因此两个加载都可能返回 0。在每个商店之后单独添加一个 lfence 不会阻止这种情况,因为它们只会阻止在同一上下文中重新排序,但是由于商店是在退休后调度的,因此您可以在执行和观察存储之前同时提交两个 lfence 和两个加载。

另一方面,mfence 会强制存储执行,然后才允许执行加载,因此您将在至少一个上下文中看到更新的数据。

至于 sfences - 正如评论中所指出的,理论上它不足以阻止负载在其上方重新排序,因此它可能仍会读取陈旧的数据。虽然就内存官方订购规则而言这是正确的,但我相信 x86 uarch 的当前实现使其稍微更强大(但我猜未来不会承诺这样做)。根据this description:

由于强大的 x86 排序模型,负载缓冲区被窥探 通过一致性流量。远程存储必须使所有其他副本无效 的缓存线。如果一个缓存行被一个负载读取,然后 被远程存储无效,加载必须被取消,因为它 可能读取无效数据。 x86 内存模型不需要 监听存储缓冲区。

因此,任何尚未提交到机器中的负载都应该可以被其他核心的 store 窥探,从而使 有效 观察到 commit 点的负载的时间,而不是 执行 点(这确实是无序的,可能已经执行得更早了)。提交是按顺序完成的,因此应该在之前的指令之后观察负载 - 就像我在上面在 cmets 中所说的那样,使 lfences 几乎没有用,因为没有它们可以以相同的方式保持一致性。 这主要是推测,试图解释 lfences 在 x86 中毫无意义的普遍概念——我不完全确定它的起源以及手头是否还有其他考虑因素——任何专家都会很高兴批准/挑战这个理论。

以上所有内容当然只适用于 WB mem 类型

【讨论】:

SFENCE 不能用于修复此示例,因为负载可以跨 SFENCE 向上迁移。该示例需要 MFENCE 来修复。 @ArchD.Robison,实际上,负载必须在之前的任何指令后退出,并且在此期间来自另一个线程的存储将能够窥探它 - 我认为这会导致重新-执行,但我不完全确定,所以我按照你的建议修复了答案 - 谢谢。 感谢您的修复。我建议根据架构保证进行编码,因为微架构师总是在寻找聪明而神秘的方法来加快速度。 @Leeor 你说的“但是因为商店是退休后派送的”是什么意思?您的意思是存储的结果先保存到存储缓冲区,然后再“分派”到 L1 缓存? @user997112,是的。存储的结果可以推测性地计算,但存储本身在提交之前不得在外部公开和可见(即 - 直到它的所有指令都已知在正确的路径上并且没有错误)。一些机器可以处理缓存中的此类存储,可能使用特殊位将它们标记为推测性的(以防出现窥探),但常见的解决方案是将存储保留在缓冲区中,直到它提交。另请注意,在 x86 等存储按顺序排列的架构中,从缓冲区分派也必须按顺序完成。

以上是关于在 x86/x86_64 处理器上使用 LFENCE 指令是不是有意义?的主要内容,如果未能解决你的问题,请参考以下文章

armeabi-v7a arm64-v8a armeabi x86 x86_64区别

armeabi-v7a arm64-v8a armeabi x86 x86_64区别

armeabi-v7a arm64-v8a armeabi x86 x86_64区别

armeabi-v7a arm64-v8a armeabi x86 x86_64区别

如何在现代 x86/amd64 芯片上关闭 L1、L2、L3 CPU 缓存?

Intel x86_64 Architecture Background 1