【问题标题】:C++ How is release-and-acquire achieved on x86 only using MOV?C++ 如何仅使用 MOV 在 x86 上实现释放和获取?
【发布时间】:2020-06-04 10:32:01
【问题描述】:

这个问题是对此的后续/澄清:

Does the MOV x86 instruction implement a C++11 memory_order_release atomic store?

这表明MOV 汇编指令足以在 x86 上执行获取-释放语义。我们不需要LOCK、栅栏或xchg 等。但是,我很难理解它是如何工作的。

英特尔文档第 3A 卷第 8 章指出:

https://software.intel.com/sites/default/files/managed/7c/f1/253668-sdm-vol-3a.pdf

在单处理器(核心)系统中......

  • 读取不会与其他读取重新排序。
  • 写入不会与较旧的读取一起重新排序。
  • 对内存的写入不会与其他写入一起重新排序,但以下情况除外:

但这是针对单核的。多核部分似乎没有提到负载是如何执行的:

在多处理器系统中,以下排序原则适用:

  • 单个处理器使用与单处理器系统相同的排序原则。
  • 所有处理器以相同的顺序观察单个处理器的写入。
  • 来自单个处理器的写入不会相对于来自其他处理器的写入进行排序。
  • 内存排序遵循因​​果关系(内存排序尊重传递可见性)。
  • 除了执行存储的处理者之外,任何两个存储都以一致的顺序被处理器看到
  • 锁定指令有一个总顺序。

那么 MOV 一个人怎么能促进获取-释放呢?

【问题讨论】:

  • MOV 本身不是顺序一致而不是设置rel-acq 栅栏吗?因为它只会在非常有限的条件下重新排序。这让我想起了很久以前 Herb Sutter 对 SC-DRF 记忆模型的非常有见地的介绍。
  • @DeanSeo:不,x86 的硬件内存模型是 SC + 带有存储转发的存储缓冲区。这就像 acq_rel,而不是 SC。
  • @PeterCordes 有趣!谢谢指正!

标签: c++ x86 memory-barriers memory-model stdatomic


【解决方案1】:

但这是针对单核的。多核部分似乎没有提到负载是如何执行的:

该部分的第一个要点是关键:单个处理器使用与单处理器系统相同的排序原则。该语句的隐含部分是 ... 加载时/storing from cache-coherent shared memory。即多处理器系统不会引入重新排序的新方法,它们只是意味着可能的观察者现在将代码包含在其他内核上,而不仅仅是 DMA / IO 设备。

对共享内存的访问重新排序的模型是单核模型,即程序顺序 + 存储缓冲区 = 基本上是 acq_rel。其实比acq_rel稍微强一点,没关系。

