【问题标题】:What does the [[carries_dependency]] attribute mean?[[carries_dependency]] 属性是什么意思?
【发布时间】:2011-06-20 12:39:29
【问题描述】:

有人能用凡人都能理解的语言解释吗?

【问题讨论】:

  • @DumbCoder:谢谢,这绝对比 N2390 本身要好,不幸的是,它重定向到许多其他“理解这个提议所必需的”论文......似乎我的问题过于宽泛:)
  • 在普通语言中,它是一个可选的优化提示(目前每个编译器都未实现或忽略它),理论上可以允许编译器在很少修改、经常读取数据时生成稍微更好的多线程代码是共享的。干得好,措辞如此扭曲以至于无论如何都没有人会使用它:-)
  • 我还向您指出这个问题,其中提到了 Anthony Williams 的一本好书:stackoverflow.com/questions/4938258/…
  • @Damon 并不是措辞被扭曲,而是语义完全愚蠢:d?a:b 打破了依赖关系,但d->static_fun() 没有......这没有意义。而且它不允许“稍微更好的多线程代码”,避免频繁操作的围栏在某些处理器上明显更好。 “很少修改时,共享频繁读取的数据”consume也适用于频繁修改的数据,只要有指向它的指针,并且记录只在发布后读取,这是常态无论如何。

标签: c++ multithreading memory-model stdatomic carries-dependency


【解决方案1】:

[[carries_dependency]] 用于允许跨函数调用携带依赖项。这可能允许编译器在与 std::memory_order_consume 一起使用时生成更好的代码,以便在具有弱排序架构(如 IBM 的 POWER 架构)的平台上的线程之间传输值。

特别是,如果将使用memory_order_consume 读取的值传递给函数,而没有[[carries_dependency]],则编译器可能必须发出内存栅栏指令以确保支持适当的内存排序语义。如果参数带有[[carries_dependency]] 注释,那么编译器可以假设函数体将正确地携带依赖关系,并且可能不再需要这个栅栏。

类似地,如果一个函数返回一个用memory_order_consume 加载的值,或者从这样的值派生的值,那么如果没有[[carries_dependency]],编译器可能需要插入一个栅栏指令来保证支持适当的内存排序语义。使用[[carries_dependency]] 注解后,可能不再需要此栅栏,因为调用者现在负责维护依赖关系树。

例如

void print(int * val)
{
    std::cout<<*val<<std::endl;
}

void print2(int * [[carries_dependency]] val)
{
    std::cout<<*val<<std::endl;
}

std::atomic<int*> p;
int* local=p.load(std::memory_order_consume);
if(local)
    std::cout<<*local<<std::endl; // 1

if(local)
    print(local); // 2

if(local)
    print2(local); // 3

在第 (1) 行中,依赖关系是显式的,因此编译器知道 local 已取消引用,并且它必须确保保留依赖关系链以避免在 POWER 上出现栅栏。

在第 (2) 行中,print 的定义是不透明的(假设它不是内联的),因此编译器必须发出栅栏以确保在 print 中读取 *p 返回正确的值.

在第 (3) 行,编译器可以假设虽然print2 也是不透明的,但从参数到取消引用值的依赖关系保留在指令流中,并且在 POWER 上不需要围栏。显然,print2 的定义实际上必须保留这种依赖关系,因此该属性也会影响为print2 生成的代码。

【讨论】:

  • 这是一个很好的答案。但是...您将如何对函数进行编码以保留依赖关系?编码不当的函数会是什么样子?后果是什么?
  • 顺便说一句,我得到了你的书的 PDF 版本。这是一本很棒的书。我真的希望你一直坚持你的“在隔间里接电话的人”的比喻。这是了解正在发生的事情的绝佳工具。
  • 从源的 POV 来看,您需要做的就是使用 [[carries_dependency]] 属性,除非您是认真的,否则不要调用 std::kill_dependency。然后编译器将确保它不会破坏生成代码中的依赖链。
  • @AnthonyWilliams:我支持 Omnifarious:听起来你只需要用[[carries_dependency]] 粘贴所有函数声明,编译器就会神奇地生成更快的代码。我对您不能使用[[carries_dependency]]或必须使用std::kill_dpendency的示例函数感兴趣。
  • @MarcMutz-mmutz "编译器会神奇地生成更快的代码" 错误。编译器将生成相同或优化程度较低(较慢)的代码。
【解决方案2】:

简而言之,我认为,如果有 Carry_dependency 属性,生成的函数代码应该针对一种情况进行优化,即实际参数将真正来自另一个线程并携带依赖项。对于返回值也是如此。如果该假设不成立(例如在单线程程序中),则可能会缺乏性能。但也缺少 [[carries_dependency]] 可能会导致相反情况下的糟糕表现......除了性能改变之外不会发生其他影响。

例如,指针解引用操作取决于指针先前是如何获得的,如果指针 p 的值来自另一个线程(通过“消费”操作),则另一个线程先前分配给 *p 的值是考虑和可见。可能有另一个指针 q 等于 p (q==p),但由于它的值不是来自另一个线程,所以 *q 的值可能与 *p 不同。实际上 *q 可能会引发一种“未定义的行为”(因为访问内存位置与另一个进行分配的线程不协调)。

真的,在某些工程案例中,内存(和思维)的功能似乎存在一些大错误.... >:-)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-09-13
    • 2016-04-06
    • 2016-06-07
    • 1970-01-01
    • 2019-06-12
    • 2010-10-23
    • 2018-12-10
    • 1970-01-01
    相关资源
    最近更新 更多