consume
is cheaper than acquire
. All CPUs (except DEC Alpha AXP's famously weak memory model1) do it for free, unlike acquire
. (Except on x86 and SPARC-TSO, where the hardware has acq/rel memory ordering without extra barriers or special instructions.)
在 ARM/AArch64/PowerPC/MIPS/etc 弱有序 ISA 上,consume
and relaxed
是唯一不需要任何额外障碍的订单,只需普通的廉价加载指令。即所有 asm 加载指令(至少)consume
负载,除了 Alpha 上。acquire
需要 LoadStore 和 LoadLoad 排序,这是比全屏障更便宜的屏障指令seq_cst
,但仍然比没有贵。
mo_consume
就好像acquire
仅适用于数据依赖于消耗负载的负载. e.g. float *array = atomic_ld(&shared, mo_consume);
,然后访问任意array[i]
如果生产者存储了缓冲区并且then used a mo_release
store 将指针写入共享变量。但独立的加载/存储不必等待consume
加载完成,并且可以在它之前发生,即使它们在程序顺序中出现得较晚。所以consume
只订购最低限度的订单,不影响其他负载或商店。
(实现支持基本上是免费的consume
大多数 CPU 设计的硬件语义,因为 OoO exec 无法打破真正的依赖关系,并且加载对指针具有数据依赖性,因此加载指针然后取消引用它本质上只是根据因果关系的性质对这两个加载进行排序。除非 CPU 进行价值预测或其他疯狂的事情。
值预测类似于分支预测,但猜测将加载什么值,而不是分支将走哪条路。
Alpha 必须做一些疯狂的事情,以使 CPU 能够在指针值真正加载之前实际加载数据,此时存储是按顺序完成且具有足够的屏障。
与存储不同的是,存储缓冲区可以在存储执行和提交到 L1d 缓存之间引入重新排序,通过从 L1d 缓存获取数据,负载变得“可见”execute,而不是当退休+最终提交时。所以订购 2 个负载。彼此实际上只是意味着按顺序执行这两个加载。由于数据之间存在相互依赖性,因果关系要求在没有值预测的 CPU 上这样做,并且在大多数架构上,ISA 规则确实特别要求这一点。因此,您不必在加载 + 使用 asm 中的指针之间使用屏障,例如用于遍历链表。)
也可以看看CPU 中的相关负载重新排序
但目前的编译器只是放弃并加强consume
to acquire
...而不是尝试将 C 依赖项映射到 asmdata依赖关系(不会意外地破坏只有分支预测+推测执行可以绕过的控制依赖关系)。显然,对于编译器来说,跟踪它并确保它的安全是一个难题。
将 C 映射到 asm 并不简单,因为如果依赖项仅采用条件分支的形式,则 asm 规则不适用。所以很难定义 C 规则mo_consume
仅以符合 asm ISA 规则中“携带依赖项”的方式传播依赖项。
所以是的,你是对的consume
可以安全地替换为acquire
,但你完全没有抓住要点。
ISAs with weak memory-ordering rules do have rules about which instructions carry a dependency. So even an instruction like ARM eor r0,r0
which unconditionally zeroes r0
is architecturally required to still carry a data dependency on the old value, unlike x86 where the xor eax,eax
idiom is specially recognized as dependency-breaking2.
也可以看看http://preshing.com/20140709/the- Purpose-of-memory_order_consume-in-cpp11/
我还提到过mo_consume
在回答中原子操作、std::atomic 和写入顺序.
脚注1:理论上实际上可以“违反因果关系”的少数 Alpha 模型没有进行价值预测,它们的存储缓存有不同的机制。我想我已经看到了关于它如何可能的更详细的解释,但是 Linus 关于它实际上是多么罕见的评论很有趣。
Linus Torvalds(Linux 首席开发人员),在 RealWorldTech 论坛帖子中
我想知道,你是自己在 Alpha 上看到非因果关系还是在手册中看到的?
我自己从未见过,我也不认为我曾经拥有过任何模型
访问实际上做到了。这实际上使得(缓慢的)人民币
指令非常烦人,因为这纯粹是缺点。
即使在实际上可以重新排序负载的 CPU 上,它也是如此
显然在实践中基本上不可能击中。这实际上是
非常讨厌。结果是“哎呀,我忘记了一个障碍,但是一切
十年来一直运行良好,但有三份奇怪的报告称“那不能”
发生“现场错误”之类的事情。弄清楚是什么
继续下去真是痛苦极了。
哪些型号实际上有它?他们究竟是怎么来到这里的?
我认为是 21264,我对它的到期时间有模糊的记忆
到分区缓存:即使原始 CPU 进行了两次写入
顺序(中间有一个 wmb),读取 CPU 最终可能会得到
第一次写入延迟(因为它进入的缓存分区是
忙于其他更新),并且会先读取第二个写入。如果
第二次写入是第一次写入的地址,然后它可以
遵循该指针,并且没有读屏障来同步
缓存分区,它可以看到旧的陈旧值。
但请注意“模糊记忆”。我可能把它与其他东西混淆了。
到目前为止,我实际上已经有近二十年没有使用过阿尔法了。你
可以从价值预测中得到非常相似的效果,但我不认为
任何 alpha 微架构都曾这样做过。
无论如何,肯定有 alpha 版本可以做到这一点
这不仅仅是纯粹的理论上的。
(RMB = Read Memory Barrier asm 指令,和/或 Linux 内核函数的名称rmb()
它包装了实现这一目标所需的任何内联汇编。例如在 x86 上,只是编译时重新排序的障碍,asm("":::"memory")
。我认为现代 Linux 在只需要数据依赖时设法避免获取障碍,与 C11/C++11 不同,但我忘记了。 Linux 仅可移植到少数编译器,并且这些编译器确实会注意支持 Linux 所依赖的内容,因此它们比 ISO C11 标准更容易编写出在实际 ISA 上运行的东西。)
也可以看看https://lkml.org/lkml/2012/2/1/521回复:Linux 的smp_read_barrier_depends()
这在 Linux 中是必要的,只是因为 Alpha。 (但是来自汉斯·伯姆指出“编译器有时可以删除依赖项”,这就是为什么 C11memory_order_consume
支持需要如此详尽以避免破损风险。因此smp_read_barrier_depends
可能很脆弱。)
脚注2:x86 对所有加载进行排序,无论它们是否带有对指针的数据依赖性,因此它不需要保留“假”依赖性,并且使用可变长度指令集,它实际上将代码大小保存为xor eax,eax
(2 个字节)代替mov eax,0
(5 字节)。
So xor reg,reg
自 8086 天早期起就成为标准习语,现在它已被识别并实际处理为mov
,不依赖于旧值或 RAX。 (事实上more有效地比mov reg,0
不仅仅是代码大小:在 x86 汇编中将寄存器设置为零的最佳方法是什么:xor、mov 或 and?)
但这对于 ARM 或大多数其他弱有序 ISA 来说是不可能的,就像我说的那样,他们实际上不允许这样做。
ldr r3, [something] ; load r3 = mem
eor r0, r3,r3 ; r0 = r3^r3 = 0
ldr r4, [r1, r0] ; load r4 = mem[r1+r0]. Ordered after the other load
需要注入依赖r0
并订购负载r4
负载后r3
,即使加载地址r1+r0
总是只是r1
因为r3^r3 = 0
. 但只有that加载,而不是所有其他后续加载;它不是获取障碍或获取负载。