发生的唯一重新排序是本地,在每个 CPU 内核中。一旦一个 store 变得全局可见,它就会同时对所有其他核心可见,并且在此之前对任何核心都不可见。 (除了通过存储转发进行存储的核心。)这就是为什么只有本地屏障就足以在 SC + 存储缓冲区模型之上恢复顺序一致性的原因。 (对于 x86,只需 mo_seq_cst 在 SC 存储之后需要 mfence,以便在执行任何进一步加载之前耗尽存储缓冲区。 mfencelocked 指令(它们也是完整的屏障)不必打扰其他内核,只需等待这个即可。

要理解的一个关键点是一致所有处理器共享的内存共享视图(通过一致缓存)。 英特尔 SDM 第 8 章的最开头部分定义了这种背景:

这些多处理机制具有以下特点:

  • 保持系统内存一致性 — 当两个或多个处理器同时尝试 访问系统内存中的相同地址,某种通信机制或内存访问协议 必须可用于促进数据一致性,并且在某些情况下,允许一个处理器临时锁定 内存位置。
  • 保持缓存一致性 — 当一个处理器访问缓存在另一个处理器上的数据时,它不能 收到不正确的数据。如果它修改了数据,访问该数据的所有其他处理器必须接收修改后的数据 数据。
  • 为了允许可预测的内存写入顺序 - 在某些情况下,内存写入很重要 以与编程完全相同的顺序从外部观察。
  • [...]

Intel 64 和 IA-32 处理器的缓存机制和缓存一致性将在第 11 章讨论。

(CPU 使用MESI 的一些变体;Intel 在实践中使用 MESIF,AMD 在实践中使用 MOESI。)

同一章还包括一些有助于说明/定义内存模型的试金石。您引用的部分实际上并不是内存模型的严格正式定义。但是8.2.3.2 加载和存储都不会使用类似操作重新排序 部分显示加载不会使用加载重新排序。另一部分还显示LoadStore reordering 被禁止。 Acq_rel 基本上阻止了除 StoreLoad 之外的所有重新排序,这就是 x86 所做的。 (https://preshing.com/20120913/acquire-and-release-semantics/https://preshing.com/20120930/weak-vs-strong-memory-models/

相关:


其他 ISA

一般来说,大多数较弱的内存硬件模型也只允许本地重新排序,因此屏障仍然仅在 CPU 内核中是本地的,只是让该内核(部分)等待某些条件。 (例如,x86 mfence 阻止稍后加载和存储执行,直到存储缓冲区耗尽。其他 ISA 也受益于轻量级障碍,以提高 x86 在每个内存操作之间强制执行的内容的效率,例如阻止 LoadLoad 和 LoadStore 重新排序。https://preshing.com/20120930/weak-vs-strong-memory-models/

一些 ISA(目前只有 PowerPC)允许 store 在对所有人可见之前对其他一些内核可见,allowing IRIW reordering。请注意,C++ 中的 mo_acq_rel 允许 IRIW 重新排序;只有seq_cst 禁止它。大多数 HW 内存模型比 ISO C++ 稍强一些,因此无法实现,因此所有内核都同意存储的全局顺序。

【讨论】:

  • @user997112:我在 x86 上的顺序一致性(SC aka seq_cst)所需的上下文中提到了mfence。我提到它是为了指出 mfence 所做的一切都是本地的,在执行它的核心内。感谢您指出我的解释方式可能造成的混乱,我现在明白了;已更新。
  • @user997112:嗯?不,acq-rel 是关于相对于这个加载/存储的其他加载/存储的排序。例如写一个大缓冲区,然后data_ready.store(true, mo_release);。执行data_ready.load(mo_acquire) 并看到true 的读取器可以安全地读取缓冲区,即使缓冲区是非原子的。如果您只有一个 64 位共享变量,则不需要对其他任何内容进行任何排序,只需 mo_relaxed 为那个无锁变量。
  • @user997112:除了 mfence? SFENCE 的用例仅适用于您使用弱排序的 NT 存储并希望使用“data-ready=true”“释放”它们的情况。 LFENCE 的用例基本上不存在。英特尔可能已经计划引入弱排序负载,但从未这样做(来自 WC 内存的 SSE4.1 movntdqa 除外,如视频 RAM)。 When should I use _mm_sfence _mm_lfence and _mm_mfence。当然,通常你自己不会手动使用屏障,而是让编译器为使用 std::atomic<> 的源发出它们。
  • @user997112:当您不需要太多排序时,可以获得比 seq_cst 更高的性能。 mov + mfence(或xchg)非常慢。获取和释放在运行时是免费的,但放松可以允许围绕原子对其他操作进行编译时优化。 (x86 上的原子 RMW 操作始终是一个完整的障碍; seq_cst 纯存储是昂贵的东西。)通常,为了获得最大性能,严格需要使用弱顺序。一般来说,为了最大限度地避免设计错误,只需使用默认的 seq_cst,尤其是当您无法在弱 ISA 上实际测试代码时。
  • @user997112:哦。 preshing.com/20120515/memory-reordering-caught-in-the-act。存储时需要 seq_cst,然后想要加载并查看其他线程可能看到/已经看到的内容。是的,编译时重新排序必须尊重 ISO C++ 内存模型(对于它们不同的情况,不是硬件内存模型,例如,可以在编译时重新排序宽松的存储,或者获取加载只能在编译时在一个方向重新排序时间,相对于宽松和非原子操作。即使在为 x86 编译时,在 asm 中,一切都是获取负载。)
【解决方案2】:

刷新获取和释放的语义(引用 cppreference 而不是标准,因为这是我手头的 - 标准更...详细,这里):

memory_order_acquire:具有此内存顺序的加载操作对受影响的内存位置执行获取操作:在此加载之前,当前线程中的任何读取或写入都不能重新排序。释放相同原子变量的其他线程中的所有写入在当前线程中可见

memory_order_release:具有此内存顺序的存储操作执行释放操作:在此存储之后,当前线程中的任何读取或写入都不能重新排序。当前线程中的所有写入在获取相同原子变量的其他线程中可见

这给了我们四点保证:

  • acquire ordering:“当前线程中的读取或写入在此加载之前不能重新排序”
  • 发布顺序:“当前线程中的读取或写入不能在此存储之后重新排序”
  • 获取-释放同步:
    • “释放相同原子变量的其他线程中的所有写入在当前线程中都是可见的”
    • “当前线程中的所有写入在获取相同原子变量的其他线程中都是可见的”

审查保证:

  • 读取不会与其他读取重新排序。
  • 写入不会与较旧的读取一起重新排序。
  • 对内存的写入不会与其他写入重新排序 [..]
  • 单个处理器使用与单处理器系统相同的排序原则。

这足以满足订购保证。

对于获取顺序,请考虑发生了原子读取:对于该线程,显然任何以后的读取或写入迁移之前都会分别违反第一个或第二个要点。

对于发布顺序,请考虑发生了原子写入:对于该线程,显然任何先前的读取或写入迁移之后将分别违反第二个或第三个要点。

剩下的唯一事情是确保如果一个线程读取一个已释放的存储,它将看到编写器线程在该点之前产生的所有其他负载。这就是需要其他多处理器保证的地方。


  • 所有处理器以相同的顺序观察单个处理器的写入。

这足以满足获取-释放同步。

我们已经确定,当发布写入发生时,之前的所有其他写入也会发生。然后,此要点确保如果另一个线程读取已释放的写入,它将读取写入器在该点之前产生的所有写入。 (如果不是,那么它将观察到单个处理器的写入顺序与单个处理器不同,这违反了要点。)

【讨论】:

    猜你喜欢
    • 2015-06-13
    • 2020-01-23
    • 2015-07-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-19
    • 2010-09-16
    相关资源
    最近更新 更多