17

来自关于 C++ 原子类型和操作的C++0x 提案:

29.1 顺序和一致性 [atomics.order]

添加带有以下段落的新子条款。

枚举memory_order指定详细的常规(非原子)内存同步顺序,如[N2334 或其采用的后续程序添加的新部分] 中定义的,并且可以提供操作顺序。其枚举值及其含义如下。

  • memory_order_relaxed

该操作不对内存进行排序。

  • memory_order_release

对受影响的内存位置执行释放操作,从而通过应用它的原子变量使常规内存写入对其他线程可见。

  • memory_order_acquire

对受影响的内存位置执行获取操作,从而使通过应用它的原子变量释放的其他线程中的常规内存写入对当前线程可见。

  • memory_order_acq_rel

该操作具有获取和释放语义。

  • memory_order_seq_cst

该操作具有获取和释放语义,此外,还具有顺序一致的操作顺序。

提案中较低的:

bool A::compare_swap( C& expected, C desired,
        memory_order success, memory_order failure ) volatile

可以指定 CAS 的内存顺序。


我的理解是“<code>memory_order_acq_rel”只会同步操作所需的那些内存位置,而其他内存位置可能保持不同步(它不会充当内存围栏)。

现在,我的问题是 - 如果我选择“<code>memory_order_acq_rel”并应用于compare_swap整数类型,例如整数,这通常如何转换为现代消费处理器(如多核 Intel i7)上的机器代码?其他常用的架构(x64、SPARC、ppc、arm)呢?

特别是(假设一个具体的编译器,比如 gcc):

  1. 如何使用上述操作比较和交换整数位置?
  2. 这样的代码会产生什么指令序列?
  3. i7的操作是无锁的吗?
  4. 这样的操作会运行完整的缓存一致性协议,同步不同处理器内核的缓存,就好像它是 i7 上的内存栅栏一样?还是只会同步此操作所需的内存位置?
  5. acq_rel与上一个问题相关 -在 i7 上使用语义是否有任何性能优势?其他架构呢?

感谢所有的答案。

4

2 回答 2

8

这里的答案并非微不足道。究竟发生了什么以及意味着什么取决于许多事情。对于缓存一致性/内存的基本理解,也许我最近的博客文章可能会有所帮助:

但除此之外,让我试着回答几个问题。首先,下面的函数对支持的内容非常有希望:非常细粒度地​​控制您获得的内存顺序保证到底有多强。这对于编译时重新排序是合理的,但通常不适用于运行时障碍。

compare_swap( C& expected, C desired,
        memory_order success, memory_order failure )

架构将无法完全按照您的要求实现这一点;许多人将不得不将其加强到足以让他们能够实施的程度。当您指定 memory_order 时,您正在指定重新排序的工作方式。要使用英特尔的术语,您将指定您想要的栅栏类型,共有三个栅栏,完整栅栏、装载栅栏和存储栅栏。(但在 x86 上,加载栅栏和存储栅栏仅对 NT 存储等弱序指令有用;原子不使用它们。常规加载/存储为您提供一切,除了存储可以在以后加载后出现。)只是因为你想要该操作上的特定围栏并不意味着它受到支持,我希望它总是回落到完整的围栏。(参见Preshing关于内存屏障的文章)

x86(包括 x64)编译器可能会使用该LOCK CMPXCHG指令来实现 CAS,而不管内存顺序如何。这意味着一个完整的障碍;x86 没有办法使没有前缀的读-修改-写操作原子化lock,这也是一个完整的障碍。Pure-store 和 pure-load 可以“独立”是原子的,许多 ISA 需要针对上述任何内容的障碍mo_relaxed,但x86acq_rel在 asm 中“免费”

该指令是无锁的,尽管所有尝试对同一位置进行 CAS 处理的内核都会争用对它的访问,因此您可能会认为它并不是真正的无等待。(使用它的算法可能不是无锁的,但操作本身是无等待的,请参阅维基百科的非阻塞算法文章)。在使用LL/SC而不是locked 指令的非 x86 上, C++11compare_exchange_weak通常是无等待的,但compare_exchange_strong在出现虚假失败的情况下需要重试循环。

