锁定 xchg 和 mfence 有相同的行为吗?

23
我想知道的是,从一个线程访问正在被其他线程突变(假定随机)的内存位置的角度来看,lock xchg是否会与mfence有类似的行为。 它保证我获得最新值吗?在其后执行的任何内存读取/写入指令中?
我的困惑原因是:
“读取或写入无法与I / O指令,锁定指令或序列化指令重新排序。”
-Intel 64开发人员手册Vol. 3
这适用于跨线程吗? mfence说明:
对先前发布的所有从内存中加载和存储到内存的指令执行一次序列化操作。 这个序列化操作保证了MFENCE指令之前按程序顺序执行的每个加载和存储指令在MFENCE指令之前在全局上可见,而在MFENCE指令之后执行的任何加载或存储指令在全局上可见之前。 MFENCE指令与所有加载和存储指令,其他MFENCE指令,任何SFENCE和LFENCE指令以及任何序列化指令(例如CPUID指令)有序。
-Intel 64开发人员手册Vol 3A
这听起来像更强的保证。因为它似乎是在刷新写缓冲区,或至少是在与写缓冲区和其他核心通信以确保我的未来加载/存储是最新的。
当执行基准测试时,两个指令都需要约100个周期才能完成。 因此,无论如何,我都看不到太大的差异。

我主要是感到困惑。关于互斥锁,我收到了基于lock的指令,但这些指令中并没有包含任何内存屏障。然后我看到了使用内存屏障,但不使用锁的无锁编程。我知道AMD64具有非常强大的内存模型,但陈旧的值可能仍存在缓存中。 如果lock没有与mfence表现出相同的行为,那么互斥锁如何帮助您查看最新值?


@hidefromkgb 这意味着指令不能被重新排序,但它并没有回答 load/stores 是否像 mfence 一样被序列化。 - Valarauca
1
在x86上,锁定的原子读-修改-写操作是顺序一致的。据我所知,在微架构中mfence特别慢的地方,已经使用了lock add [mem], 0lock or [mem], 0lock and [mem], -1来代替它。诀窍在于找到一个保证可访问但未被使用的缓存内存位置。我记得从堆栈指针中使用了一个不错的偏移量来作为[mem] - EOF
“mfence” 对我来说听起来像是序列化了所有内存读写操作。“xchg” 只能关注参数中的内存吗?(让 CPU 按照不同的顺序执行其他内存读写操作)。但是,x86 内存模型太强大了,而这种情况不太可能发生。我更想问的是,我已经打开了几周的 x86 内存模型文章,等到我心情好时再仔细研究它们。 - Ped7g
2
它们都是完全的内存障碍。 没有时间写一篇完整的答案,但请参见x86标签wiki中一些关于内存排序的链接。 在AMD CPU上,MFENCE可能还暗示了部分序列化指令流的其他语义,而不仅仅是内存,其中它的吞吐量比用于内存障碍的lock add要低。 - Peter Cordes
1
更新:在我最后一条评论中,我没有考虑NT存储。对于无锁算法中的内存排序,mov [shared], eax / mfencexchg [shared], eax 兼容,作为实现 shared.store(eax, std::memory_order_seq_cst) 的一种方式。但是正如 BeeOnRope 的答案指出的那样,mfence 具有较低的背靠背吞吐量,这表明它正在执行某些不同的操作,也许 lock 操作没有将 NT 存储作为障碍。 - Peter Cordes
显示剩余8条评论
1个回答

22
我相信你的问题与询问mfence是否具有与x86上的lock前缀指令相同的屏障语义或者在某些情况下是否提供更少或额外的保证相同。我的最佳答案是,这是英特尔的意图,并且ISA文档保证mfencelock指令提供相同的屏障语义,但由于实现疏忽,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类似,根据BV116BJ138,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中是lfencemfence

4并且通常更慢:Agner在最近的硬件上将其列为33个周期的延迟,而锁定指令通常约为20个周期。


1
在Skylake上,xchg [shared], eax是NT存储的屏障。使用此代码进行测试,该代码填充缓冲区并将当前输出位置存储到共享变量中的每个高速缓存行,使用(mfence+)movxchg:https://godbolt.org/g/7Q9xgz(一些计时结果在注释中,来自ocperf.py整个过程的计时包括`mmap(MAP_POPULATE)`的时间)。仅使用`mov`而不是`mfence`,我们会得到重新排序。但是,`mfence`+`mov`和`xchg`都可以,消费者循环的速度对于两个生产者有很大的差异,因此存在一些主要差异。 - Peter Cordes
1
这并不排除lock指令未对来自WC内存的movntdqa加载进行栅栏操作; 我认为我看到过一个声明,即在这种情况下需要使用mfence(而不仅仅是lfence)。与旋转读取的消费者线程交互时的差异很有趣,并值得进一步研究(也许可以使用分别对生产者和消费者进行分析的工具,并且不计算mmap(MAP_POPULATE) ~4GiB RAM的时间。此外,在AMD CPU上进行测试将是有趣的; x86文档在纸上似乎含糊不清,因此xchg在Intel上是一个屏障并不能告诉我们它们的意思。 - Peter Cordes
1
顺便提一下,我使用 t=nt-produce+consume.xchg; g++ -Wall -std=gnu++17 -march=native -pthread -O2 nt-fence-lock-buffer.cpp -o $t && taskset -c 3,4 ocperf.py stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,instructions,uops_issued.any,uops_executed.thread,machine_clears.memory_ordering -r3 ./"$t" 进行编译(在i7-6700k上使用gcc7.3.0和DDR4-2666的Arch Linux系统,并且CPU governor在大部分测试中以约3.8GHz的速度运行)。 - Peter Cordes
1
感谢@PeterCordes,我已经把你的测试放在我的待办事项列表上一段时间了,但现在这个勘误信息出现了,我认为我们可以说lock指令旨在并且实际上确实像通常那样对NT存储进行栅栏操作,因为我们有NT加载勘误和NT存储到WB内存的数量级或两个数量级更常见,并且分布在各种代码中,所以在那里的差异可能已经被注意到(而加载行为需要勘误的事实意味着我们可以理解Intel可能打算使用lock来进行栅栏操作)。 - BeeOnRope
1
@Peter:是的,在x86上,原子RMW与C++11 memory_order_seq_cst一样强大,因此它们包括获取和释放。您只需要在x86上进行普通的mov存储和普通的mov加载,就可以获得所需的同步量,以发布指向其他线程的指针并使它们看到指向的数据(C++ ...release...acquire)。但是,如果您出于其他原因需要编写器和读取器中的原子RMW,则这已经足够了。并且已经在此答案中涵盖。 - Peter Cordes
显示剩余3条评论

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