【问题标题】:Does lock xchg have the same behavior as mfence?lock xchg 是否具有与 mfence 相同的行为?
【发布时间】:2017-03-17 11:57:03
【问题描述】:

我想知道lock xchg 是否会与mfence 有类似的行为,从一个线程访问正在被其他线程变异(让我们随机说)的内存位置的角度来看。它能保证我得到最新的价值吗?后面的内存读/写指令是什么?

我困惑的原因是:

8.2.2 “不能使用 I/O 指令、锁定指令或序列化指令重新排序读取或写入。”

-英特尔 64 位开发人员手册卷。 3

这是否适用于跨线程?

mfence 状态:

对在 MFENCE 指令之前发出的所有从内存加载和存储到内存指令执行序列化操作。这种序列化操作保证了在程序顺序中位于 MFENCE 指令之前的每条加载和存储指令都是全局可见的,然后在 MFENCE 指令之后的任何加载或存储指令都是全局可见的。 MFENCE 指令相对于所有加载和存储指令、其他 MFENCE 指令、任何 SFENCE 和 LFENCE 指令以及任何序列化指令(例如 CPUID 指令)进行排序。

-英特尔 64 位开发人员手册第 3A 卷

听起来是一个更强有力的保证。听起来mfence 几乎要刷新写缓冲区,或者至少要接触到写缓冲区和其他内核,以确保我未来的加载/存储是最新的。

当进行基准测试时,两条指令都需要大约 100 个周期才能完成。所以无论哪种方式我都看不出有那么大的区别。

主要是我很困惑。 I 指令基于在互斥体中使用的lock,但这些指令不包含内存栅栏。然后我看到 lock free 编程使用内存栅栏,但没有锁。我了解 AMD64 具有非常强大的内存模型,但过时的值可以保留在缓存中。如果lock 的行为与mfence 的行为不同,那么互斥锁如何帮助您查看最新值?

【问题讨论】:

  • 可能与以下内容重复:stackoverflow.com/questions/9027590/…
  • xchg 包含了锁逻辑,所以 lock / xchg 是多余的。
  • x86 上的锁定原子读取-修改-写入是顺序一致的。 AFAIR、lock add [mem], 0lock or [mem], 0lock and [mem], -1 已在 mfence 速度特别慢的微架构上代替 mfence。诀窍是在缓存中找到保证可访问但未使用的内存位置。我似乎记得用于[mem] 的堆栈指针的偏移量不错。
  • 它们都是完整的内存屏障。没有时间写完整的答案,但请查看x86 tag wiki 中的一些内存排序链接。 MFENCE 还可能暗示一些关于部分序列化指令流的其他语义,而不仅仅是内存,至少在其吞吐量低于 lock add 用作内存屏障的 AMD CPU 上。
  • 更新:我在上次评论中没有考虑 NT 商店。对于无锁算法中的内存排序,mov [shared], eax/mfencexchg [shared], eax 兼容,作为实现shared.store(eax, std::memory_order_seq_cst) 的一种方式。但正如 BeeOnRope 的回答所指出的那样,mfence 的背靠背吞吐量较低表明它正在做一些不同的事情,也许locked 操作员没有围堵 NT 商店。

标签: multithreading assembly x86 cpu-architecture memory-barriers


【解决方案1】:

我相信您的问题与询问 mfence 是否具有与 x86 上的 lock-prefixed 指令相同的屏障语义相同,或者它是否提供了更少的1 或在某些情况下提供了额外的保证案例。

我目前的最佳答案是,这是英特尔的意图,并且 ISA 文档保证 mfencelocked 指令提供相同的屏蔽语义,但由于实施疏忽,@ 987654334@ 实际上在最近的硬件上提供了更强的防护语义(至少从 Haswell 开始)。特别是,mfence 可以从 WC 类型的内存区域隔离后续非临时加载,而locked 指令则不能。

我们知道这一点是因为英特尔在处理器勘误表中告诉我们这一点,例如 HSD162 (Haswell)SKL155 (Skylake),它们告诉我们锁定指令不会屏蔽随后从 WC 内存进行的非临时读取:

来自 WC 内存的 MOVNTDQA 可能会传递较早的锁定指令

问题:从 WC(写入组合)内存加载的 (V)MOVNTDQA(流式加载指令)的执行可能会通过 访问不同缓存行的较早锁定指令。

含义:期望锁定来隔离后续 (V)MOVNTDQA 指令的软件可能无法正常运行。

