C ++ 11:memory_order_relaxed和memory_order_consume之间的区别

时间:2016-07-09 10:03:29

标签: c++ c++11 memory-model

我现在正在学习C++11 memory order model,希望了解memory_order_relaxedmemory_order_consume之间的区别。

具体而言,我正在寻找一个简单的示例,其中无法用memory_order_consume替换memory_order_relaxed

有一个很好的post详细阐述了一个简单但非常具有说明性的例子,可以应用memory_order_consume。下面是文字复制粘贴。

实施例:

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

制片:

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

消费者:

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

我的问题包括两部分:

  1. 在上面的示例中,可以用memory_order_consume替换memory_order_relaxed吗?
  2. 可以建议使用memory_order_consume无法替换memory_order_relaxed的类似示例吗?

2 个答案:

答案 0 :(得分:10)

问题1

没有。
memory_order_relaxed根本不施加任何记忆命令:

  

轻松操作:没有同步或排序约束,此操作只需要原子性。

虽然memory_order_consume对数据相关读取(在当前线程上)强制执行内存排序

  

使用此内存顺序的加载操作会对受影响的内存位置执行使用操作:在此加载之前,可以重新排序当前线程中与当前加载的值无关的读取。

修改

一般来说memory_order_seq_cst更强memory_order_acq_rel更强memory_ordering_relaxed 这就像拥有一部电梯A,可以提升800公斤电梯C,提升100Kg 现在,如果您有能力将电梯A神奇地更换为电梯C,如果前者充满了10名平均加权人员会怎样? 那会很糟糕。

要确切了解代码可能出现的问题,请考虑您问题的示例:

Thread A                                   Thread B
Payload = 42;                              g = Guard.load(memory_order_consume);
Guard.store(1, memory_order_release);      if (g != 0)
                                               p = Payload;

此代码段旨在循环,两个线程之间没有同步,只有排序。

使用memory_order_relaxed,假设自然词加载/存储是原子的,代码将等同于

Thread A                                   Thread B
Payload = 42;                              g = Guard
Guard = 1                                  if (g != 0)
                                               p = Payload;

从线程A的CPU角度来看,有两个存储区到两个不同的地址,所以如果Guard与另一个处理器“更接近”CPU(意味着存储将更快完成),那么它似乎是线程A正在实施

Thread A
Guard = 1
Payload = 42

这种执行顺序是可能的

Thread A   Guard = 1
Thread B   g = Guard
Thread B   if (g != nullptr) p = Payload
Thread A   Payload = 42

这很糟糕,因为线程B读取非更新的Payload值

然而,似乎在线程B中同步将是无用的,因为CPU不会像

那样进行重新排序
Thread B
if (g != 0) p = Payload;
g = Guard

但它确实会。

从它的角度来看,有两个不相关的负载,确实有一个在依赖数据路径上,但CPU仍然可以推测性地负载:

Thread B
hidden_tmp = Payload;
g = Guard
if (g != 0) p = hidden_tmp

这可能会生成序列

Thread B   hidden_tmp = Payload;
Thread A   Payload = 42;
Thread A   Guard = 1;
Thread B   g = Guard
Thread B   if (g != 0) p = hidden_tmp

糟糕。

问题2

一般来说,这是永远无法做到的 当您要在加载的值与需要访问其值的值之间生成地址依赖关系时,可以将memory_order_acquire替换为memory_order_consume

要了解memory_order_relaxed,我们可以将ARM架构作为参考 ARM体系结构仅强制要求弱内存排序,这意味着程序的加载和存储通常可以任何顺序执行。

str r0, [r2]
str r0, [r3]

在上面的代码段中,可以在存储到[r3] 1 之前在外部观察到[r2]的商店。

然而,当使用内存中的值加载来计算另一个加载的地址时,CPU没有像Alpha CPU那样强加two kinds of dependencies地址依赖当使用内存中的值加载来计算另一个加载/存储的控制标志时,存储和控制依赖性

在存在这种依赖性的情况下,两个内存操作的顺序保证为visible in program order

  

如果存在地址依赖性,则按程序顺序观察两次存储器访问。

所以,虽然memory_order_acquire会产生内存障碍,但是memory_order_consume你告诉编译器你使用加载值的方式会产生地址依赖性,所以它可以,如果与架构相关,利用这一事实并省略内存障碍。

1 如果r2是同步对象的地址,那就不好了。

答案 1 :(得分:3)

在上面的示例中,可以用memory_order_consume替换memory_order_relaxed吗?

在ISO C ++中安全:否。

在大多数ISA的大多数实现的实践中,通常是。通常,它将在第一次加载结果和第二次加载的地址之间具有数据相关性的情况下编译为asm,并且大多数ISA都会保证该顺序。 (这是硬件功能consume打算公开的。)

