Tru*_*uLa 6 c++ memory-model c++11
我现在正在学习C++ 11内存阶模型,并想明白之间的差别memory_order_relaxed和memory_order_consume.
具体而言,我正在寻找一个简单的例子,其中一个不能代替memory_order_consume用memory_order_relaxed.
有一篇很好的文章详细阐述了一个memory_order_consume可以应用的简单但非常具有说明性的例子.下面是文字复制粘贴.
例:
atomic<int*> Guard(nullptr);
int Payload = 0;
Run Code Online (Sandbox Code Playgroud)
制片人:
Payload = 42;
Guard.store(&Payload, memory_order_release);
Run Code Online (Sandbox Code Playgroud)
消费者:
g = Guard.load(memory_order_consume);
if (g != nullptr)
p = *g;
Run Code Online (Sandbox Code Playgroud)
我的问题包括两部分:
memory_order_consume用memory_order_relaxed在上面的例子?memory_order_consume不能被替换memory_order_relaxed?Mar*_*oom 11
号
memory_order_relaxed规定没有记忆顺序可言:
放松操作:没有同步或排序约束,此操作只需要原子性.
虽然memory_order_consume对数据强加存储器排序依赖性读取(在当前线程上)
具有此内存顺序的加载操作会对受影响的内存位置执行使用操作:在此加载之前,可以重新排序当前线程中与当前加载的值无关的读取.
编辑
总的来说memory_order_seq_cst,强者memory_order_acq_rel更强memory_ordering_relaxed.
这就像拥有一台能够举起800公斤升降机100公斤的电梯A的电梯A.
现在,如果您有能力将电梯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;
Run Code Online (Sandbox Code Playgroud)
此代码段旨在循环,两个线程之间没有同步,只有排序.
使用memory_order_relaxed,并假设自然词加载/存储是原子的,代码将等效于
Thread A Thread B
Payload = 42; g = Guard
Guard = 1 if (g != 0)
p = Payload;
Run Code Online (Sandbox Code Playgroud)
从线程A的CPU角度来看,有两个存储区到两个不同的地址,所以如果Guard从另一个处理器"更接近"CPU(意味着存储将更快完成),似乎线程A正在执行
Thread A
Guard = 1
Payload = 42
Run Code Online (Sandbox Code Playgroud)
这种执行顺序是可能的
Thread A Guard = 1
Thread B g = Guard
Thread B if (g != nullptr) p = Payload
Thread A Payload = 42
Run Code Online (Sandbox Code Playgroud)
这很糟糕,因为线程B读取了Payload的非更新值.
然而,似乎在线程B中同步将是无用的,因为CPU不会像重新排序一样
Thread B
if (g != 0) p = Payload;
g = Guard
Run Code Online (Sandbox Code Playgroud)
但它实际上会.
从它的角度来看,有两个不相关的负载,确实有一个在依赖数据路径上,但CPU仍然可以推测性地执行负载:
Thread B
hidden_tmp = Payload;
g = Guard
if (g != 0) p = hidden_tmp
Run Code Online (Sandbox Code Playgroud)
这可能会产生序列
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
Run Code Online (Sandbox Code Playgroud)
哎呦.
一般来说,这是永远无法做到的.
您可以替换memory_order_acquire与memory_order_consume你打算什么时候产生的加载值和值(S),其接入需要订购之间的地址的依赖性.
要了解memory_order_relaxed我们可以将ARM体系结构作为参考.
ARM体系结构仅强制要求弱内存排序,这意味着通常可以按任何顺序执行程序的加载和存储.
str r0, [r2]
str r0, [r3]
Run Code Online (Sandbox Code Playgroud)
在商店上面的片段中[r3]可以观察到外部,在商店之前到[r2]1.
然而,CPU并没有像Alpha CPU那样强加两种依赖关系:地址依赖性,当来自内存的值加载用于计算另一个加载/存储的地址,以及控制依赖性时,当一个值加载时memory用于计算另一个加载/存储的控制标志.
在存在这种依赖性的情况下,保证两个存储器操作的顺序在程序顺序中可见:
如果存在地址依赖性,则以程序顺序观察两次存储器访问.
因此,虽然a memory_order_acquire会产生内存障碍,但是memory_order_consume你告诉编译器你使用加载值的方式将产生地址依赖性,因此如果与架构相关,它可以利用这个事实并省略内存障碍.
1如果r2是同步对象的地址,那就不好了.
可以替换上面例子中
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:memory_order_consume 的悲伤故事:最后有一个幸福的结局吗?关于 Linux 如何使用它来使 RCU 的读取端变得非常非常便宜,没有任何障碍。(在内核中,他们只是使用volatile而不是_Atomicwith memory_order_relaxed,但对于纯加载或纯存储来说,它们的编译本质上是相同的。)
通过小心使用consume,并了解编译器通常如何编译代码,可以让 gcc 和 clang 等已知编译器相当可靠地为 x86、ARM 和 POWER 等已知目标发出安全/正确且高效的asm。在硬件中进行依赖排序。
(x86acquire在硬件中为你做,所以如果你只关心 x86,你将不会从使用relaxedoverconsume或 中获得任何好处acquire。)
memory_order_consume有人可以提出一个不能被替换的类似例子吗memory_order_relaxed?
DEC Alpha AXP 不保证硬件中的依赖顺序,并且一些 Alpha 微体系结构确实可能通过加载*g早于g. 请参阅CPU 中的相关负载重新排序以及C11 中的内存顺序消耗使用情况,以获取 Linus Torvalds 的引用,了解如何只有少数 Alpha 机器实际上可以做到这一点。
或者对于任何 ISA,如果编译器通过控制依赖项破坏了数据依赖项,则它可能会在编译时被破坏。 例如,如果编译器有某种理由认为它将g具有一定的值,则允许将其转换p = *g为类似的代码
if (g == expected_address)
p = *expected_address;
else
p = *g;
Run Code Online (Sandbox Code Playgroud)
真实的 CPU 使用分支预测,因此即使分支g.load()尚未完成,分支后的指令也可以执行。因此p = *expected_address可以在不依赖数据的情况下执行g。
弱序 ISA 确实记录了其依赖性排序保证(POWER、ARM 等),但不能保证跨分支,只能保证真正的数据依赖性。(如果树枝两边都用就好了*g。)
这可能不是编译器可能会做的事情,但 C++consume保证 即使array[foo.load(consume) & 1]在加载后也是依存顺序的。由于只有 2 个可能的值,编译器更有可能进行分支。
(或者在您的示例中,如果atomic<int*> Guard(nullptr);isstatic并且它的地址没有转义编译单元,那么编译器可能会发现它唯一可以拥有的两个值是nullptror&Payload,因此如果它非空,那么它一定是 Payload 。所以是的,这种优化实际上在你的情况下是合理的,对于mo_relaxed。我认为当前的 gcc / clang 可能永远不会对从原子加载的值做出任何假设(就像他们对待的那样volatile),所以你在实践中可能是安全的。这可能会改变一旦 C++ 找到一种方法可以让编译器安全地优化原子。 编译器是否可以优化两个原子加载?)
事实上,ISO C++consume甚至保证了依赖关系排序。int dep = foo.load(consume); dep -= dep; p = array[dep]; 例如,即使将依赖关系减少到编译时已知的值1后,您也可以使用它在标志分支后获取依赖关系排序。在这种情况下为零。
但是编译器会寻找变量减少到只有 1 个可能值的情况,并将其转换p = array[dep]为p = array[0],从而消除对加载的依赖。(这是一种依赖项跟踪,用于确定何时进行正常优化是安全的或不安全的,这使得在consume不到处限制编译器的情况下几乎不可能安全地实现。carries_dependency和kill_dependency的东西可能会将其限制在函数边界,但是结果还是太难了。)
脚注 1:这就是为什么像 ARM 这样的 ISA 甚至不允许eor r0, r0像x86xor eax,eax对. asm 规则确实保证在 asm 中执行此类操作是安全的。(固定指令宽度 ISA 无论如何都没有异或归零的用处;mov r0, #0大小相同。)问题是让编译器发出具有仅消耗所需的依赖项的 asm,而不执行任何避免数据的常规转换依赖关系并为乱序执行创建指令级并行性以查找和利用。
另请参阅P0371R1:暂时阻止 memory_order_consume和其他 C++ wg21 文档,该文档链接自有关为什么不鼓励使用的内容。
困难似乎源于高实现复杂性、当前定义使用相当普遍的“依赖关系”定义,因此需要频繁且不方便地使用调用
kill_dependency,以及频繁需要[[carries_dependency]]注释。详细信息可参见P0098R0等。
| 归档时间: |
|
| 查看次数: |
2746 次 |
| 最近记录: |