我在读取有关依赖关系和依赖顺序之前,在其定义中使用了一个5.1.2.4(p16)
:
在评估
A
之前,评估B
是依赖性排序的,如果:-
A
对原子对象M
执行释放操作,并且在另一个线程中,B
对M
执行消耗操作,并读取由A
为首的释放序列中的任何副作用写入的值,或者- 对于一些评估
X
,A
在X
和X
依赖于B
之前是依赖性排序的。
所以我试图制作一个可能有用的例子。就这个:
static _Atomic int i;
void *produce(void *ptr){
int int_value = *((int *) ptr);
atomic_store_explicit(&i, int_value, memory_order_release);
return NULL;
}
void *consume(void *ignored){
int int_value = atomic_load_explicit(&i, memory_order_consume);
int new_int_value = int_value + 42;
printf("Consumed = %d\n", new_int_value);
}
int main(int args, const char *argv[]){
int int_value = 123123;
pthread_t t2;
pthread_create(&t2, NULL, &produce, &int_value);
pthread_t t1;
pthread_create(&t1, NULL, &consume, NULL);
sleep(1000);
}
在void *consume(void*)
函数中,int_value
对new_int_value
具有依赖性,因此如果atomic_load_explicit(&i, memory_order_consume);
读取由某些atomic_store_explicit(&i, int_value, memory_order_release);
写的值,则new_int_value
计算依赖序列 - 在atomic_store_explicit(&i, int_value, memory_order_release);
之前。
但是之前有依赖性的有用的东西能给我们带来什么?
我目前认为memory_order_consume
可能会被memory_order_acquire
取代,而不会导致任何数据竞争......
consume
比acquire
便宜。所有CPU(除了DEC Alpha AXP着名的弱内存模型1)都是免费的,与acquire
不同。 (除了x86和SPARC-TSO,硬件具有acq / rel内存排序,没有额外的障碍或特殊指令。)
在ARM / AArch64 / PowerPC / MIPS /等弱排序的ISA上,consume
和relaxed
是唯一不需要任何额外障碍的订单,只需普通的廉价加载指令。即所有asm加载指令都是(至少)consume
加载,除了Alpha。 acquire
需要LoadStore和LoadLoad排序,这是一个比seq_cst
的全屏障更便宜的屏障指令,但仍然比没有更昂贵。
mo_consume
就像acquire
一样,仅用于对消耗负载具有数据依赖性的负载。例如float *array = atomic_ld(&shared, mo_consume);
,然后访问任何array[i]
是安全的,如果生产者存储缓冲区,然后使用mo_release
存储写入指向共享变量的指针。但是独立的加载/存储不必等待consume
加载完成,并且可以在它之前发生,即使它们在程序顺序中稍后出现。所以consume
只订购最低限度,不影响其他负载或商店。
(对于大多数CPU设计,基本上可以自由地在硬件中实现对consume
语义的支持,因为OoO exec无法破坏真正的依赖关系,并且加载对指针有数据依赖性,因此加载指针然后取消引用它本身就命令那些2仅仅因为因果关系的性质而加载。除非CPU进行价值预测或某些事情是疯狂的。价值预测就像分支预测一样,但要猜测要加载什么值而不是分支将走哪条路。
Alpha必须做一些疯狂的事情来制作CPU,这些CPU实际上可以在指针值真正加载之前加载数据,当存储按顺序完成时有足够的障碍。
与商店不同,商店缓冲区可以在商店执行和提交到L1d缓存(loads become "visible" by taking data from L1d cache when they execute)之间引入重新排序,而不是在退出+最终提交时。所以订购2加载wrt。彼此真的只是意味着按顺序执行这两个负载。由于一个数据依赖于另一个,因果关系要求在没有值预测的CPU上,并且在大多数体系结构上,ISA规则特别需要这样做。因此,您不必在加载+使用asm中的指针之间使用屏障,例如用于遍历链表。)
另见Dependent loads reordering in CPU
consume
to acquire
...而不是试图将C依赖关系映射到asm数据依赖关系(不会意外地破坏只有分支预测+推测执行可以绕过的控制依赖关系)。显然,编译器跟踪它并使其安全是一个难题。
将C映射到asm是非常重要的,因为如果依赖关系仅以条件分支的形式存在,则asm规则不适用。因此,很难为mo_consume
传播依赖关系定义C规则,只能采用与asm ISA规则相关的“携带依赖关系”的方式。
所以,是的,你是正确的consume
可以安全地替换为acquire
,但你完全忽略了这一点。
具有弱内存排序规则的ISA确实具有关于哪些指令具有依赖性的规则。因此,即使像eor r0,r0
无条件地将零qzxswpoi归零的指令在架构上也需要依旧对旧值进行数据依赖,这与x86不同,其中r0
成语被特别识别为依赖性破坏2。
另见xor eax,eax
我还在http://preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/的答案中提到了mo_consume
。
脚注1:实际上理论上可能“违反因果关系”的少数Alpha模型没有进行价值预测,其存储缓存存在不同的机制。我想我已经看到了关于它是如何可能的更详细的解释,但是Linus关于它实际上有多罕见的评论很有意思。
Atomic operations, std::atomic<> and ordering of writes
我想知道,您是否自己或仅仅在手册中看到Alpha的非因果关系?
我自己从未见过它,我认为我曾经访问过的任何模型都没有实际做过。这实际上使(慢)人民币指令更加烦人,因为它只是纯粹的缺点。
即使在实际可以重新订购负载的CPU上,显然在实践中基本上不可能实现。这其实非常讨厌。它导致了“哎呀,我忘记了一个障碍,但一切都运行了十年,有三个奇怪的报道'不可能发生'来自现场的各种错误”。弄清楚发生了什么事只会让人感到痛苦。
实际上有哪些型号呢?他们到底是怎么来的?
我认为它是21264,我有这个昏暗的内存是由于分区缓存:即使原始CPU按顺序执行了两次写入(中间有一个wmb),读取CPU可能最终会有第一次写入延迟(因为它进入的缓存分区忙于其他更新),并将首先读取第二个写入。如果第二次写入是第一次写入的地址,那么它可以跟随该指针,并且没有读取障碍来同步缓存分区,它可以看到旧的过时值。
但要注意“昏暗的记忆”。我可能把它与其他东西搞混了。到目前为止,我实际上还没有使用过近二十年的alpha版。你可以从价值预测中得到非常相似的效果,但我认为任何alpha微体系结构都没有这样做过。
无论如何,确实存在可以做到这一点的alpha版本,而且它不仅仅是纯粹的理论。
(RMB =读取内存屏障asm指令,和/或包含任何内联函数的Linux内核函数Linus Torvalds (Linux lead developer), in a RealWorldTech forum thread的名称是实现这一点所必需的。例如在x86上,只是编译时重新排序的障碍,rmb()
。我认为现代Linux与C11 / C ++ 11不同,当只需要数据依赖时,设法避免获取障碍,但我忘记了.Linux只能移植到一些编译器,而那些编译器确实需要支持Linux所依赖的东西,所以他们比起ISO C11标准更容易制作在真实ISA上实际运作的东西。)
另请参阅asm("":::"memory")
re:Linux的https://lkml.org/lkml/2012/2/1/521,这只是因为Alpha而在Linux中是必需的。 (但smp_read_barrier_depends()
的回复指出“编译器可以,有时会删除依赖性”,这就是为什么C11 Hans Boehm支持需要如此精细以避免破坏的风险。因此memory_order_consume
可能很脆弱。)
脚注2:x86命令所有加载它们是否携带指针的数据依赖性,因此它不需要保留“false”依赖项,并且使用可变长度指令集它实际上将代码大小保存到smp_read_barrier_depends
(2个字节) )而不是xor eax,eax
(5字节)。
所以mov eax,0
成为自8086年初以来的标准成语,现在它被认可并且实际上像xor reg,reg
一样处理,不依赖于旧值或RAX。 (实际上比mov
更有效率,而不仅仅是代码大小:mov reg,0
)
但对于ARM或大多数其他弱有序的ISA来说这是不可能的,就像我说他们实际上不允许这样做。
What is the best way to set a register to zero in x86 assembly: xor, mov or and?
需要在加载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
目前尚未明确,并且有一些memory_order_consume
来修复它。目前AFAIK所有实现都隐式将其推广到ongoing work。