但是,由于C ++ 11的consume设计对于编译器来说是不切实际的,因此他们都放弃了并将其增强到acquire,这在大多数弱排序的ISA上都需要一个内存屏障。 (例如POWER或ARM,但不包括x86)。

因此,在现实生活中,为了获得多汁的性能来读取几乎从未改变的内容,某些真实代码(例如RCU)实际上确实谨慎地使用了relaxed,我们希望不会对其进行优化。不安全。请参阅Paul E. McKenney在CppCon 2016上的演讲:C++ Atomics: The Sad Story of memory_order_consume: A Happy Ending At Last?,有关Linux如何使用它使RCU的读取非常便宜,没有障碍。 (在内核中,它们只使用volatile而不是_Atomic来使用memory_order_relaxed,但是对于纯负载或纯存储而言,它们的编译本质上是相同的。)

通过谨慎使用consume,并了解编译器通常如何编译代码,可以使gcc和clang等已知编译器相当可靠地发出安全/正确且效率用于已知目标(例如x86,ARM和POWER)的asm,这些已知目标在硬件中进行依赖项排序。

(x86在硬件上为您提供了acquire,因此,如果您只关心x86,则在relaxedconsume上使用acquire并不会获得任何收益。)< / p>

有人可以提出一个类似的示例,其中memory_order_consume不能替换为memory_order_relaxed吗?

DEC Alpha AXP不保证硬件中的依存关系顺序,并且某些Alpha微体系结构确实可以通过加载早于*g的{​​{1}}值来违反因果关系。请参阅Dependent loads reordering in CPUMemory order consume usage in C11,以获取Linus Torvalds关于仅少数Alpha机器如何实际执行此操作的报价。

或者对于任何ISA,如果编译器使用控件依赖关系破坏数据依赖关系,则它可能在编译时中断。如果编译器有某种理由认为g将具有某个值,则可以将其转换为g到类似

的代码
p = *g

实际的CPU使用分支预测,因此即使 if (g == expected_address) p = *expected_address; else p = *g; 尚未完成,也可以在分支之后执行指令。因此g.load()可以在不依赖p = *expected_address的情况下执行。

确实记录了其依赖项排序保证(POWER,ARM等)的弱排序ISA不能保证跨分支,只有真正的 data 依赖项可以得到保证。 (如果分支的两边都使用g,就可以了。)

这可能不是编译器可能要做的事情,但是C ++ *g保证即使consume在加载后也按依赖关系排序。只有两个可能的值,编译器会分支似乎更合理。

(或者在您的示例中,如果array[foo.load(consume) & 1]atomic<int*> Guard(nullptr);并且其地址未转义到编译单元,则然后,编译器可能会看到它只能有两个值staticnullptr ,因此,如果它不是非null,则必须是有效载荷。因此,对于您&Payload来说,这种优化实际上是合理的。 gcc / clang可能永远不会对从原子加载的值做任何假设(就像它们对待mo_relaxed一样),因此您在实践中可能是安全的。一旦C ++获得了使其对其安全的方法,这种情况可能会改变。 Can and does the compiler optimize out two atomic loads?


实际上,ISO C ++ volatile甚至可以保证对consume的依赖关系排序,例如,即使在减少标记后,也可以使用它来获得对分支的依赖关系排序对编译时已知的值的依赖 1 。在这种情况下为零。

但是编译器会寻找变量仅减少到1个可能值的情况,并将int dep = foo.load(consume); dep -= dep; p = array[dep];转换为p = array[dep],从而消除了对负载的依赖。 (这是一种依赖项跟踪,用于确定何时进行常规优化是安全的还是不安全的,这使得p = array[0]几乎不可能安全地实现而不会在各处拖延编译器。限制边界,但最终还是太难了。)

脚注1:这就是为什么像ARM这样的ISA甚至都不允许允许特殊情况consume作为打破依赖的清零习惯the way x86 does for xor eax,eax的原因。 asm规则确实可以保证在asm中执行类似 这样的操作是安全的。 (并且固定指令宽度的ISA根本不用于Xor归零; eor r0, r0的大小相同。)问题在于,使编译器发出的asm的依赖仅是消耗,而不执行它们的任何操作。避免数据依赖并创建指令级并行性以进行乱序执行以查找和利用的常见转换。


另请参阅P0371R1: Temporarily discourage memory_order_consume和其他与之相关的C ++ wg21文档,其中不鼓励使用它们。

困难似乎是由于实施复杂性高,当前定义使用相当普遍的“依赖关系”定义(因此需要频繁mov r0, #0调用)以及经常需要kill_dependency注释。细节可以在例如P0098R0