英译中:Intel 内存模型是否使得 SFENCE 和 LFENCE 变得多余?

23

英特尔内存模型保证:

  • 存储操作不会与其他存储操作重排序
  • 加载操作不会与其他加载操作重排序

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

我曾经看到过有人声称在x86-64上SFENCE指令是多余的,但从未听说过LFENCE是否也是多余的。上述内存模型规则是否使这两条指令都变成了多余的?


那么存储-加载和加载-存储排序呢? - Iwillnotexist Idonotexist
@IwillnotexistIdonotexist:MFENCE是一个StoreLoad屏障(还有其他三种)。是的,你仍然需要它。:P 我不确定movNT加载/存储是否会显示LoadStore重新排序,或者他们是否省略了单独的LoadStore屏障指令,因为你通常(总是?)需要在需要LoadStore屏障时使用StoreLoad屏障。由于它只影响movnt流操作,所以它是一个特殊情况的特殊情况,x86没有问题。:P - Peter Cordes
@PeterCordes 在之前的回答中,我引用了Intel SDMs中允许重新排序的完整列表。但是,我的上面的评论所指的是OP指出,大多数情况下是正确的,即Load-Load和Store-Store重新排序不会发生。然而,这些只是总共四种可能性中的两种(Load-Store和Store-Load是另外两种组合),而这些其他可能性确实会发生,因此需要使用mfence/sfence/lfence - Iwillnotexist Idonotexist
1
@IwillnotexistIdonotexist:哦,很有趣,因此LFENCE也是一种Load-Store屏障,因为后面的存储在加载/lfence之前不能全局可见。我假设通常movnt加载/存储以那种方式重新排序,最可能是如果加载地址直到存储之后才可用。我没有仔细研究过LFENCE,并且认为它只是LoadLoad屏障。 - Peter Cordes
1个回答

24
正确,对于正常代码,LFENCE和SFENCE在x86的获取/释放语义下是无用的,除非您使用其他特殊指令或内存类型。对于正常无锁代码唯一重要的障碍是来自“lock”指令的完整障碍(包括StoreLoad)或缓慢的MFENCE。与mov + mfence相比,更喜欢使用xchg进行顺序一致性存储。 加载和存储是唯一可以重新排序的指令吗? 因为它更快。 假设没有非临时指令,'xchg'是否包含'mfence'? (是的,即使有NT指令,只要没有WC内存。)

Jeff Preshing的Memory Reordering Caught in the Act文章是Bartosz帖子讨论的同一案例的易读版本,在这种情况下,您需要像MFENCE这样的StoreLoad屏障。只有MFENCE才行;您不能使用SFENCE + LFENCE构建MFENCE。(为什么SFENCE + LFENCE等同于MFENCE还是不等同于MFENCE?)

如果您在阅读您发布的链接后有疑问,请阅读Jeff Preshing的其他博客文章。它们使我对这个主题有了很好的理解。:) 尽管我认为我在Doug Lea的页面中找到了关于SFENCE/LFENCE通常是无操作的提示。Jeff的帖子没有考虑NT负载/存储。


相关: 何时使用_mm_sfence、_mm_lfence和_mm_mfence (我和@BeeOnRope的回答都很好。我写这个答案比那个答案早得多,所以这个答案的部分显示了我多年前的经验不足。我在那里的回答考虑了C++内置函数和C++编译时内存顺序,这与x86汇编运行时内存顺序完全不同。但你仍然不想要_mm_lfence()。)


SFENCE 只在使用 movnt (非临时)流式存储或者处理类型设置为非正常写回的内存区域时才相关。或者使用类似于弱序存储的 clflushopt。NT 存储既绕过了缓存又是弱序的。x86 的正常内存模型是强序的,除了 NT 存储、WC(写合并)内存和 ERMSB 字符串操作(见下文)。

LFENCE 只对弱序加载的内存排序有用,这种情况非常罕见。(或者可能适用于普通加载 之前 的 LoadStore 排序与 NT 存储?)

NT从WB内存中加载(movntdqa)仍然是强顺序的,即使在一个假设的未来CPU上,该CPU不会忽略NT提示;在x86上进行弱有序加载的唯一方法是从弱有序内存(WC)读取,然后我认为只能使用movntdqa。这在"正常"程序中不会意外发生,因此只有在映射视频RAM或类似情况下才需要担心。
(lfence的主要用途根本不是内存排序,而是用于序列化指令执行,例如用于Spectre缓解或与RDTSC配合使用。请参见{{link3:AMD处理器上的LFENCE序列化吗?}}和该问题的“相关问题”侧边栏。)

