memory_order_consume 到底是做什么的?

What does memory_order_consume really do?

来自 link:

这个回答让我印象深刻:

Using an atomic variable solves the problem - by using atomics all threads are guarantees to read the latest writen-value even if the memory order is relaxed.

今天,我阅读了下面的 link: https://preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/

atomic<int*> Guard(nullptr);
int Payload = 0;

线程 1:

  Payload = 42;
    Guard.store(&Payload, memory_order_release);

thred2:

g = Guard.load(memory_order_consume);
if (g != nullptr)
    p = *g;

问题: 我了解到 Data Dependency 会阻止相关指令被重新排序。 但我认为这对于确保执行结果的正确性是显而易见的。 comsume-release 语义是否存在并不重要。 所以我想知道 comsume-release 真的有。哦,也许它使用数据依赖性来防止指令重新排序,同时 确保有效负载的可见性?

所以

如果我重新排序 1.preventing 指令,是否有可能使用 memory_order_relaxed 获得相同的正确结果 2.ensuring 有效载荷的非原子变量的可见性 :

atomic<int*> Guard(nullptr);
volatile int Payload = 0;   // 1.Payload is volatile now

// 2.Payload.assign and Guard.store in order for data dependency
Payload = 42;               
Guard.store(&Payload, memory_order_release);

// 3.data Dependency make w/r of g/p in order
g = Guard.load(memory_order_relaxed);  
if (g != nullptr)
    p = *g;      // 4. For 1,2,3 there are no reorder, and here, volatile Payload make the value of 42 is visable.

附加内容(由于 Sneftel 的回答):

1.Payload = 42; volatile 使 W/R of Payload to/from main memory but not to/from cache.So 42 will write to memory.

2.Guard.store(&Payload, 任何MO flag都可以写);正如你所说,守卫是非易失性的,但是是原子的

Using an atomic variable solves the problem - by using atomics all threads are guarantees to read the latest writen-value even if the memory order is relaxed.

In fact, atomics are always thread safe, regardless of the memory order! The memory order is not for the atomics -> it's for non atomic data.

所以在Guard.store执行后,Guard.load(有任何MO标志都可以读取)可以正确获取Payload的地址。然后从内存中正确获取42。

以上代码:

1.no 数据依赖的重新排序效果。

2.no 可变负载的缓存效果

3.no 原子防护的线程安全问题

我能得到正确的值 - 42 吗?

回到正题

When you use consume semantics, you’re basically trying to make the compiler exploit data dependencies on all those processor families. That’s why, in general, it’s not enough to simply change memory_order_acquire to memory_order_consume. You must also make sure there are data dependency chains at the C++ source code level.

" 您还必须确保在 C++ 源代码级别存在数据依赖链。"

我认为 C++ 源代码级别的数据依赖链会阻止指令自然地重新排序。 那么 memory_order_consume 到底做了什么?

我可以使用 memory_order_relaxed 来获得与上述代码相同的结果吗?

补充内容结束

问题是,答案并不完全正确,因为存在一些细微差别。

Using an atomic variable solves the problem - by using atomics all threads are guarantees to read the latest writen-value even if the memory order is relaxed.

他们确实读取了“最新写入的值”,但是随着内存顺序的“放松”,指令的顺序可以重新排列。

因此,如果您说先写 DoSomething(); x = y.load(relaxed);,然后 post 编译,则松弛加载可能会先于 DoSomething(); 排序。假设例程花费了相当长的时间,那么 x 的值可能与 y 的最新值相差很大。

内存顺序“消耗”禁止指令重排,因此不会发生此类问题。

  1. volatile与c/c++中的多线程无关,其顺序可见性副作用仅出现在单线程程序中,通常仅用于告诉编译器不要优化这个值。它不同于 Java/C#。

  2. release/consume都是数据依赖,它可能会建立一个依赖链(可以通过kill_dependency打破,以避免以后不必要的障碍)。

  3. release/acquire 形成成对的 synchronize-with/inter-thread happens-before 关系。

对于您的情况,release/acquire 将形成预期的 happens-before 关系。 release/consume 也可以工作,因为 *g 依赖于 g.

但是请注意,对于当前的编译器,consume 被视为 acquire 的同义词,因为事实证明它很难有效地实现。

首先,ISO C++ 委员会暂时不鼓励 memory_order_consume,直到他们提出编译器可以实际实现的东西。几年来,编译器将 consume 视为 acquire 的同义词。 请参阅此答案底部的部分。

硬件仍然提供数据依赖性,所以谈论这个很有趣,尽管目前没有任何安全可移植的 ISO C++ 方法可以利用。 (仅使用 mo_relaxed 或 hand-rolled 原子进行黑客攻击,并根据对编译器优化和 asm 的理解进行仔细编码,有点像您尝试使用 relaxed 进行的操作。但您不需要 volatile。)

Oh, maybe it uses data dependencies to prevent reordering of instructions while ensuring the visibility of Payload?

