我相信你的问题与询问
mfence
是否具有与x86上的
lock
前缀指令相同的屏障语义或者在某些情况下是否提供更少或额外的保证相同。我的最佳答案是,这是英特尔的意图,并且ISA文档保证
mfence
和
lock
指令提供相同的屏障语义,但由于实现疏忽,
mfence
实际上在最近的硬件上(至少从Haswell开始)提供了更强的屏障语义。特别是,
mfence
可以从WC类型内存区域中对后续的非暂态负载进行屏障处理,而
lock
指令则不能。我们之所以知道这一点,是因为英特尔在处理器勘误中告诉了我们这一点,例如
HSD162(Haswell)和
SKL155(Skylake),它们告诉我们锁定指令不会对来自WC内存的后续非暂态读取进行屏障处理。
MOVNTDQA从WC内存中加载可能会跳过先前访问不同高速缓存行的锁定指令。这意味着,期望锁定指令来隔离后续(V)MOVNTDQA指令的软件可能无法正常运行。目前没有已知解决方法。如果软件依赖于锁定指令来隔离后续(V)MOVNTDQA指令,则应在锁定指令和后续(V)MOVNTDQA指令之间插入MFENCE指令。由此可以确定:(1)英特尔可能打算让锁定指令隔离WC类型内存的NT加载,否则这不会成为勘误表;(2)锁定指令实际上并未实现该功能,英特尔无法或选择不使用微码更新修复,因此建议使用mfence代替。
在Skylake中,根据SKL079的说法,“mfence”实际上失去了对NT负载的额外围栏功能,这与“lock-instruction”勘误表几乎具有相同的文本,但适用于“mfence”。然而,此勘误表的状态是“BIOS可能包含此勘误的解决方法”,这通常是英特尔的说法,“微码更新解决了这个问题”。
这一系列勘误表的出现可以通过时间来解释:Haswell勘误表仅在2016年初才出现,比处理器发布日期晚了数年,因此我们可以假设该问题在此之前已引起英特尔的注意。此时,Skylake几乎肯定已经进入市场,并且显然采用了更少保守的“mfence”实现,也没有在WC类型内存区域上围栏NT负载。从Haswell一直修复锁定指令的工作方式可能是不可能或代价高昂的,因为它们被广泛使用,但需要某种方式来围栏NT负载。“mfence”显然已经在Haswell上完成了这项工作,并且Skylake将被修复,以便“mfence”也在那里工作。
然而,这并不能真正解释为什么SKL079(“mfence”)出现在2016年1月,比SKL155(“locked”)晚两年多,后者出现在2017年末,而与Haswell勘误表相同的时间间隔较大。
有人可能会猜测英特尔未来会做什么。由于他们无法/不愿意为Haswell到Skylake的lock指令进行更改,这代表了数亿(十亿?)部署芯片,他们将永远无法保证锁定指令栅栏NT负载,因此他们可能会考虑将其作为未来的文档化、体系结构行为。或者他们可能会更新锁定指令,使它们对这样的读取进行栅栏操作,但实际上你不能依赖这一点,可能要等待十年或更长时间,直到具有当前非栅栏行为的芯片几乎退出市场。
与Haswell类似,根据BV116和BJ138,NT负载可能会在Sandy Bridge和Ivy Bridge上分别传递先前锁定的指令。更早的微架构也可能存在此问题。这个“漏洞”似乎不存在于Broadwell和Skylake之后的微架构中。
Peter Cordes在this answer的末尾写了一些关于Skylake mfence
变化的内容。
本答案的剩余部分是我在不知道勘误之前撰写的原始答案,主要出于历史兴趣而被保留。
旧答案
我的猜测是,
mfence
提供了额外的屏障功能:在使用弱序指令(例如 NT 存储)之间的访问以及在访问弱序区域(例如 WC 类型内存)之间。但需要说明的是,这只是我的猜测,下面将详细解释。
详情
文档
mfence
的内存一致性效果与带有
lock
前缀的指令(包括带有内存操作数的
xchg
指令,它会被隐式锁定)的区别并不明显。
我认为可以肯定地说,仅涉及写回内存区域且不涉及任何非暂态访问时,
mfence
提供与
lock
前缀操作相同的排序语义。
有待讨论的是,在上述情况之外的场景中,特别是涉及到除 WB 区域之外的区域或涉及非暂态(流式)操作时,
mfence
是否与
lock
前缀指令有所不同。
例如,您可以找到一些建议(例如
这里或
这里),表明当涉及WC类型操作(例如NT存储)时,
mfence
意味着强制屏障语义。
例如,在
此线程中引用McCalpin博士的话(重点添加):
栅栏指令只需要确保在后续的“普通”存储之前所有非临时存储都可见即可。最明显的情况是并行代码中,在并行区域结尾处的“barrier”可能包含一个“普通”存储。没有栅栏,处理器可能仍然在写组合缓冲区中修改数据,但通过障碍并允许其他处理器读取“陈旧”的副本。这种情况也可能适用于单个线程,该线程由操作系统从一个核心迁移到另一个核心(不确定此情况)。
我记不清详细的推理(今天早上还没有喝足够的咖啡),但是在非暂态存储之后要使用的指令是MFENCE。根据SWDM第3卷第8.2.5节所述,MFENCE是唯一可以防止后续负载和后续存储在栅栏完成之前执行的栅栏指令。我很惊讶地发现,这在第11.3.1节中未提及,该节告诉您在使用写组合时手动确保一致性的重要性,但未告诉您如何实现!
让我们查看Intel SDM的引用第8.2.5节:
加强或削弱内存排序模型
Intel 64和IA-32体系结构提供了几种机制,用于加强或削弱内存排序模型以处理特殊编程情况。这些机制包括:
• I/O指令、锁定指令、LOCK前缀和序列化指令可以强制处理器上更强的排序。
• SFENCE指令(在Pentium III处理器中引入到IA-32体系结构中)、LFENCE和MFENCE指令(在Pentium 4处理器中引入)为特定类型的内存操作提供内存排序和序列化能力。
这些机制可以如下使用:
映射到内存的设备和总线上的其他I/O设备通常对写入其I/O缓冲区的顺序敏感。可以使用I/O指令(IN和OUT指令)来强制进行此类访问的强写入排序。在执行I/O指令之前,处理器等待程序中的所有先前指令完成并等待所有缓冲写入到内存中。只有指令获取和页表遍历可以通过I/O指令。在处理器确定I/O指令已完成之前,不会开始执行后续指令。
多处理器系统中的同步机制可能取决于强内存排序模型。在这里,程序可以使用锁定指令(如XCHG指令或LOCK前缀)来确保对内存的读取-修改-写入操作是原子执行的。锁定操作通常类似于I/O操作,因为它们等待所有先前的指令完成并等待所有缓冲写入到内存中(参见第8.1.2节“总线锁定”)。
程序同步也可以使用序列化指令(参见第8.3节)。这些指令通常在关键过程或任务边界处使用,以强制在跳转到代码的新部分或上下文切换之前完成所有先前的指令。与I/O和锁定指令一样,在执行序列化指令之前,处理器会等待所有先前的指令完成并等待所有缓冲写入到内存中。
SFENCE、LFENCE和MFENCE指令提供了一种性能高效的方法,用于确保产生弱有序结果的例程和消耗该数据的例程之间的加载和存储内存排序。这些指令的功能如下:
• SFENCE - 串行化程序指令流中SFENCE指令之前发生的所有存储(写入)操作,但不影响加载操作。
• LFENCE - 串行化程序指令流中LFENCE指令之前发生的所有加载(读取)操作,但不影响存储操作。
• MFENCE - 串行化程序指令流中MFENCE指令之前发生的所有存储和加载操作。
请注意,SFENCE、LFENCE和MFENCE指令提供了比CPUID指令更有效的控制内存排序的方法。
与McCalpin博士的解释相反,我认为这一部分在mfence是否有额外操作方面有些模糊。涉及IO、锁定指令和序列化指令的三个部分确实意味着它们提供了在操作之前和之后的内存操作之间提供完整屏障的功能。他们没有对弱排序内存做任何例外,在IO指令的情况下,人们也会认为它们需要以一致的方式与弱排序内存区域一起工作,因为这样的区域经常用于IO。
然后是FENCE指令的部分,它明确提到了弱内存区域:“SFENCE、LFENCE和MFENCE指令提供了一种性能高效的方式,确保在产生弱排序结果和消耗该数据的例程之间进行加载和存储内存排序。”
我们是否应该读懂其中的含义,认为这些是唯一可以完成此任务的指令,并且先前提到的技术(包括锁定指令)对于弱内存区域没有帮助?我们可以通过注意到fence指令与弱排序非暂态存储指令同时引入
3,以及像
11.6.13缓存提示指令中特别处理弱排序指令的文本来找到对这个想法的一些支持。
一个使用数据的消费者了解数据的弱排序程度可能因情况而异,因此在产生弱排序数据的例程和消耗数据的例程之间应使用SFENCE或MFENCE指令来确保排序。 SFENCE和MFENCE提供了一种性能高效的方法来确保排序,通过保证程序顺序中在SFENCE / MFENCE之前的每个存储指令在后面跟随栅栏的存储指令之前全局可见。同样,在此处特别提到栅栏指令是适用于对弱排序指令进行隔离的。
我们还发现,锁定指令可能不会提供弱排序访问之间的屏障,已经在上述最后一句引文中明确提到:
注意,SFENCE,LFENCE和MFENCE指令提供了比CPUID指令更有效的控制内存排序的方法。
这基本上意味着FENCE指令在内存排序方面实际上取代了以前由序列化的CPUID提供的功能。但是,如果锁定前缀指令提供与CPUID相同的障碍功能,那可能是先前建议的方式,因为这些通常比需要200个或更多周期的CPUID快得多。暗示存在场景(可能是弱排序场景)未能处理锁定前缀指令的情况,而使用CPUID,并在此处建议使用MFENCE作为替代方案,暗示其具有比锁定前缀指令更强的障碍语义。
然而,我们可以以不同的方式解释上述内容:请注意,在栅栏指令的上下文中,经常提到它们是一种性能高效的方法来确保排序。因此,这些指令可能并非旨在提供额外的屏障,而只是更有效的屏障。
实际上,几个周期内的sfence要比通常需要20个周期或更长时间的序列化指令如cpuid或带有lock前缀的指令快得多。另一方面,mfence通常不比锁定指令更快,至少在现代硬件上不是这样。但是,在引入时可能会更快,或在某些未来的设计上可能会更快,或者可能预计它会更快,但事实并非如此。
因此,我无法根据手册的这些部分作出确定的评估:我认为您可以合理地争辩说它可以被解释为任何一种方式。
我们还可以查看Intel ISA指南中各种非暂态存储指令的文档。例如,在非暂态存储movnti的文档中,您会发现以下引用:
由于WC协议使用弱顺序内存一致性模型,如果多个处理器可能使用不同的内存类型读/写目标内存位置,则应结合使用SFENCE或MFENCE指令实现的栅栏操作与MOVNTI指令。
关于“如果多个处理器可能使用不同的内存类型来读/写目标内存位置”的部分对我有些困惑。我认为这应该更像是“在使用弱排序提示的指令之间强制执行全局可见写入顺序”,或者类似于此类的内容。实际上,{{内存类型}}(例如由MTTR定义)在这里可能根本没有发挥作用:当使用弱排序指令时,排序问题可以仅在WB内存中出现。
性能方面,基于Agner Fog的指令定时,mfence指令报告需要33个周期(背靠背延迟),但是像lock cmpxchg这样更复杂的锁定指令报告只需要18个周期。如果mfence提供的屏障语义不比lock cmpxchg更强,那么后者正在做更多的工作,并且似乎没有明显的理由让mfence花费显著更长的时间。当然,你可以说lock cmpxchg比mfence更重要,因此得到了更多的优化。这个论点被所有锁定指令都比mfence快得多(即使是很少使用的指令)的事实削弱了。此外,你会想象,如果有一个单一的屏障实现被所有lock指令共享,那么mfence将简单地使用相同的实现,因为这是最简单和最容易验证的。
因此,我认为
mfence
的性能较慢是一些额外操作的重要证据。
0.5这不是一个严密的论点。一些东西可能会出现在勘误表中,这些错误显然是“设计上”的而不是漏洞,比如popcnt
对目标寄存器的虚假依赖-因此有些勘误表可以被视为一种文档形式来更新期望,而不总是意味着硬件漏洞。
1 显然,带有lock
前缀的指令也执行原子操作,这不可能仅通过mfence
实现,因此lock
前缀指令肯定具有额外的功能。因此,为了使mfence
有用,我们期望它在某些情况下具有额外的屏障语义,或者表现更好。
2他阅读的可能是手册的不同版本,其中的散文不同。
3在SSE中是SFENCE
,在SSE2中是lfence
和mfence
。
4并且通常更慢:Agner在最近的硬件上将其列为33个周期的延迟,而锁定指令通常约为20个周期。
mfence
一样被序列化。 - Valaraucamfence
特别慢的地方,已经使用了lock add [mem], 0
或lock or [mem], 0
或lock and [mem], -1
来代替它。诀窍在于找到一个保证可访问但未被使用的缓存内存位置。我记得从堆栈指针中使用了一个不错的偏移量来作为[mem]
。 - EOFlock add
要低。 - Peter Cordesmov [shared], eax
/mfence
与xchg [shared], eax
兼容,作为实现shared.store(eax, std::memory_order_seq_cst)
的一种方式。但是正如 BeeOnRope 的答案指出的那样,mfence
具有较低的背靠背吞吐量,这表明它正在执行某些不同的操作,也许lock
操作没有将 NT 存储作为障碍。 - Peter Cordes