memory_order_consume 到底有什么作用?

bre*_*r00 5 c++ cpu-architecture memory-model lock-free stdatomic

来自链接: 加载/存储宽松原子变量和普通变量有什么区别?

这个回答给我留下了深刻的印象:

使用原子变量解决了这个问题——通过使用原子,即使内存顺序放宽,所有线程都可以保证读取最新的写入值。

今天,我阅读了以下链接:https : //preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/

atomic<int*> Guard(nullptr);
int Payload = 0;
Run Code Online (Sandbox Code Playgroud)

线程1:

  Payload = 42;
    Guard.store(&Payload, memory_order_release);
Run Code Online (Sandbox Code Playgroud)

线程2:

g = Guard.load(memory_order_consume);
if (g != nullptr)
    p = *g;
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

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

所以

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

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.
Run Code Online (Sandbox Code Playgroud)

附加内容(由于 Sneftel 的 anwser):

1.有效载荷=42;易失性使有效负载的 W/R 到/从主内存而不是/从缓存。因此 42 将写入内存。

2.Guard.store(&Payload,任何MO标志都可以写); 正如您所说,Guard 是非易失性的,但它是原子的

使用原子变量解决了这个问题——通过使用原子,即使内存顺序放宽,所有线程都可以保证读取最新的写入值。

事实上,无论内存顺序如何,原子总是线程安全的!内存顺序不适用于原子 -> 用于非原子数据。

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

以上代码:

1.对数据依赖没有重新排序的影响。

2. 对 volatile Payload 没有缓存效果

3. atomic Guard 没有线程安全问题

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

回到主要问题

当您使用消费语义时,您基本上是在尝试让编译器利用所有这些处理器系列上的数据依赖性。这就是为什么一般来说,仅仅将 memory_order_acquire 更改为 memory_order_consume 是不够的。您还必须确保在 C++ 源代码级别存在数据依赖链。

在此处输入图片说明

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

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

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

附加内容结束

Pet*_*des 12

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

硬件仍然提供数据依赖性,因此谈论这一点很有趣,尽管目前没有任何安全可移植的 ISO C++ 方法可以利用。(仅使用mo_relaxed或手动原子进行黑客攻击,并基于对编译器优化和汇编的理解进行仔细编码,有点像您尝试使用relaxed 所做的那样。但您不需要易失性。)

噢,也许它利用数据依赖来防止指令重新排序,同时保证 Payload 的可见性?

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

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

发布/获取在线程之间创建发生前同步关系,这保证了编写者在发布存储之前所做的一切的可见性。(consume 不会,这就是为什么只对依赖的负载进行排序。)

consume对此的优化:只要遵循一些依赖关系规则,就可以让编译器利用硬件保证,从而避免读取器中的内存障碍。)


您对 CPU 缓存是什么以及它的作用有一些误解volatile,我在问题下对此进行了评论。发布存储确保早期的非原子分配在内存中可见。

(此外,缓存是连贯的;它为所有 CPU 提供了他们可以同意的共享内存视图。寄存器是线程私有的并且不连贯,这就是人们所说的值被“缓存”时的意思。寄存器不是 CPU缓存,但软件可以使用它们来保存内存中某些内容的副本。 何时在多线程中使用 易失性? -从不,但它确实在实际 CPU 中产生一些影响,因为它们具有一致的缓存。这是一个糟糕的方式来滚动你自己的缓存mo_relaxed. 另请参阅https://software.rajivprab.com/2018/04/29/myths-programmers- believe-about- cpu-caches/ )

在实际的 CPU 上,内存重新排序发生在每个核心本地;缓存本身是一致的,永远不会“不同步”。(在商店变得全局可见之前,其他副本将失效)。因此release,只需确保本地 CPU 存储以正确的顺序全局可见(提交到 L1d 缓存)。ISO C++ 没有指定任何详细程度,并且假设可能有一种工作方式非常不同的实现。

使编写器的存储变得易失性在实践中是无关紧要的,因为发布存储后面的非原子分配已经必须使所有内容对可能执行获取加载并与该发布存储同步的其他线程可见。这在纯 ISO C++ 的纸面上是无关紧要的,因为它无法避免数据争用 UB。

(当然,理论上整个程序优化可以看到没有任何获取或消耗负载会加载此存储,并优化释放属性。但是编译器目前一般不会优化原子,即使是在本地也是如此,永远不要尝试进行这种全程序分析。因此编写器函数的代码生成将假设可能有一个读取器与任何给定的发布存储或 seq_cst 排序同步。)


memory_order_consume 到底有什么作用?

要做的一件事mo_consume是确保编译器在底层硬件自然/免费提供依赖项排序的实现上使用屏障指令。实际上这意味着仅在 DEC Alpha 上。 CPU /内存顺序中的相关负载重新排序消耗了 C11 中的使用量

您的问题几乎是 C++11 的重复:内存_order_relaxed 和内存_order_consume 之间的差异- 请参阅那里的答案,了解您的问题正文,关于错误地尝试使用易失性和轻松来做事情。(我主要是因为标题问题才回答。)

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

对于您的用例来说,这种情况是不可能发生的(relaxed在实践中,除 Alpha 之外的 ISA 上都可以工作),但纸上设计允许mo_consume各种需要与编译器通常所做的不同代码生成的东西。这就是高效实现如此困难的部分原因,以至于编译器只是将其提升为mo_acquire.

问题的另一部分是它需要代码散布和kill_dependency/或[[carries_dependency]]遍布各处,否则无论如何您都会在函数边界处遇到障碍。这些问题导致ISO C++委员会暂时劝阻consume


顺便说一句:

示例代码使用release+时是安全的consume,无论是否易失。release在实践中,存储 +加载在大多数编译器和大多数 ISA 上都是安全的relaxed,尽管 ISO C++ 对于该代码的正确性没有任何说明。但就编译器的当前状态而言,这是某些代码所做的黑客攻击(例如 Linux 内核的 RCU)。

如果您需要这种级别的读取端缩放,则必须在 ISO C++ 保证的范围之外工作。这意味着您的代码必须对编译器的工作方式做出假设(并且您正在“正常”ISA 上运行,而不是 DEC Alpha),这意味着您需要支持某些编译器集(也许是 ISA,尽管周围没有很多多核 ISA)。Linux 内核只关心少数编译器(主要是最近的 GCC,我认为也是 clang),以及它们拥有内核代码的 ISA。

  • 类似地,允许轻松地将数据依赖项优化到分支中,例如,对于像“int idx = x.load(relaxed);”这样的东西。int *p = 表[idx]; q = *p;` 使用 2 元素表:编译器可以在 0 与 1 上进行分支并选择一个,从而失去依赖性。因此,ISO C++ 需要某种方法来禁止编译器执行此操作,同时仍然允许对不依赖于数据依赖顺序的代码进行完全灵活的优化。因此,“mo_consume”以某种形式作为正式语言规范的一部分是必要的,以避免“一切”都带有依赖关系并禁止分支。 (2认同)