`std :: kill_dependency`做了什么,为什么我要使用它?

时间:2011-08-22 16:16:53

标签: c++ multithreading memory-model c++11

我一直在阅读有关新的C ++ 11内存模型的内容,并且我发现了std::kill_dependency函数(§29.3/ 14-15)。我很难理解为什么我会想要使用它。

我在N2664 proposal中找到了一个例子,但它没有多大帮助。

首先显示没有std::kill_dependency的代码。这里,第一行将依赖关系带入第二行,它依赖于索引操作,然后将依赖关系带入do_something_with函数。

r1 = x.load(memory_order_consume);
r2 = r1->index;
do_something_with(a[r2]);

还有一个例子,使用std::kill_dependency来打破第二行和索引之间的依赖关系。

r1 = x.load(memory_order_consume);
r2 = r1->index;
do_something_with(a[std::kill_dependency(r2)]);

据我所知,这意味着索引和对do_something_with的调用不是在第二行之前排序的依赖项。根据N2664:

  

这允许编译器将调用重新排序为do_something_with,例如,通过执行预测a[r2]的值的推测性优化。

为了拨打do_something_with,需要值a[r2]。假设编译器“知道”数组填充了零,它可以优化对do_something_with(0);的调用,并根据需要相对于其他两条指令重新排序此调用。它可以产生以下任何一个:

// 1
r1 = x.load(memory_order_consume);
r2 = r1->index;
do_something_with(0);
// 2
r1 = x.load(memory_order_consume);
do_something_with(0);
r2 = r1->index;
// 3
do_something_with(0);
r1 = x.load(memory_order_consume);
r2 = r1->index;

我的理解是否正确?

如果do_something_with通过其他方式与另一个线程同步,那对于x.load调用和另一个线程的排序意味着什么?

假设我的描述是正确的,那还有一件事让我感到困惑:当我编写代码时,有什么理由导致我选择杀死依赖?

4 个答案:

答案 0 :(得分:39)

memory_order_consume的目的是确保编译器不会执行某些可能会破坏无锁算法的不幸优化。例如,请考虑以下代码:

int t;
volatile int a, b;

t = *x;
a = t;
b = t;

符合标准的编译器可能会将其转换为:

a = *x;
b = *x;

因此,a可能不等于b。它也可以:

t2 = *x;
// use t2 somewhere
// later
t = *x;
a = t2;
b = t;

通过使用load(memory_order_consume),我们要求在使用点之前不要移动正在加载的值的使用。换句话说,

t = x.load(memory_order_consume);
a = t;
b = t;
assert(a == b); // always true

标准文件考虑的情况是您可能只对订购结构的某些字段感兴趣。例子是:

r1 = x.load(memory_order_consume);
r2 = r1->index;
do_something_with(a[std::kill_dependency(r2)]);

这指示编译器允许它有效地执行此操作:

predicted_r2 = x->index; // unordered load
r1 = x; // ordered load
r2 = r1->index;
do_something_with(a[predicted_r2]); // may be faster than waiting for r2's value to be available

甚至这个:

predicted_r2 = x->index; // unordered load
predicted_a  = a[predicted_r2]; // get the CPU loading it early on
r1 = x; // ordered load
r2 = r1->index; // ordered load
do_something_with(predicted_a);

如果编译器知道do_something_with不会改变r1或r2的加载结果,那么它甚至可以将它一直提升:

do_something_with(a[x->index]); // completely unordered
r1 = x; // ordered
r2 = r1->index; // ordered

这使编译器在优化方面有了更多的自由。

答案 1 :(得分:11)

除了另一个答案之外,我还要指出,C ++社区最权威的领导者之一Scott Meyers非常强烈地抨击了memory_order_consume。他基本上说他相信它在标准中没有位置。他说有两种情况,其中memory_order_consume有任何影响:

  • 异域架构旨在支持1024+核心共享内存机器。
  • DEC Alpha

是的,再一次,DEC Alpha通过使用任何其他芯片中没有出现的优化,直到多年后才使用荒谬的专用机器,从而进入臭名昭着。

特定的优化是那些处理器允许在实际获得该字段的地址之前取消引用字段(即,它甚至在使用x的预测值查找x之前查找x-> y)。然后它返回并确定x是否是它预期的值。成功后,它节省了时间。失败时,它必须返回并再次获得x-> y。

Memory_order_consume告诉编译器/体系结构这些操作必须按顺序发生。但是,在最有用的情况下,最终会想要做(x-> y.z),其中z不会改变。 memory_order_consume将强制编译器按顺序保持x y和z。 kill_dependency(x-> y).z告诉编译器/架构它可能会继续进行这种恶意的重新排序。

99.999%的开发人员可能永远不会在需要此功能的平台上工作(或者根本没有任何影响)。

答案 2 :(得分:3)

kill_dependency的通常用例来自以下内容。假设您要对非平凡的共享数据结构进行原子更新。执行此操作的典型方法是以非原子方式创建一些新数据,并将指针从数据结构原子摆动到新数据。一旦你这样做,你就不会改变新的数据,直到你把指针从它转向别的东西(并等待所有读者腾出)。这种范例被广泛使用,例如, Linux内核中的read-copy-update。

现在,假设读者读取指针,读取新数据,稍后返回并再次读取指针,发现指针没有改变。硬件无法判断指针是否未再次更新,因此通过consume语义,他无法使用数据的缓存副本,但必须再次从内存中读取它。 (或者以另一种方式来思考,硬件和编译器在读取指针之前不能推测性地移动数据读取。)

这是kill_dependency拯救的地方。通过将指针包装在kill_dependency中,您可以创建一个不再传播依赖关系的值,允许通过指针访问以使用新数据的缓存副本。

答案 3 :(得分:0)

我的猜测是它可以实现这种优化。

r1 = x.load(memory_order_consume);
do_something_with(a[r1->index]);