解决方法:未发现。依赖锁定指令来隔离 (V)MOVNTDQA 后续执行的软件 应该在锁定指令之间插入一条 MFENCE 指令 以及随后的 (V)MOVNTDQA 指令。

据此,我们可以确定 (1) 英特尔可能有意锁定指令将 NT 加载到 WC 型内存中,否则这不会是勘误表0.5 sup> 和 (2) 锁定指令不会实际上这样做,英特尔无法或选择不通过微码更新来解决此问题,建议使用 mfence

在 Skylake 中,mfence 实际上失去了针对 NT 负载的额外防护功能,根据 SKL079:来自 WC 内存的 MOVNTDQA 可能通过早期的 MFENCE 指令 - 这与文本几乎相同lock-指令勘误表,但适用于 mfence。但是,此勘误表的状态是“BIOS 可能包含此勘误表的解决方法。”,这通常是英特尔所说的“微码更新解决此问题”。

这一系列勘误表或许可以用时间来解释:Haswell 勘误表仅出现在 2016 年初,也就是该处理器发布几年后,因此我们可以假设该问题在此之前的一段时间内引起了英特尔的注意。在这一点上,Skylake 几乎可以肯定已经在野外了,显然是一个不那么保守的mfence 实现,它也没有在 WC 类型的内存区域上隔离 NT 负载。基于锁定指令的广泛使用,修复锁定指令一直工作到 Haswell 的方式可能是不可能的或昂贵的,但需要某种方法来隔离 NT 负载。 mfence 显然已经在 Haswell 上完成了这项工作,Skylake 将得到修复,以便 mfence 也在那里工作。

这并不能真正解释为什么 SKL079(mfence 一个)出现在 2016 年 1 月,比 SKL155(locked 一个)在 2017 年末出现早了近两年,或者为什么后者在相同之后出现了这么多然而,Haswell 勘误表。

人们可能会猜测英特尔将来会做什么。由于他们无法/不愿意通过 Skylake 更改 Haswell 的 lock 指令,代表数亿(数十亿?)个已部署的芯片,他们将永远无法保证锁定的指令会限制 NT 负载,因此他们可能考虑将其作为未来记录的、结构化的行为。或者他们可能会更新锁定的指令,所以他们会屏蔽这样的读取,但实际上你不能依赖这个可能十年或更长时间,直到具有当前非屏蔽行为的芯片几乎停止流通。

与 Haswell 类似,根据 BV116BJ138,NT 加载可能会分别在 Sandy Bridge 和 Ivy Bridge 上传递较早的锁定指令。早期的微架构也可能会遇到这个问题。在 Skylake 之后的 Broadwell 和微架构中似乎不存在这个“错误”。

Peter Cordes 在 this answer 末尾写了一些关于 Skylake mfence 更改的文章。

这个答案的其余部分是我最初的答案,在我知道勘误表之前,主要是为了历史兴趣。

旧答案

我对答案的明智猜测是,mfence 提供了额外的屏障功能:在使用弱排序指令的访问之间(例如,NT 存储)以及可能在弱排序的区域访问之间(例如, WC 型内存)。

也就是说,这只是一个有根据的猜测,您可以在下面找到我的调查的详细信息。

详情

文档

目前尚不清楚mfence 的内存一致性效果与lock 前缀指令(包括带有隐式锁定的内存操作数的xchg)提供的内存一致性效果的不同程度。

我认为可以肯定地说,仅就回写内存区域而言,不涉及任何非临时访问,mfence 提供与lock-prefixed 操作相同的排序语义。

有待商榷的是mfence 是否与lock-prefixed 指令在涉及上述之外的场景时完全不同,特别是当访问涉及非 WB 区域或非时间(流)时涉及操作。

例如,您可以找到一些建议(例如herehere),mfence 在涉及 WC 类型的操作(例如 NT 存储)时意味着强屏障语义。

例如,在 this thread 中引用 McCalpin 博士的话(强调):

栅栏指令只需要绝对确定所有 非临时存储在随后的“普通”之前可见 店铺。这很重要的最明显情况是平行的 代码,其中并行区域末尾的“障碍”可能包括 一家“普通”的商店。没有栅栏,处理器可能仍然有 写入组合缓冲区中的修改数据,但通过 屏障并允许其他处理器读取 写入组合数据。这种情况也可能适用于单个 操作系统从一个核心迁移到另一个核心的线程(不是 确定这个案例)。

