内存顺序在 C11 中消耗使用量

Som*_*ame 4 c multithreading c11 stdatomic

我读过有关携带依赖关系和依赖关系排序之前,在其定义中使用一个5.1.2.4(p16)

在以下情况下,评估A在评估之前是依赖顺序的B

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

— 对于某些求值XA之前是依存顺序XX带有对 的依存关系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);
}
Run Code Online (Sandbox Code Playgroud)

在功能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);

但是dependency-ordered-before 能给我们带来什么有用的东西呢?

我目前认为很memory_order_consume可能在memory_order_acquire不引起任何数据竞争的情况下被替换...

Pet*_*des 11

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存储将指针写入共享变量,则访问 any是安全的。但是独立加载/存储不必等待consume加载完成,并且可以在加载完成之前发生,即使它们在程序顺序中出现较晚。所以consume只订购最低限度,不影响其他负载或商店。


对于大多数 CPU 设计,在硬件中实现对consume语义的支持基本上是免费的,因为 OoO exec 无法破坏真正的依赖关系,并且加载对指针具有数据依赖性,因此加载指针然后取消引用它固有地对这两个加载进行排序只是根据因果关系的性质。除非 CPU 进行值预测或一些疯狂的事情。值预测就像分支预测,但是猜测将要加载什么值而不是分支将要走哪条路。

Alpha 不得不做一些疯狂的事情来使 CPU 能够在指针值真正加载之前实际加载数据,当存储按顺序完成并有足够的障碍时。

与存储不同,存储缓冲区可以在存储执行和提交到 L1d 缓存之间引入重新排序,加载通过在执行时从 L1d 缓存中获取数据而变得“可见”,而不是在退休 + 最终提交时。所以订购 2 负载 wrt。彼此真的只是意味着按顺序执行这两个加载。由于数据相互依赖,因果关系需要在没有值预测的 CPU 上进行,而在大多数架构上,ISA 规则确实特别要求这样做。 因此,您不必在加载 + 使用 asm 中的指针之间使用障碍,例如用于遍历链表。)

另请参阅CPU 中的相关负载重新排序


但是当前的编译器只是放弃并加强consumeacquire

... 而不是尝试将 C 依赖项映射到 asm数据依赖项(不会意外破坏只有分支预测 + 推测执行可以绕过的控制依赖项)。显然,编译器跟踪它并使其安全是一个难题。

将 C 映射到 asm 并非易事,因为如果依赖项仅采用条件分支的形式,则 asm 规则不适用。因此,很难mo_consume仅以符合 asm ISA 规则中“携带依赖项”的方式来定义用于传播依赖项的C规则。

所以是的,你是正确的,consume可以安全地替换为acquire,但你完全没有抓住重点。


具有弱内存排序规则的 ISA确实有关于哪些指令携带依赖项的规则。因此,即使像 ARMeor r0,r0这样无条件清零的指令r0在体系结构上仍需要对旧值进行数据依赖,这与 x86 不同,在 x86 中,xor eax,eax习惯用法被特别识别为破坏依赖关系2

另见http://preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/

我还在mo_consume关于Atomic 操作、std::atomic<> 和 ordering of writes的回答中提到。


脚注 1:理论上实际上可以“违反因果关系”的少数 Alpha 模型没有进行价值预测,它们的存储缓存有不同的机制。我想我已经看到了关于它是如何可能的更详细的解释,但是 Linus 关于它实际上有多罕见的评论很有趣。

Linus Torvalds(Linux 首席开发人员),在 RealWorldTech 论坛帖子中

我想知道,你是自己看到 Alpha 的非因果关系还是只是在手册中?

我自己从未见过它,而且我认为我曾经接触过的任何模型都没有真正做到过。这实际上使(缓慢的)人民币指令更加烦人,因为它只是纯粹的缺点。

