C++ 如何仅使用 MOV 在 x86 上实现释放和获取?

use*_*112 8 c++ x86 memory-model memory-barriers stdatomic

这个问题是对此的跟进/澄清:

MOV x86 指令是否实现了 C++11 memory_order_release 原子存储?

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

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

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

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

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

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

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

  • 单个处理器使用与单处理器系统相同的排序原则。
  • 所有处理器都以相同的顺序观察单个处理器的写入。
  • 来自单个处理器的写入与来自其他处理器的写入无关。
  • 记忆排序服从因果关系(记忆排序尊重传递可见性)。
  • 除了执行存储的处理器之外的处理器以一致的顺序看到任何两个存储
  • 锁定指令具有总顺序。

那么如何才能MOV单独促进获取释放呢?

Pet*_*des 6

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

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

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

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

一个关键点理解的是,有一个相干的存储器的所有处理器共享的共享视图(通过相干高速缓存)。 英特尔 SDM 第 8 章的最顶部定义了一些这样的背景:

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

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

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

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

同一章还包括一些有助于说明/定义内存模型的试金石。您引用的部分实际上并不是内存模型的严格正式定义。但是第8.2.3.2既不加载也不使用类似操作重新排序存储显示加载不使用加载重新排序。另一部分还显示禁止LoadStore 重新排序。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)允许存储在对所有人可见之前对其他一些内核可见,从而允许 IRIW 重新排序。请注意,mo_acq_rel在 C++ 中允许 IRIW 重新排序;只seq_cst禁止它。大多数 HW 内存模型比 ISO C++ 稍强,因此无法实现,因此所有内核都同意全局存储顺序。

  • @user997112:当您不需要那么多排序时,可以获得比 seq_cst 更高的性能。`mov` + `mfence` (或 `xchg`)非常慢。获取和释放在运行时是免费的,但宽松可以允许围绕原子的其他操作进行编译时优化。(x86 上的原子 RMW 操作始终是一个完整的障碍;seq_cst 纯存储是昂贵的东西。)一般来说,为了获得最大性能,请严格使用弱顺序。一般来说,为了最大限度地防止设计错误,只需使用默认的 seq_cst,特别是当您无法在弱 ISA 上实际测试代码时。 (2认同)

GMa*_*ckG 5

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

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

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

这给了我们四点保证:

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

审查担保:

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

这足以满足订购保证。

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

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

剩下的唯一一件事是确保如果一个线程读取已发布的存储,它将看到编写器线程到目前为止已生成的所有其他加载。这就是需要其他多处理器保证的地方。


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

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

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


归档时间:

查看次数:

800 次

最近记录:

5 年,11 月 前