我不记得详细的推理(咖啡还不够这个 早上),但是你想在非临时之后使用的指令 商店是一个MFENCE。 根据第 3 卷第 8.2.5 节 SWDM,MFENCE 是唯一可以防止两者的栅栏指令 后续加载和后续存储不会提前执行 围栏的完成。我很惊讶这不是 11.3.1 节中提到过,它告诉您它的重要性 使用写组合时手动确保连贯性,但不 告诉你怎么做!

让我们看看引用的英特尔 SDM 的第 8.2.5 节:

加强或削弱记忆排序模型

英特尔 64 和 IA-32 架构提供了几种机制来加强或 削弱内存排序模型以处理特殊编程 情况。这些机制包括:

• I/O 指令,锁定 指令、LOCK 前缀和序列化指令强制 处理器上的排序更强。

• SFENCE 指令 (介绍了奔腾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 博士的解释相反2,我认为这部分对于 mfence 是否做了额外的事情有点模棱两可。涉及 IO、锁定指令和序列化指令的三个部分确实意味着它们在操作之前和之后的内存操作之间提供了一个完整的屏障。对于弱序内存,它们没有任何例外,在 IO 指令的情况下,人们还会假设它们需要以与弱序内存区域一致的方式工作,因为弱序内存区域通常用于 IO。

然后是FENCE 指令部分,它明确提到了弱内存区域:“SFENCE、LFENCE 和 MFENCE 指令**提供了一种高效的方式来确保加载和存储内存在产生弱排序结果的例程和使用该数据的例程之间进行排序。”

我们是否从字里行间解读并认为这些是完成此任务的唯一指令,并且前面提到的技术(包括锁定指令)对薄弱的内存区域没有帮助?我们可以通过注意到栅栏指令与弱序非临时存储指令同时引入3 以及类似 11.6.13 中的文本来找到对这一想法的支持Cacheability Hint Instructions 专门处理弱序指令:

数据消费者知道数据弱的程度 对于这些情况,ordered 可能会有所不同。因此,SFENCE 或 MFENCE 指令应用于确保例程之间的顺序 生成弱序数据和消耗数据的例程。科学 和 MFENCE 提供了一种高效的方式来确保按顺序排序 保证 SFENCE/MFENCE 之前的每条存储指令 在存储指令之前,程序顺序是全局可见的 跟着栅栏走。

再次,这里特别提到了栅栏指令,以适用于弱序指令的栅栏。

我们还支持这样一种观点,即锁定指令可能不会在上面已经引用的最后一句话中的弱排序访问之间提供障碍:

请注意,SFENCE、 LFENCE 和 MFENCE 指令提供了一种更有效的方法 控制内存排序而不是 CPUID 指令。

这基本上意味着FENCE 指令实质上替换了之前序列化cpuid 在内存排序方面提供的功能。然而,如果lock-prefixed 指令提供与cpuid 相同的屏障能力,那可能是先前建议的方式,因为这些通常比cpuid 快得多,后者通常需要200 个或更多周期。这意味着存在lock-prefixed 指令无法处理的场景(可能是弱排序场景),在哪里使用cpuid,现在建议在哪里使用mfence 作为替代,这意味着更强的屏障语义比lock-前缀指令。

但是,我们可以用不同的方式解释上述某些内容:请注意,在栅栏指令的上下文中,经常提到它们是性能高效的方式,以确保排序。因此,这些说明可能并非旨在提供额外的屏障,而只是提供更有效的屏障。

确实,sfence 在几个周期内比序列化指令(如 cpuidlock 前缀指令通常需要 20 个或更多周期)快得多。另一方面,mfence 并不通常比锁定指令快4,至少在现代硬件上是这样。尽管如此,它在引入时可能会更快,或者在某些未来的设计中,或者也许 预计会更快,但并没有成功。

因此,我无法根据手册的这些部分做出某种评估:我认为您可以提出合理的论点,即它可以以任何一种方式解释。

我们可以在英特尔 ISA 指南中进一步查看各种非临时存储指令的文档。例如,在非临时存储 movnti 的文档中,您可以找到以下引用:

因为 WC 协议使用弱序内存一致性 模型,使用 SFENCE 或 MFENCE 实现的围栏操作 指令应与 MOVNTI 指令结合使用,如果 多个处理器可能使用不同的内存类型来读/写 目标内存位置。

关于“如果多个处理器可能使用不同的内存类型来读/写目标内存位置”的部分让我有点困惑。我希望这更像是“使用弱排序提示在指令之间强制执行全局可见写入顺序的排序”或类似的东西。实际上,实际的内存类型(例如,由 MTTR 定义)可能甚至不会在这里发挥作用:当使用弱排序指令时,排序问题可能仅出现在 WB 内存中。

性能

据报道,mfence 指令在现代 CPU 上基于 Agner fog 的指令时序需要 33 个周期(背靠背延迟),但据报道,像 lock cmpxchg 这样的更复杂的锁定指令仅需要 18 个周期。

如果mfence 提供的屏障语义不比lock cmpxchg 强,则后者正在做更多的工作,mfence 没有明显的理由花费显着更长。当然你可以说lock cmpxchgmfence 更重要,因此得到了更多的优化。 all 的锁定指令比mfence 快得多,即使是不经常使用的指令也削弱了这一论点。此外,您可以想象,如果所有lock 指令共享一个屏障实现,mfence 将简单地使用相同的屏障实现,因为这是最简单和最容易验证的。

因此,在我看来,mfence 较慢的性能是mfence 正在做一些额外的重要证据。


0.5 这不是一个无懈可击的论点。有些东西可能出现在勘误表中,显然是“设计使然”而不是错误,例如popcnt 对目标寄存器的错误依赖——因此一些勘误表可以被视为一种更新预期的文档形式,而不是总是暗示硬件错误。

1 显然,lock-prefixed 指令执行一个原子操作,这是不可能单独使用 mfence 实现的,所以 lock -前缀指令肯定有额外的功能。因此,为了使mfence 有用,我们希望它在某些场景中具有额外的屏障语义,性能更好。

2也完全有可能他正在阅读不同版本的手册,其中散文不同。

3SSE 中的SFENCE、SSE2 中的lfencemfence

4 而且通常速度较慢:Agner 列出了在最近的硬件上的延迟为 33 个周期,而锁定指令通常约为 20 个周期。

【讨论】:

  • 在 Skylake 上,xchg [shared], eax 是 NT 商店的障碍。使用此代码进行测试,该代码填充缓冲区并将每个高速缓存行的当前输出位置存储到共享变量(mfence+)movxchggodbolt.org/g/7Q9xgz(某些计时结果在 cmets 中,来自 ocperf.py整个事情,所以时间包括mmap(MAP_POPULATE)的时间)。只有mov 而不是mfence,我们会重新排序。但是mfence+mov 可以,xchg 也可以。两个生产者的消费者循环的速度有很大不同,因此存在一些重大差异。
  • 这不排除 locked 指令不屏蔽 movntdqa 从 WC 内存加载;我想我已经看到有人声称mfence(不仅仅是lfence)在那里是必要的。与读取时旋转的消费者线程交互时的差异很有趣,需要进一步调查(也许有一些东西可以分别描述生产者和消费者,并且不计算mmap(MAP_POPULATE) ~4GiB RAM 的时间。此外,在 AMD 上进行测试CPU 会很有趣;纸上的 x86 文档似乎模棱两可,所以 xchg 是英特尔的障碍这一事实并不能告诉我们它们的意思。
  • 顺便说一句,我使用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 和 DDR4-2666 上的 Arch Linux 上使用 gcc7.3.0,CPU 调控器在大部分测试中以 ~3.8GHz 运行它)。
  • 感谢@PeterCordes,我在待办事项清单上已经有一段时间来运行你的测试了,但是现在这个勘误信息已经曝光,我认为我们可以说这很可能是locked 指令旨在并且实际上以通常的方式隔离 NT 存储,因为我们有 NT 加载勘误表,并且 NT 存储到 WB 内存是一个或两个数量级更常见并且分布在各种代码中,因此可能会注意到存在差异(并且负载行为值得勘误的事实意味着我们可以理解英特尔可能打算将lock 围起来)。
  • @Peter:是的,x86 上的原子 RMW 与 C++11 memory_order_seq_cst 一样强大,因此它们包括获取和释放。您只需要一个普通的mov 存储和一个普通的mov 在x86 上加载即可获得发布指向其他线程的指针并让它们看到指向的数据所需的同步量。 (C++ ...release...acquire)。但是,如果您出于某些其他原因需要编写器和读取器中的原子 RMW,那自动就足够了。这个答案已经涵盖了。
猜你喜欢
  • 2012-02-20
  • 2019-01-29
  • 2013-10-06
  • 2011-03-09
  • 2014-09-14
  • 2011-07-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多