C11 中的内存顺序消耗使用情况
Memory order consume usage in C11
我读到了 带有依赖性 关系和 依赖性排序在 之前 在其定义 5.1.2.4(p16)
中使用了一个:
An evaluation A
is dependency-ordered before an evaluation B
if:
— A
performs a release operation on an atomic object M
, and, in another
thread, B
performs a consume operation on M
and reads a value written
by any side effect in the release sequence headed by A
, or
— for some evaluation X
, A
is dependency-ordered before X
and X
carries a dependency to 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
计算 dependency-ordered-before atomic_store_explicit(&i, int_value, memory_order_release);
。
但是dependency-ordered-before能给我们带来什么有用的东西呢?
我目前认为 memory_order_consume
可以很好地替换为 memory_order_acquire
而不会导致任何数据竞争...
memory_order_consume
目前未指定,有一些 ongoing work 可以修复它。目前 AFAIK 所有实现都隐含地将其提升为 memory_order_acquire
。
consume
比 acquire
便宜。所有 CPUs(除了 DEC Alpha AXP 著名的弱内存模型1)都是免费的,不像 acquire
.(除了 x86 和SPARC-TSO,其中硬件具有 acq/rel 内存排序,没有额外的障碍或特殊指令。)
在 ARM/AArch64/PowerPC/MIPS/etc 弱排序的 ISA 上,consume
和 relaxed
是唯一不需要任何额外障碍的排序,只需要普通的廉价加载指令。即所有 asm 加载指令都是(至少)consume
加载,Alpha 除外。 acquire
需要 LoadStore 和 LoadLoad 排序,这是一种比 seq_cst
的完全屏障更便宜的屏障指令,但总比没有昂贵。
mo_consume
类似于 acquire
仅适用于数据依赖于消费负载 的负载。例如float *array = atomic_ld(&shared, mo_consume);
,如果生产者存储了缓冲区并且 then 使用 mo_release
存储将指针写入共享,那么访问任何 array[i]
都是安全的多变的。但是独立 loads/stores 不必等待 consume
加载完成,并且可以在它之前发生,即使它们在程序顺序中出现得较晚。所以 consume
只订购最低限度的订单,不影响其他货物或商店。
(对于大多数CPU设计,在硬件中实现对consume
语义的支持基本上是免费的,因为OoO exec不能破坏真正的依赖关系,并且加载对指针具有数据依赖性,因此加载指针然后取消引用它本质上仅根据因果关系的性质对这 2 个加载进行排序。除非 CPUs 进行值预测或一些疯狂的事情。
值预测类似于分支预测,但猜测将加载什么值而不是分支将走哪条路。
Alpha 必须做一些疯狂的事情才能使 CPUs 能够真正加载指针值之前的数据,当存储按顺序完成并具有足够的障碍时。
与存储不同,存储缓冲区可以在存储执行和提交到 L1d 缓存之间引入重新排序,,而不是在退出 + 最终提交时。所以订购 2 负载 wrt。彼此真的只是意味着按顺序执行这两个负载。由于数据相互依赖,因果关系要求在 CPUs 上没有值预测,并且在大多数体系结构上,ISA 规则确实要求这样做。 因此您不必在加载和使用 asm 中的指针之间使用屏障,例如用于遍历链表。)
另见 Dependent loads reordering in CPU
但是目前的编译器只是放弃并加强了consume
到acquire
... 而不是尝试将 C 依赖项映射到 asm data 依赖项(不会意外破坏只有分支预测 + 推测执行可以绕过的控制依赖项)。显然,编译器要跟踪它并使其安全是一个难题。
将 C 映射到 asm 并非易事,因为如果依赖仅以条件分支的形式存在,则 asm 规则不适用。因此,很难为 mo_consume
传播依赖关系定义 C 规则,仅以符合 "carry a dependency" 在 asm ISA 规则方面所做的事情的方式。
所以是的,您说 consume
可以安全地替换为 acquire
是正确的,但您完全没有抓住重点。
具有弱内存排序规则的 ISA 有关于哪些指令具有依赖性的规则。因此,即使是像 ARM eor r0,r0
这样无条件地将 r0
置零的指令在架构上仍然需要对旧值具有数据依赖性,这与 x86 不同,其中 xor eax,eax
习惯用法被特别认为是依赖性破坏2.
另见 http://preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/
我在 的回答中也提到了 mo_consume
。
脚注 1:理论上实际上可以的少数 Alpha 模型 "violate causality" 没有进行值预测,它们的银行缓存有不同的机制。我想我已经看到了关于它如何可能的更详细的解释,但 Linus 关于它实际上是多么罕见的评论很有趣。
Linus Torvalds (Linux lead developer), in a RealWorldTech forum thread
I wonder, did you see non-causality on Alpha by yourself or just in the manual?
我自己从来没见过,而且我认为我曾经拥有的任何模型都没有
访问实际上做到了。这实际上使(缓慢的)人民币
指令特别烦人,因为它只是纯粹的缺点。
即使在 CPU 上实际上可以重新订购负载,它也是
显然在实践中基本上不可能击中。这实际上是
很讨厌。结果是“哎呀,我忘记了一个屏障,但是一切
工作了十年,有三份奇怪的报告说“那不能”
发生“来自现场的错误”之类的事情。弄清楚是什么
继续下去真是太痛苦了。
Which models actually had it? And how exactly they got here?
我想是 21264,我对它的到期时间记忆犹新
到分区缓存:即使原始 CPU 在中进行了两次写入
顺序(中间有一个 wmb),读数 CPU 可能最终会得到
首次写入延迟(因为它进入的缓存分区是
忙于其他更新),并且会先读取第二个写入。如果
第二个写的是第一个的地址,然后它可以
跟随那个指针,并且没有读屏障来同步
缓存分区,它可以看到旧的陈旧值。
但请注意 "dim memory"。我可能把它和别的东西混淆了。
到现在为止,我实际上已经有将近二十年没有使用过 alpha 了。你
可以从价值预测中得到非常相似的效果,但我不认为
任何 alpha 微架构都曾这样做过。
无论如何,肯定有一些 alpha 版本可以做到
这,而且不仅仅是纯粹的理论。
(RMB = Read Memory Barrier asm 指令,and/or Linux 内核函数的名称 rmb()
,它包装了实现这一点所必需的任何内联 asm。例如,在 x86 上,只是编译时重新排序的障碍,asm("":::"memory")
。我认为现代 Linux 在只需要数据依赖时设法避免了获取障碍,这与 C11/C++11 不同,但我忘记了. Linux 仅适用于少数编译器,这些编译器确实会注意支持 Linux 所依赖的内容,因此它们比 ISO C11 标准更容易编写出在实践中有效的内容在真正的 ISA 上。)
另见 https://lkml.org/lkml/2012/2/1/521 re: Linux's smp_read_barrier_depends()
which is necessary in Linux only because of Alpha. (But a reply from Hans Boehm 指出“编译器可以,有时确实会删除依赖项”,这就是为什么需要 C11 memory_order_consume
支持如此精心 以避免破损的风险。因此 smp_read_barrier_depends
可能很脆弱。)
脚注 2:x86 对所有加载进行排序,无论它们是否对指针具有数据依赖性,因此它不需要保留 "false" 依赖性,并且使用可变长度指令集,它实际上将代码大小保存为 xor eax,eax
(2 字节)而不是 mov eax,0
(5 字节)。
所以 xor reg,reg
从 8086 天开始就成为标准习语,现在它被识别并实际处理为 mov
,不依赖于旧值或 RAX。 (事实上 比 mov reg,0
更有效 除了代码大小:)
但这对于 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
的依赖,并在 r3
的加载之后排序 r4
的加载,即使加载地址 r1+r0
始终只是r1
因为 r3^r3 = 0
。 但只有那个加载,而不是所有其他以后的加载;它不是获取障碍或获取负载。
我读到了 带有依赖性 关系和 依赖性排序在 之前 在其定义 5.1.2.4(p16)
中使用了一个:
An evaluation
A
is dependency-ordered before an evaluationB
if:—
A
performs a release operation on an atomic objectM
, and, in another thread,B
performs a consume operation onM
and reads a value written by any side effect in the release sequence headed byA
, or— for some evaluation
X
,A
is dependency-ordered beforeX
andX
carries a dependency toB
.
所以我尝试制作一个可能有用的示例。在这里:
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
计算 dependency-ordered-before atomic_store_explicit(&i, int_value, memory_order_release);
。
但是dependency-ordered-before能给我们带来什么有用的东西呢?
我目前认为 memory_order_consume
可以很好地替换为 memory_order_acquire
而不会导致任何数据竞争...
memory_order_consume
目前未指定,有一些 ongoing work 可以修复它。目前 AFAIK 所有实现都隐含地将其提升为 memory_order_acquire
。
consume
比 acquire
便宜。所有 CPUs(除了 DEC Alpha AXP 著名的弱内存模型1)都是免费的,不像 acquire
.(除了 x86 和SPARC-TSO,其中硬件具有 acq/rel 内存排序,没有额外的障碍或特殊指令。)
在 ARM/AArch64/PowerPC/MIPS/etc 弱排序的 ISA 上,consume
和 relaxed
是唯一不需要任何额外障碍的排序,只需要普通的廉价加载指令。即所有 asm 加载指令都是(至少)consume
加载,Alpha 除外。 acquire
需要 LoadStore 和 LoadLoad 排序,这是一种比 seq_cst
的完全屏障更便宜的屏障指令,但总比没有昂贵。
mo_consume
类似于 acquire
仅适用于数据依赖于消费负载 的负载。例如float *array = atomic_ld(&shared, mo_consume);
,如果生产者存储了缓冲区并且 then 使用 mo_release
存储将指针写入共享,那么访问任何 array[i]
都是安全的多变的。但是独立 loads/stores 不必等待 consume
加载完成,并且可以在它之前发生,即使它们在程序顺序中出现得较晚。所以 consume
只订购最低限度的订单,不影响其他货物或商店。
(对于大多数CPU设计,在硬件中实现对consume
语义的支持基本上是免费的,因为OoO exec不能破坏真正的依赖关系,并且加载对指针具有数据依赖性,因此加载指针然后取消引用它本质上仅根据因果关系的性质对这 2 个加载进行排序。除非 CPUs 进行值预测或一些疯狂的事情。
值预测类似于分支预测,但猜测将加载什么值而不是分支将走哪条路。
Alpha 必须做一些疯狂的事情才能使 CPUs 能够真正加载指针值之前的数据,当存储按顺序完成并具有足够的障碍时。
与存储不同,存储缓冲区可以在存储执行和提交到 L1d 缓存之间引入重新排序,
另见 Dependent loads reordering in CPU
但是目前的编译器只是放弃并加强了consume
到acquire
... 而不是尝试将 C 依赖项映射到 asm data 依赖项(不会意外破坏只有分支预测 + 推测执行可以绕过的控制依赖项)。显然,编译器要跟踪它并使其安全是一个难题。
将 C 映射到 asm 并非易事,因为如果依赖仅以条件分支的形式存在,则 asm 规则不适用。因此,很难为 mo_consume
传播依赖关系定义 C 规则,仅以符合 "carry a dependency" 在 asm ISA 规则方面所做的事情的方式。
所以是的,您说 consume
可以安全地替换为 acquire
是正确的,但您完全没有抓住重点。
具有弱内存排序规则的 ISA 有关于哪些指令具有依赖性的规则。因此,即使是像 ARM eor r0,r0
这样无条件地将 r0
置零的指令在架构上仍然需要对旧值具有数据依赖性,这与 x86 不同,其中 xor eax,eax
习惯用法被特别认为是依赖性破坏2.
另见 http://preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/
我在 mo_consume
。
脚注 1:理论上实际上可以的少数 Alpha 模型 "violate causality" 没有进行值预测,它们的银行缓存有不同的机制。我想我已经看到了关于它如何可能的更详细的解释,但 Linus 关于它实际上是多么罕见的评论很有趣。
Linus Torvalds (Linux lead developer), in a RealWorldTech forum thread
I wonder, did you see non-causality on Alpha by yourself or just in the manual?
我自己从来没见过,而且我认为我曾经拥有的任何模型都没有 访问实际上做到了。这实际上使(缓慢的)人民币 指令特别烦人,因为它只是纯粹的缺点。
即使在 CPU 上实际上可以重新订购负载,它也是 显然在实践中基本上不可能击中。这实际上是 很讨厌。结果是“哎呀,我忘记了一个屏障,但是一切 工作了十年,有三份奇怪的报告说“那不能” 发生“来自现场的错误”之类的事情。弄清楚是什么 继续下去真是太痛苦了。
Which models actually had it? And how exactly they got here?
我想是 21264,我对它的到期时间记忆犹新 到分区缓存:即使原始 CPU 在中进行了两次写入 顺序(中间有一个 wmb),读数 CPU 可能最终会得到 首次写入延迟(因为它进入的缓存分区是 忙于其他更新),并且会先读取第二个写入。如果 第二个写的是第一个的地址,然后它可以 跟随那个指针,并且没有读屏障来同步 缓存分区,它可以看到旧的陈旧值。
但请注意 "dim memory"。我可能把它和别的东西混淆了。 到现在为止,我实际上已经有将近二十年没有使用过 alpha 了。你 可以从价值预测中得到非常相似的效果,但我不认为 任何 alpha 微架构都曾这样做过。
无论如何,肯定有一些 alpha 版本可以做到 这,而且不仅仅是纯粹的理论。
(RMB = Read Memory Barrier asm 指令,and/or Linux 内核函数的名称 rmb()
,它包装了实现这一点所必需的任何内联 asm。例如,在 x86 上,只是编译时重新排序的障碍,asm("":::"memory")
。我认为现代 Linux 在只需要数据依赖时设法避免了获取障碍,这与 C11/C++11 不同,但我忘记了. Linux 仅适用于少数编译器,这些编译器确实会注意支持 Linux 所依赖的内容,因此它们比 ISO C11 标准更容易编写出在实践中有效的内容在真正的 ISA 上。)
另见 https://lkml.org/lkml/2012/2/1/521 re: Linux's smp_read_barrier_depends()
which is necessary in Linux only because of Alpha. (But a reply from Hans Boehm 指出“编译器可以,有时确实会删除依赖项”,这就是为什么需要 C11 memory_order_consume
支持如此精心 以避免破损的风险。因此 smp_read_barrier_depends
可能很脆弱。)
脚注 2:x86 对所有加载进行排序,无论它们是否对指针具有数据依赖性,因此它不需要保留 "false" 依赖性,并且使用可变长度指令集,它实际上将代码大小保存为 xor eax,eax
(2 字节)而不是 mov eax,0
(5 字节)。
所以 xor reg,reg
从 8086 天开始就成为标准习语,现在它被识别并实际处理为 mov
,不依赖于旧值或 RAX。 (事实上 比 mov reg,0
更有效 除了代码大小:
但这对于 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
的依赖,并在 r3
的加载之后排序 r4
的加载,即使加载地址 r1+r0
始终只是r1
因为 r3^r3 = 0
。 但只有那个加载,而不是所有其他以后的加载;它不是获取障碍或获取负载。