有人可以用凡人都能理解的语言来解释它吗?
d?a:b
打破了依赖关系,但d->static_fun()
没有……那是没有道理的。而且它不允许“稍微更好的多线程代码”,在某些处理器上,避免频繁操作的围栏会更好。“当很少修改时,共享经常读取的数据”只要有指向它的指针,消费也适用于经常修改的数据,并且记录一旦发布就只能读取,这是常态。
有人可以用凡人都能理解的语言来解释它吗?
d?a:b
打破了依赖关系,但d->static_fun()
没有……那是没有道理的。而且它不允许“稍微更好的多线程代码”,在某些处理器上,避免频繁操作的围栏会更好。“当很少修改时,共享经常读取的数据”只要有指向它的指针,消费也适用于经常修改的数据,并且记录一旦发布就只能读取,这是常态。
Answers:
[[carries_dependency]]
用于允许依赖关系在函数调用之间传递。当与std::memory_order_consume
诸如IBM POWER架构之类的顺序较弱的平台上的线程之间传输值一起使用时,这可能会使编译器生成更好的代码。
特别是,如果将使用读取的值memory_order_consume
传递给函数,则如果不带[[carries_dependency]]
,则编译器可能必须发出内存围栅指令以确保维持适当的内存排序语义。如果给参数加上注释,[[carries_dependency]]
则编译器可以假定函数主体将正确地承载依赖项,并且可能不再需要此防护。
类似地,如果一个函数返回一个加载有memory_order_consume
或从该值派生的值,则[[carries_dependency]]
可能不需要编译器插入fence指令即可保证适当的内存排序语义得以保留。有了[[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
是不透明的(假定它没有内联),因此编译器必须发出篱笆,以确保*p
in读取print
返回正确的值。
在第(3)行上,编译器可以假定,尽管print2
它也是不透明的,但从参数到取消引用的值的依赖关系仍保留在指令流中,并且在POWER上不需要隔离。显然,的定义print2
实际上必须保留此依赖关系,因此该属性还将影响为生成的代码print2
。
[[carries_dependency]]
属性,std::kill_dependency
除非您是故意的,否则不要调用它。然后,编译器将确保它不会破坏所生成代码中的依赖关系链。
[[carries_dependency]]
编译器就会神奇地生成更快的代码。我会对无法使用[[carries_dependency]]
或必须使用的示例函数感兴趣std::kill_dpendency
。
简而言之,我认为,如果有carry_dependency属性,则应针对一种情况优化函数生成的代码,此时实际参数确实来自另一个线程并带有依赖项。对于返回值也是如此。如果该假设不成立,则可能会缺乏性能(例如,在单线程程序中)。但是,如果缺少[[carries_dependency]],则在相反的情况下可能会导致性能下降。
例如,指针取消引用操作取决于先前获取指针的方式,并且如果指针p的值来自另一个线程(通过“消耗”操作),则应考虑该另一个线程先前为* p分配的值。并且可见。可能存在另一个等于p(q == p)的指针q,但是由于其值不是来自其他线程,因此* q的值可能与* p不同。实际上* q可能会引起某种“未定义的行为”(因为访问内存位置与进行分配的另一个线程不协调)。
确实,在某些工程案例中,内存(和思维)的功能似乎存在一些大错误。...::-)