即使在实际上可以重新排序负载的 CPU 上,在实践中显然基本上不可能命中。这实际上非常讨厌。结果是“哎呀,我忘记了一个障碍,但是十年来一切都很好,有三起奇怪的报告,说是来自现场的‘不可能发生’的错误”之类的事情。弄清楚发生了什么是痛苦的。

哪些模型实际上有它?他们究竟是如何来到这里的?

我认为它是 21264,由于分区缓存,我对它的记忆很暗:即使原始 CPU 按顺序执行两次写入(中间有一个 wmb),读取 CPU 可能最终会进行第一次写入延迟(因为它进入的缓存分区正忙于其他更新),并且会先读取第二个写入。如果第二次写入是第一次写入的地址,那么它可以跟随该指针,并且没有读取屏障来同步缓存分区,它可以看到旧的陈旧值。

但请注意“昏暗的记忆”。我可能把它和别的东西混淆了。到目前为止,我实际上已经有将近二十年没有使用过 alpha 了。您可以从价值预测中获得非常相似的效果,但我认为没有任何 alpha 微体系结构能做到这一点。

无论如何,肯定有 alpha 版本可以做到这一点,这不仅仅是理论上的。

(RMB = Read Memory Barrier asm 指令,和/或 Linux 内核函数的名称,rmb()它包装了实现这一点所需的任何内联 asm。例如,在 x86 上,只是编译时重新排序的障碍asm("":::"memory")。我认为现代 Linux 设法在只需要数据依赖项时避免获取障碍,与 C11/C++11 不同,但我忘记了。Linux 仅可移植到少数编译器,而这些编译器确实注意支持 Linux 所依赖的内容,因此它们具有比 ISO C11 标准更容易制定在实际 ISA 上实际有效的东西。)

另请参阅https://lkml.org/lkml/2012/2/1/521 re:Linuxsmp_read_barrier_depends()中仅由于 Alpha 而需要的 Linux。(但Hans Boehmmemory_order_consume的回复指出“编译器可以,有时确实会删除依赖项”,这就是为什么 C11支持需要如此精心设计以避免损坏的风险。因此smp_read_barrier_depends可能很脆弱。)


脚注 2:x86 对所有加载进行排序,无论它们是否带有对指针的数据依赖性,因此它不需要保留“假”依赖性,并且使用可变长度指令集实际上将代码大小节省为xor eax,eax(2 个字节)而是mov eax,0(5 个字节)。

Soxor reg,reg从 8086 天开始就成为标准习语,现在它被识别并实际处理为mov,不依赖于旧值或 RAX。(事实上​​,不仅仅是代码大小有效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
Run Code Online (Sandbox Code Playgroud)

需要在r0的加载r4之后注入对 的依赖并对其进行排序r3,即使加载地址r1+r0始终只是r1因为r3^r3 = 0. 但只有那个负载,而不是所有其他后来的负载;它不是获取障碍或获取负载。

  • @SomeName:是的,消费就像仅获取对消费负载具有数据依赖性的负载。独立的加载/存储仍然可以在它之前发生,而无需等待 `consume` 加载完成。 (3认同)
  • @SomeName:我(最近)实际上并没有阅读定义消费的标准中的语言,我只知道在公开有用的 asm 行为方面*应该*是什么。但是,是的,这是有道理的,*synchronize-with* 是一种技术语言,它意味着在此之后的所有内容都在发布之前的所有内容之后,并且 `consume` 的全部意义不是拖延/阻塞其他操作以等待它发生。 (3认同)
  • @SomeName:在 x86 上,我们有免费的 `acquire`,`consume` 既不有趣也不特别。但是在 ARM/AArch64/PowerPC/MIPS/etc 上,我们只有 `consume`(和 `relaxed`)是免费的,任何更多的东西至少需要便宜的障碍。(虽然不如`seq_cst` 所需的StoreLoad full 障碍那么昂贵)。x86的TSO内存模型是seq_cst+一个store buffer(带store forwarding),所以stores只能get到,其他的都是有序的。 (2认同)