不完全是“指令的重新排序”,而是内存重新排序。正如您所说,在这种情况下,理智和因果关系就足够了如果硬件提供依赖排序。 C++ 可移植到不支持的机器。 (例如 DEC Alpha。)

获取 Payload 可见性的正常方法是通过编写器中的 release-store,在 reader 中获取负载,它从 release-store。 https://preshing.com/20120913/acquire-and-release-semantics/。 (因此,当然,将相同的值重复存储到“ready_flag”或指针不会让 reader 弄清楚它看到的是新存储还是旧存储。)

Release/acquire 在线程之间创建了一个 happens-before 同步关系,这保证了作者在 release-store 之前所做的一切的可见性。 (消费没有,这就是为什么只订购依赖负载。)

consume 是对此的优化:通过让编译器利用硬件保证来避免 reader 中的内存屏障,只要您遵循一些依赖性规则。)


您对 CPU 缓存是什么以及 volatile 做了什么有一些误解,我在问题下评论过。 release-store 确保较早的 non-atomic 赋值在内存中可见。

(此外,缓存是连贯的;它为所有 CPU 提供了他们可以同意的共享内存视图。寄存器是 thread-private 而不是连贯的, 那是 当人们说一个值被“缓存”时,他们的意思是什么。寄存器不是 CPU 缓存,但软件可以使用它们来保存内存中某些内容的副本。When to use volatile with multi threading? - never, but it does have some effects in real CPUs because they have coherent cache. It's a bad way to roll your own mo_relaxed. See also https://software.rajivprab.com/2018/04/29/myths-programmers-believe-about-cpu-caches/)

在实际 CPUs 上的实践中,内存重新排序发生在每个内核的本地;缓存本身是连贯的,永远不会“不同步”。 (其他副本在 之前 商店可以变得全局可见之前无效)。所以 release 只需要确保本地 CPUs 存储以正确的顺序变得全局可见(提交到 L1d 缓存)。 ISO C++ 没有指定任何那种级别的细节,并且假设可能有一个工作方式非常不同的实现。

在实践中使作者的存储变得易变是无关紧要的,因为 non-atomic 赋值后跟 release-store 已经必须使所有内容对可能执行 acquire-load 和同步的其他线程可见与那个发行商店。它在纯 ISO C++ 中与纸面无关,因为它不会避免 data-race UB。

(当然,理论上 whole-program 优化可以看到没有任何获取或消耗负载会加载此存储,并优化发布 属性。但是编译器目前不要在一般情况下甚至在本地优化原子,并且永远不要尝试进行那种 whole-program 分析。因此 code-gen 对于编写器函数将假设可能有一个 reader 与任何同步给定发布商店或 seq_cst 订购。)


What does memory_order_consume really do?

mo_consume 做的一件事是确保编译器在底层硬件 自然/免费提供依赖顺序的实现上使用屏障指令。实际上,这意味着仅在 DEC Alpha 上。 Dependent loads reordering in CPU / Memory order consume usage in C11

您的问题几乎与 C++11: the difference between memory_order_relaxed and memory_order_consume 重复 - 请参阅此处关于 body 的问题的答案,该问题是关于误入歧途的尝试以不稳定和放松的方式做事。 (我主要是因为标题问题而回答。)

它还确保编译器在执行传递到不知道此值所携带的数据依赖性的代码之前的某个时刻使用屏障。 (即声明中函数 arg 上没有 [[carries_dependency]] 标记)。这样的代码可能会用常量 0 替换 x-x 并进行优化,从而失去数据依赖性。但是知道依赖关系的代码必须使用类似 sub r1, r1, r1 指令的东西来获得具有数据依赖性的零。

您的 use-case 不会发生这种情况(其中 relaxed 实际上可以在 Alpha 以外的 ISA 上运行),但是 mo_consume 的 on-paper 设计允许各种需要与编译器通常所做的不同 code-gen 的东西。这是使其难以有效实现的部分原因,编译器只是将其提升为 mo_acquire.

问题的另一部分是它要求代码乱七八糟 kill_dependencyand/or [[carries_dependency]] 到处都是,否则你最终会在函数边界处遇到障碍。这些问题导致 ISO C++ 委员会暂时劝阻 consume.


顺便说一句:

示例代码在 release + consume 中是安全的,无论 volatile 是什么。在实践中,使用 release store + relaxed load 在大多数编译器和大多数 ISA 上是安全的,当然 ISO C++ 对该代码的正确性无话可说。但是对于编译器的当前状态,这是一些代码造成的黑客攻击(比如 Linux 内核的 RCU)。

如果您需要那种级别的 read-side 缩放,您将不得不在 ISO C++ 保证的范围之外工作。这意味着您的代码将不得不对编译器的工作方式做出假设(并且您 运行 在不是 DEC Alpha 的“普通”ISA 上),这意味着您需要支持某些编译器集(并且也许是 ISA,尽管周围 multi-core ISA 并不多)。 Linux 内核只关心少数几个编译器(主要是最近的 GCC,我认为也是 clang),以及它们拥有内核代码的 ISA。