“[[carries_dependency]]”属性是什么意思?

81

有人能用普通人听得懂的语言解释一下吗?


4
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2643.html - DumbCoder
1
@DumbCoder:谢谢,这确实比N2390本身好很多,不幸的是它重定向到很多其他“理解此提案必要”的文献...看起来我的问题太宽泛了 :) - Yakov Galka
1
在普通语言中,它是一个可选的优化提示(目前每个编译器都未实现或忽略),理论上可以允许编译器在共享很少修改但频繁读取的数据时生成稍微更好的多线程代码。幸好措辞如此扭曲,以至于没有人会使用它 :-) - Damon
我还想指向这个问题,其中提到了Anthony Williams的一本好书:https://dev59.com/a2445IYBdhLWcg3wTIZf - Omnifarious
1
@Damon,问题不在于措辞含糊,而在于语义完全荒谬:d?a:b会破坏依赖性,但d->static_fun()却不会……这毫无意义。而且它也不允许“稍微好一点的多线程代码”,避免频繁操作的屏障对某些处理器来说显著更好。"当经常读取的数据很少修改时,可以共享" Consume也适用于经常修改的数据,只要有指向它的指针,并且记录只发布一次并且只读一次,这通常是规范。 - curiousguy
2个回答

63

[[carries_dependency]] 用于允许依赖关系跨越函数调用。在具有弱序架构(如IBM的POWER架构)的平台上与 std::memory_order_consume 结合使用时,这可能会使编译器生成更好的代码。

特别地,如果使用 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

在第一行中,该依赖关系是显式的,因此编译器知道 local 被解引用,并且必须确保依赖链被保留以避免在 POWER 上造成障碍。

在第二行中,print 的定义是不透明的(假设没有内联),因此编译器必须发出障碍,以确保在 print 中读取 *p 返回正确的值。

在第三行中,编译器可以假设虽然 print2 也是不透明的,但从参数到解引用值的依赖关系在指令流中得到保留,因此在 POWER 上不需要障碍。显然,print2 的定义实际上必须保留此依赖关系,因此属性还将影响生成的 print2 代码。


21
这是一个很好的答案。但是...你会如何编写函数来保留依赖?一个编写不当的函数会是什么样子,造成什么后果? - Omnifarious
2
顺便说一下,我得到了你的书的预发行PDF副本。这是一本很棒的书。虽然如此,我真的希望你能一直沿用“在小隔间接电话的人”这个比喻。那是一个非常好的理解正在发生的事情的工具。 - Omnifarious
4
从源代码的角度来看,你只需要使用[[carries_dependency]]属性,不必调用std::kill_dependency函数,除非你确实需要它。编译器会确保在生成的代码中不会破坏依赖关系链。 - Anthony Williams
10
@AnthonyWilliams:我和Omnifarious一样认为,你似乎只需要在所有函数声明中加上[[carries_dependency]],编译器就会神奇地生成更快的代码。我很想看一个不能使用[[carries_dependency]]或者必须使用std::kill_dependency的示例函数。 - Marc Mutz - mmutz
2
@MarcMutz-mmutz "编译器会神奇地生成更快的代码" 错误。编译器将生成相等或不太优化(更慢)的代码。 - curiousguy
显示剩余8条评论

-2
简而言之,如果存在carries_dependency属性,则函数的生成代码应该针对实际参数来自另一个线程并具有依赖性的情况进行优化。同样适用于返回值。如果这种假设不成立(例如在单线程程序中),可能会导致性能不足。但是,缺少[[carries_dependency]]也可能导致相反情况下的性能不佳...除了性能变化外,不应该有其他影响。
例如,指针解引用操作取决于先前获取指针的方式,如果指针p的值来自另一个线程(通过“consume”操作),则将考虑并显示由该另一个线程分配给*p的值。可能存在另一个等于p的指针q(q==p),但由于其值不来自另一个线程,*q的值可能与*p的值不同。实际上,*q可能会引发某种“未定义行为”(因为访问与进行分配的另一个线程不协调的内存位置)。
实际上,在某些工程案例中,似乎存在一些功能上的大问题(以及心理问题).... >:-)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接