C++中的内存排序以及它如何映射到x86汇编

我几周前对此产生了兴趣,并在最近的一个问题中发布了一个相当详细的答案: 原子操作,std::atomic<>和写入顺序。 我包括了很多关于C++内存模型与硬件内存模型的内容链接。

如果您正在使用C++编写代码,使用std::atomic<>是告诉编译器您具有哪些排序要求的绝佳方式,因此它不会在编译时重新排列您的内存操作。 在适当的情况下,您可以使用较弱的释放或获取语义,而不是默认的顺序一致性,这样编译器就不必在x86上发出任何屏障指令。 它只需按源顺序保留操作即可。


在像ARM、PPC或者带有movnt的x86等弱序体系结构中,需要在写入缓冲区并设置标志以指示数据已就绪之间加入StoreStore屏障指令。此外,在检查标志并读取缓冲区之间,读取者需要使用LoadLoad屏障指令。
不包括movnt,x86已经在每次加载时有LoadLoad屏障,并且在每次存储时有StoreStore屏障。(也保证了LoadStore排序)。MFENCE是4种类型的屏障,包括StoreLoad屏障,这是x86默认不执行的唯一屏障。MFENCE确保负载不会使用其他线程看到您的存储并可能执行自己的存储之前预取的旧值。(以及作为NT存储顺序和负载顺序的障碍。)
有趣的事实:x86的lock-前缀指令也是完整的内存屏障。它们可以用作旧的32位代码中MFENCE的替代品,这些代码可能在不支持MFENCE的CPU上运行。否则,"lock add [esp], 0" 是一个无操作指令,并在L1高速缓存中非常热并且已经在MESI协议的M状态下对内存执行读取/修改/写入周期。

SFENCE是一个StoreStore障碍。在NT存储后使用它可以为后续的存储创建发布语义。

LFENCE几乎总是与内存障碍无关,因为唯一的弱序加载

一个LoadLoad和还有一个LoadStore障碍。(loadNT / LFENCE / storeNT防止存储在加载之前变为全局可见。我认为如果加载地址是长依赖链的结果,或者是另一个未命中缓存的加载的结果,在实践中可能会发生这种情况。)


ERMSB字符串操作

有趣的事实#2(感谢@EOF):ERMSB(增强的rep movsb/rep stosb在IvyBridge及更高版本上)的存储是弱序的(但不是缓存旁路)。 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等。在这种情况下,我想您会获得旧的快速字符串微码,它可能使用高效的缓存协议,但确实使所有存储看起来对其他内核按顺序。


3
不仅 movnt 指令具有较弱的内存序,memcpy/strcpy 指令 (rep[ne] movs[b/w/d/q]) 也是如此。 - EOF
@EOF:谢谢,我不知道这个!奇怪的是指令参考手册没有提到这一点,只有Vol1手册。我更新了我的答案,并解释了文档的含义:在rep movsb之后,有一个隐式的StoreStore屏障(用于字符串数据),因此您只需要单独编写数据准备标志(而不是作为字符串操作的最后几个字节)。 - Peter Cordes
@EOF: 不仅仅是movntrep[ne] movs[b/w/d/q]; 而是(潜在地)每一个访问内存的指令; 因为可以通过将PAT/页面表或MTRRs配置为正在访问的内存的“写组合”(而不是“写回”),从而削弱内存排序模型。 - Brendan
@Brendan:我假设的是在像Linux这样的普通操作系统中的用户进程上下文。您可以假设除非您采取了特殊的OS特定映射来映射任何其他页面,否则所有页面都是WB。对于大多数用途,WB内存的性能要比任何其他类型的内存都要好得多。不过有趣的一点是,WC内存是弱序的。 - Peter Cordes
@PeterCordes:如果假设是正确的,那么假设就没问题了,但即使如此,至少要意识到存在这些假设不正确的情况(例如设备驱动程序与内存映射的IO区域通信,如视频显示内存)。 - Brendan
@Brendan:我认为我的回答目前已经清楚地提到了内存类型,因此不会引起误导。但是,是的,我猜其他人在使用汇编语言时对上下文的默认假设可能会有所不同。好观点。 - Peter Cordes

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