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

59

在互联网上经常看到说x86处理器中的LFENCE无意义,即它什么也不做,所以我们可以代替使用MFENCE来绝对无痛地使用SFENCE,因为MFENCE=SFENCE+LFENCE=SFENCE+NOP=SFENCE

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

  1. LOAD(没有栅栏)和STORE+MFENCE
  2. LOAD(没有栅栏)和LOCK XCHG
  3. MFENCE+LOADSTORE(没有栅栏)
  4. LOCK XADD(0)和STORE(没有栅栏)

摘自此处:http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html

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

如果LFENCE什么也不做,那么方法(3)会有以下含义:SFENCE + LOAD和STORE(没有栅栏),但在执行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将清空存储缓冲区。


6
有一些带有“非时间提示”的指令,它们的顺序并不像通常的读写那样强制;我想这些指令可能会受益于屏障。 (编辑:实际上,您链接的页面中提到了这一点。) - Kerrek SB
2个回答

45
底线(TL;DR):仅使用LFENCE似乎无法实现内存排序,但这并不意味着SFENCE可以替代MFENCE。问题中的“算术”逻辑不适用。

这里是来自英特尔软件开发手册第三卷第8.2.2节的摘录(2014年9月版号为325384-052US),与另一个答案中使用的相同。

  • 读操作不会与其他读操作重排。
  • 写操作不会与先前的读操作重排。
  • 内存写操作不会与其他写操作重排,但有以下例外:
    • 使用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 单独对于内存排序是无用的。

为了支持最后一个观点,我查看了Intel手册的所有3卷中提到 LFENCE 的所有地方,并没有发现任何一个说 LFENCE 是必需的以保证内存一致性。即使是唯一的非临时加载指令 MOVNTDQA 也提到了 MFENCE 而不是 LFENCE


更新:关于为什么SFENCE + LFENCE等同于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进行栅栏。)

感谢您的评论。事实上更加复杂了;我已经重写了答案并添加了更多细节。 - Alexey Kukanov
是的,谢谢您的留言,我同意,LFENCE+SFENCE != SFENCE+LFENCE :) 因为对于第一个可以发生下面的情况:MOV [addr1], reg LFENCE SFENCE MOV reg, [addr2] --> LFENCE MOV [addr1], reg MOV reg, [addr2] SFENCE --> LFENCE MOV [addr1], reg MOV reg, [addr2] SFENCE --> LFENCE MOV reg, [addr2] MOV [addr1], reg SFENCE。也就是说,我们得到了重新排序的存储 <-> 加载,即不满足顺序一致性。我在我的问题中解决了这个问题。 - Alex
1
可能新的主要问题值得单独在SO上提问。 - Alexey Kukanov
1
谢谢!好的,我创建了一个新的单独问题:https://dev59.com/8F4c5IYBdhLWcg3wssFs - Alex
在我看来,SFENCE/LFENCE对实现了C++11的释放/获取内存顺序语义。难道不是这样吗? - Maxim Egorushkin
显示剩余4条评论

9
考虑以下情况 - 这是关键的情况,其中推测性负载执行理论上可能会损害顺序一致性。
最初 [x]=[y]=0。
CPU0:                              CPU1: 
store [x]<--1                      store [y]<--1
load  r1<--[y]                     load r2<--[x]

由于x86允许对不同地址的早期存储进行重新排序,因此两个加载都可能返回0。仅在每个存储后添加lfence无法防止这种情况,因为它们只能防止同一上下文中的重新排序,但是由于存储是在退休之后分派的,因此您可以在执行和观察存储之前提交两个lfences和两个加载。

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

至于sfences - 正如评论中指出的那样,从理论上讲,它不足以防止加载重新排序到它上面,因此它可能仍会读取旧数据。尽管就内存官方排序规则而言这是正确的,但我认为当前的x86 uarch实现使其稍微更强(虽然我猜不会承诺未来这样做)。根据this description

由于x86架构的强大排序模型,加载缓冲区会被一致性流量监视。远程存储必须使缓存行的所有其他副本无效。如果缓存行被加载后由远程存储无效,则必须取消加载,因为它可能读取了无效数据。x86内存模型不要求监视存储缓冲区。
因此,任何尚未在机器中提交的加载都应该可以被其他核心的存储进行嗅探,从而使加载的有效观察时间为提交点,而不是执行点(后者确实是无序的,可能已经执行得早得多)。提交按顺序进行,因此在先前指令之后应观察加载 - 使lfences基本上无用,正如我在评论中所说的那样,因为可以通过相同的方式维护一致性,而无需使用它们。这主要是猜测,试图解释lfences在x86中毫无意义的普遍概念 - 我不确定它的起源和是否有其他考虑因素 - 如果有专家可以批准/挑战这个理论,我会很高兴。
以上所有内容仅适用于WB mem类型。

2
SFENCE 无法用于修复此示例,因为负载可以在 SFENCE 上方迁移。该示例需要使用 MFENCE 进行修复。 - Arch D. Robison
@ArchD.Robison,在实践中,负载必须在任何先前的指令之后退役,在此期间,来自其他线程的存储将能够窥视它 - 我认为这将导致重新执行,但我不完全确定,所以我按照您的建议修复了答案 - 谢谢。 - Leeor
谢谢修复。我建议编写针对架构保证的代码,因为微架构师总是在寻找聪明而神秘的方法来加速事情。 - Arch D. Robison
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - user997112
@user997112,是的。存储的结果可以进行推测计算,但存储本身在提交之前不能被外部暴露和可见(即-直到所有指令都已知道正确路径并且不会故障)。一些机器可以处理这样的存储,可能使用特殊位将它们标记为推测性的(以防一个窥视者出现),但常见的解决方案是将存储保留在缓冲区中,直到它提交。还要注意,在像x86这样的体系结构中,从缓冲区调度的存储也必须按顺序进行。 - Leeor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接