3

我在这篇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]] 注释,这个栅栏可能不再需要,因为调用者现在负责维护依赖树。"

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

4

1 回答 1

6

仅供参考,memory_order_consume(and [[carries_dependency]]) 基本上已被弃用,因为编译器很难按照 C++11 设计规则的方式有效和正确地实现规则。(和/或因为[[carries_dependency]]和/或kill_dependency最终会被各地需要。)请参阅P0371R1:暂时不鼓励 memory_order_consume

当前的编译器只是将mo_consume其视为mo_acquire(因此在需要一个的 ISA 上,在消耗负载之后立即设置一个障碍)。如果您希望数据依赖排序的性能没有障碍,您必须通过mo_relaxed仔细使用和编码来欺骗编译器,以避免可能使编译器创建没有实际依赖关系的 asm 的事情。(例如 Linux RCU)。请参阅C++11:memory_order_relaxed 和 memory_order_consume 之间的区别以获取更多详细信息和链接,以及mo_consume旨在公开的 asm 功能。

内存订单也消耗 C11中的使用量。
理解依赖顺序的概念(在 asm 中)对于理解这个 C++ 特性是如何设计的基本上是必不可少的。

当 [an] atomic 变量作为参数传递给函数时,编译器将引入栅栏硬件指令......

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

相关的事情是通过mo_consume 传递从原子变量加载的值。像这样:

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

func可以使用该 arg 作为数组的索引atomic<int>来进行mo_relaxed加载。shared_var.load为了在没有内存屏障的情况下对加载进行依赖排序,code-gen forfunc必须确保加载对 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 个可能的值,编译器有时会将数据依赖项转换为控制依赖项如果值来自负载或函数 arg ,这将破坏mo_consume,并且是不允许的优化。这是难以实施的部分原因;它需要教授大量关于数据依赖排序的优化传递,而不是仅仅期望用户编写不做通常会优化掉的事情的代码。(喜欢)mo_consume[[carries_dependency]]tmp -= tmp;

于 2020-09-29T06:38:04.750 回答