Pro*_*mer 3 c++ memory-model c++11 carries-dependency stdatomic
我在这篇SO帖子中阅读了 [[carries_dependency]] 。
但我无法理解的是接受的答案中的以下句子:
“特别是,如果将使用 memory_order_consume 读取的值传递给函数,然后没有 [[carries_dependency]],那么编译器可能必须发出内存栅栏指令以保证支持适当的内存排序语义。如果参数用 [[carries_dependency]] 注释,那么编译器可以假设函数体将正确携带依赖项,并且可能不再需要此围栏。
类似地,如果一个函数返回一个加载了 memory_order_consume 的值,或者从这样的值派生,那么如果没有 [[carries_dependency]],编译器可能需要插入一个栅栏指令来保证适当的内存排序语义得到支持。有了 [[carries_dependency]] 注释,这个围栏可能不再是必要的,因为调用者现在负责维护依赖树。”
让我们一步一步来:
“如果将使用 memory_order_consume 读取的值传递给函数,然后没有 [[carries_dependency]],那么编译器可能必须发出内存栅栏指令,以确保支持适当的内存排序语义。”
因此,对于释放-消耗内存模型中的原子变量,当原子变量作为参数传递给函数时,编译器将引入栅栏硬件指令,以便它始终具有提供给函数的原子变量的最新和更新值。
下一个 -
“如果参数用 [[carries_dependency]] 注释,那么编译器可以假设函数体将正确携带依赖项,并且可能不再需要这个围栏。”
这让我很困惑 - 原子变量值已经被消耗了,然后函数携带了什么依赖?
相似地 -
“如果一个函数返回一个加载了 memory_order_consume 的值,或者从这样的值派生,那么如果没有 [[carries_dependency]],编译器可能需要插入一个栅栏指令来保证适当的内存排序语义得到支持。使用 [[ Carry_dependency]] 注释,这个围栏可能不再是必要的,因为调用者现在负责维护依赖树。”
从这个例子中,它不清楚它试图说明携带依赖的意义是什么?
仅供参考,memory_order_consume(和[[carries_dependency]])基本上已被弃用,因为编译器很难以 C++11 设计规则的方式有效和正确地实现规则。(和/或因为[[carries_dependency]]和/或kill_dependency最终会在所有地方都需要。)参见P0371R1:暂时不鼓励 memory_order_consume。
当前的编译器简单地将其mo_consume视为mo_acquire(因此在需要一个 ISA 的 ISA 上,在消耗负载之后立即设置障碍)。如果您想要无障碍的数据依赖排序的性能,您必须通过mo_relaxed仔细使用和编码来欺骗编译器,以避免可能使编译器在没有实际依赖的情况下创建 asm 的事情。(例如 Linux RCU)。请参阅C++11:memory_order_relaxed 和 memory_order_consume 之间的区别以获取更多详细信息和相关链接,以及mo_consume旨在公开的 asm 功能。
此外,内存顺序消耗 C11 中的使用量。
理解依赖顺序(在 asm 中)的概念对于理解这个 C++ 特性是如何设计的基本上是必不可少的。
当 [an] 原子变量作为参数传递给函数时,编译器将引入栅栏硬件指令......
首先,您不会将“原子变量”传递给函数;这甚至意味着什么?如果您传递一个指向原子对象的指针或引用,该函数将从它自己加载,并且该函数的源代码将使用memory_order_consume或不使用。
相关的事情是使用 mo_consume传递从原子变量加载的值。像这样:
int tmp = shared_var.load(std::memory_order_consume);
func(tmp);
Run Code Online (Sandbox Code Playgroud)
func可以使用该 arg 作为数组的索引atomic<int>来执行mo_relaxed加载。为了在shared_var.load没有内存屏障的情况下对加载进行依赖性排序,代码func生成必须确保加载对 arg 具有 asm 数据依赖性,即使 C++ 代码执行类似tmp -= tmp;编译器通常只会处理与tmp = 0;(杀死以前的值)相同。
但是[[carries_dependency]]会使编译器在实现类似array[idx+tmp].
原子变量值已经被消耗掉了,那么这个函数携带的是什么依赖?
“已经消耗”不是一个有效的概念。整点consume,而不是acquire为以后负荷正确排序,因为他们有一个数据的依赖mo_consume加载结果,让您避免的障碍。如果您希望在原始加载之后排序,那么以后的每个加载都需要这样的依赖项;没有任何意义可以说一个值“已被消耗”。
如果由于某个函数缺少 Carry_dependency 而最终插入了一个障碍来促进消费获得,那么以后的函数将不需要另一个障碍,因为您可以说该值“已经获得”。(虽然这不是标准术语。您应该在加载后订购第一个屏障之后说代码。)
了解 Linux 内核如何处理这个问题可能会很有用,它们的手工原子和它们支持的编译器集有限。在https://github.com/torvalds/linux/blob/master/Documentation/memory-barriers.txt 中搜索“依赖”
,并注意“控制依赖”(如 )if(flag) data.load()与数据依赖(如 )之间的区别data[idx].load。
IIRC,即使是 C++ 也不保证mo_consume当依赖项是像if(x.load(consume)) tmp=y.load();.
请注意,编译器会有时候把一个数据依赖关系到控制依赖性如果只有2例如可能的值。mo_consume如果值来自mo_consume负载或[[carries_dependency]]函数 arg ,这将中断,并且是不允许的优化。这就是为什么它难以实施的部分原因;它需要教授大量关于数据依赖排序的优化过程,而不是仅仅期望用户编写不做通常会优化掉的事情的代码。(喜欢tmp -= tmp;)
| 归档时间: |
|
| 查看次数: |
148 次 |
| 最近记录: |