比较和交换 C++0x

Posted

技术标签:

【中文标题】比较和交换 C++0x【英文标题】:Compare and swap C++0x 【发布时间】:2011-05-11 22:36:15 【问题描述】:

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

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 指定内存顺序。


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

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

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

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

感谢大家的回答。

【问题讨论】:

"来自 C++0x 关于 C++ 原子类型和操作的提案:" 你引用的文字是一个非常非常糟糕的解释。 【参考方案1】:

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

CPU Reordering – What is actually being reordered? CPU Memory – Why do I need a mutex?

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

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

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

x86(包括 x64)编译器可能会使用 LOCK CMPXCHG 指令来实现 CAS,而不管内存顺序如何。这意味着一个完整的障碍; x86 没有make a read-modify-write operation atomic 没有lock 前缀的方法,这也是一个完整的障碍。 Pure-store 和 pure-load 可以“独立”是原子的,许多 ISA 需要为高于 mo_relaxed 和 x86 does acq_rel "for free" in asm 的任何内容设置障碍。

该指令是无锁的,尽管所有尝试对同一位置进行 CAS 处理的内核都会争用对其的访问权限,因此您可能会争辩说它并不是真正的无等待。 (使用它的算法可能不是无锁的,但操作本身是无等待的,see wikipedia's non-blocking algorithm article)。在使用LL/SC 而不是locked 指令的非x86 上,C++11 compare_exchange_weak 通常无需等待,但compare_exchange_strong 需要重试循环以防出现虚假故障。

既然 C++11 已经存在多年,您可以查看各种架构的 asm 输出 on the Godbolt compiler explorer。


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

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

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

-- 对一些 cmets 的回答:

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

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

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

【讨论】:

我还应该补充一点,通常不会使用明确的围栏指令。 “锁定”语义、某些函数的隐式锁定和顺序保证通常就足够了。 感谢您的详细解答。 1)我对锁最关心的是线程抢占。由于lock cmpxchg 中的lock 并不是真正的锁,而是语义注释,因此lock cmpxchg“立即”执行。 2) 让我烦恼的下一件事是lock cmpxchg 实际上将缓冲区刷新到内存 - 从您在博客上所说和写的内容来看,这不会发生在新 CPU 上。 3) 此外,在我看来,当 2 个内核在 2 个独立的、遥远的内存位置上执行原子操作时,争用较少,因为没有重新加载。它是否正确?不错的博文,顺便说一句。 您写道:“如果两个内核都在缓存行中缓存了相同的内存位置,则一个将被标记为脏,另一个将根据需要重新加载。”,以及您博客中的类似内容.另一方面,在这个问题***.com/questions/4213639/… 中,用户声称:“但是,如果 A 对地址 0x100 进行普通写入“0”,则 B 向 0x100 写入“1”,然后他们都在地址 0x200 上进行 C&S -- 之后他们都会在 0x200 看到相同的值,但 A 可能仍然认为 0x100 包含“0”。这两种说法不矛盾吗? 最后一条评论,当然假设,说缓存行重新加载发生,你的意思是它发生在普通的加载和存储中,而不是那些标记为原子的。 大部分基本操作不会刷新到内存。除非你明确告诉 CPU 这样做,否则它通常不会刷新到内存,直到它是这样做的好时机——这不太可能干扰你的程序。【参考方案2】:

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

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

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

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

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

它们已经同步。请参阅Does a memory barrier ensure that the cache coherence has been completed? - 内存屏障仅在运行屏障的核心内部执行本地操作,例如刷新存储缓冲区。

或者它只是同步内存位置 此操作需要?

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

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

【讨论】:

以上是关于比较和交换 C++0x的主要内容,如果未能解决你的问题,请参考以下文章

MAC地址比较大小?

Ruby - 比较数组和交换索引

自己写一个swap函数交换任意两个相同类型元素的值 对空指针的使用 字节大小的判断了解原理

求问数组从小到大排序最少交换次数的题目怎么做比较好?

冒泡排序法

路由器和交换机命令