现在 C++11 已经存在多年,您可以在Godbolt 编译器资源管理器上查看各种架构的 asm 输出。


在内存同步方面,您需要了解缓存一致性的工作原理(我的博客可能会有所帮助)。新 CPU 使用 ccNUMA 架构(以前称为 SMP)。本质上,内存上的“视图”永远不会不同步。代码中使用的栅栏实际上并不强制任何缓存刷新本身发生,只是存储缓冲区在飞行存储中提交到缓存,然后再加载。

如果两个内核都在缓存行中缓存了相同的内存位置,则一个内核的存储将获得缓存行的独占所有权(使所有其他副本无效)并将其自己的副本标记为脏。一个非常复杂的过程的一个非常简单的解释

要回答您的最后一个问题,您应该始终使用逻辑上需要正确的内存语义。大多数架构不支持您在程序中使用的所有组合。但是,在许多情况下,您会得到很好的优化,特别是在您请求的订单得到保证而没有围栏的情况下(这很常见)。

-- 对一些评论的回答:

您必须区分执行写入指令和写入内存位置的含义。这就是我试图在我的博客文章中解释的内容。当“0”被提交到 0x100 时,所有内核都看到该零。写入整数也是原子的,即使没有锁,当您写入一个位置时,如果所有内核希望使用它,它们都会立即拥有该值。

问题是要使用您可能首先将其加载到寄存器中的值,之后对位置的任何更改显然都不会触及寄存器。这就是为什么需要互斥体或atomic<T>尽管有缓存一致的内存:允许编译器将普通变量值保存在私有寄存器中。(在 C++11 中,这是因为非atomic变量上的数据竞争是未定义行为。)

至于相互矛盾的说法,通常你会看到各种各样的说法。它们是否矛盾取决于上下文中“看到”“加载”“执行”的确切含义。如果您将“1”写入 0x100,这是否意味着您执行了写入指令或 CPU 是否实际提交了该值。存储缓冲区产生的差异是重新排序的主要原因之一(x86 唯一允许的)。CPU 可以延迟写入“1”,但您可以确定它最终提交“1”的那一刻,所有内核都能看到它。栅栏通过使线程等到存储提交后再执行后续操作来控制此顺序。

于 2010-11-18T11:34:32.967 回答
1

您的整个世界观似乎都偏离了基础:您的问题暗示缓存一致性由 C++ 级别的内存顺序和 CPU 级别的栅栏或原子操作控制。

但缓存一致性是物理架构最重要的不变量之一,它始终由所有 CPU 和 RAM 互连的内存系统提供。您永远无法从 CPU 上运行的代码中击败它,甚至无法查看其操作细节。当然,通过直接观察 RAM 并在其他地方运行代码,您可能会在某个内存级别看到陈旧数据:根据定义,RAM 并不具有所有内存位置的最新值。

但是在 CPU 上运行的代码不能直接访问 DRAM,只能通过包含相互通信的缓存的内存层次结构来访问内存共享视图的一致性。(通常与 MESI 一起使用)。即使在单个内核上,回写式缓存也会让 DRAM 值过时,这对于非缓存一致的 DMA 可能是一个问题,但对于从 CPU 读取/写入内存则不会。

所以这个问题只存在于外部设备上,而且只存在于那些做非相干 DMA 的设备上。(DMA 在现代 x86 CPU 上是缓存一致的;内置于 CPU 的内存控制器使这成为可能)。

这样的操作会运行完整的缓存一致性协议,同步不同处理器内核的缓存,就好像它是 i7 上的内存栅栏一样?

它们已经同步。请参阅内存屏障是否确保缓存一致性已完成?- 内存屏障仅在运行屏障的核心内部执行本地操作,例如刷新存储缓冲区。

还是只会同步此操作所需的内存位置?

原子操作仅适用于一个内存位置。您还想到哪些其他地点?

在弱排序 CPU 上,memory_order_relaxed原子增量可以避免在该增量之前使较早的加载/存储可见。但是 x86 的强序内存模型不允许这样做。

于 2019-12-08T21:37:16.653 回答