内存顺序消耗C11中的使用情况

问题描述 投票:2回答:2

我在读取有关依赖关系和依赖顺序之前,在其定义中使用了一个5.1.2.4(p16)

在评估A之前,评估B是依赖性排序的,如果:

- A对原子对象M执行释放操作,并且在另一个线程中,BM执行消耗操作,并读取由A为首的释放序列中的任何副作用写入的值,或者

- 对于一些评估XAXX依赖于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_valuenew_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取代,而不会导致任何数据竞争......

c multithreading c11 stdatomic
2个回答
3
投票

consumeacquire便宜。所有CPU(除了DEC Alpha AXP着名的弱内存模型1)都是免费的,与acquire不同。 (除了x86和SPARC-TSO,硬件具有acq / rel内存排序,没有额外的障碍或特殊指令。)

在ARM / AArch64 / PowerPC / MIPS /等弱排序的ISA上,consumerelaxed是唯一不需要任何额外障碍的订单,只需普通的廉价加载指令。即所有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


But current compilers just give up and strengthen 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。但只有那个负载,而不是所有其他后续负载;它不是获取障碍或获取负担。


2
投票

r3^r3 = 0目前尚未明确,并且有一些memory_order_consume来修复它。目前AFAIK所有实现都隐式将其推广到ongoing work

© www.soinside.com 2019 - 2024. All rights reserved.