[[carries_dependency]] 什么意思以及如何实现

[[carries_dependency]] what it means and how to implement

我在这篇 SO post 中阅读了有关 [[carries_dependency]] 的内容。

但是我无法理解的是接受的答案中的以下句子:

"In particular, if a value read with memory_order_consume is passed in to a function, then without [[carries_dependency]], then the compiler may have to issue a memory fence instruction to guarantee that the appropriate memory ordering semantics are upheld. If the parameter is annotated with [[carries_dependency]] then the compiler can assume that the function body will correctly carry the dependency, and this fence may no longer be necessary.

Similarly, if a function returns a value loaded with memory_order_consume, or derived from such a value, then without [[carries_dependency]] the compiler may be required to insert a fence instruction to guarantee that the appropriate memory ordering semantics are upheld. With the [[carries_dependency]] annotation, this fence may no longer be necessary, as the caller is now responsible for maintaining the dependency tree."

让我们一步步来:

"if a value read with memory_order_consume is passed in to a function, then without [[carries_dependency]], then the compiler may have to issue a memory fence instruction to guarantee that the appropriate memory ordering semantics are upheld."

因此,对于释放-使用内存模型中的原子变量,当原子变量作为参数传递给函数时,编译器将引入栅栏硬件指令,以便它始终具有原子变量的最新和更新值提供给函数。

下一个-

"If the parameter is annotated with [[carries_dependency]] then the compiler can assume that the function body will correctly carry the dependency, and this fence may no longer be necessary."

这让我很困惑 - 原子变量值已经被消耗,然后函数携带什么依赖?

同样-

"if a function returns a value loaded with memory_order_consume, or derived from such a value, then without [[carries_dependency]] the compiler may be required to insert a fence instruction to guarantee that the appropriate memory ordering semantics are upheld. With the [[carries_dependency]] annotation, this fence may no longer be necessary, as the caller is now responsible for maintaining the dependency tree."

从示例中不清楚它试图说明携带依赖项的意义是什么?

仅供参考,memory_order_consume(和 [[carries_dependency]])基本上已被弃用,因为编译器很难有效和正确地实现 C++11 设计规则的方式。 (And/or 因为 [[carries_dependency]] and/or kill_dependency 最终到处都需要。)见 P0371R1: Temporarily discourage memory_order_consume.

当前的编译器只是将 mo_consume 视为 mo_acquire(因此在需要一个的 ISA 上,在消耗负载之后放置一个屏障)。如果您想要无障碍地执行数据依赖排序,则必须通过使用 mo_relaxed 来欺骗编译器,并仔细编写代码以避免编译器可能在没有实际依赖的情况下创建 asm。 (例如 Linux RCU)。有关详细信息和相关链接,以及 mo_consume 旨在公开的 asm 功能,请参阅 C++11: the difference between memory_order_relaxed and memory_order_consume

还有.
理解依赖排序的概念(在 asm 中)对于理解这个 C++ 特性是如何设计的基本上是必不可少的。

When [an] atomic variable is being passed as a parameter to the function the compiler will introduce a fence hardware instruction ...

您首先不会“将原子变量传递给”函数;那甚至意味着什么?如果您传递一个指针或引用到一个原子对象,该函数将从它自己加载,并且该函数的源代码将使用 memory_order_consume 或不使用。

相关的事情是从具有 mo_consume 的原子变量传递值 loaded。像这样:

    int tmp = shared_var.load(std::memory_order_consume);
    func(tmp);

func 可以使用该 arg 作为 atomic<int> 数组的索引来执行 mo_relaxed 加载。对于 shared_var.load 之后的负载 dependency-ordered 即使没有内存屏障, code-gen for func 必须确保负载对 arg 具有 asm 数据依赖性,即使如果 C++ 代码执行类似 tmp -= tmp; 的操作,编译器通常会将其视为与 tmp = 0; 相同(杀死先前的值)。

但是 [[carries_dependency]] 会使编译器在实现类似 array[idx+tmp].

时仍然引用具有数据依赖性的归零值

the atomic variable value is already consumed and then what dependency the function is carried?

“已消费”不是一个有效的概念。 consume 而不是 acquire 的全部要点是后面的加载顺序正确,因为它们对 mo_consume 加载结果有 data 依赖性,让你避开障碍。如果您希望它在原始加载之后排序,那么以后的每个加载都需要这样的依赖关系;说一个值“已经消耗”是没有意义的。

如果你最终插入了一个屏障来促进消费获取,因为缺少一个函数 carries_dependency,后面的函数将不需要另一个屏障,因为你可以说该值“已经获得” . (尽管这不是标准术语。您会在加载后订购第一个障碍后说代码。)


了解 Linux 内核如何使用它们的 hand-rolled 原子和它们支持的有限编译器集来处理这个问题可能会很有用。在中搜索“依赖项” 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;