首页 文章

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

提问于
浏览
54

std::kill_dependency 已经发现 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的调用,例如,通过执行预测[r2]值的推测优化 .

为了调用 do_something_with ,需要值 a[r2] . 假设编译器"knows"数组填充了零,它可以优化对 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 回答

  • 11

    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
    

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

  • 3

    除了另一个答案之外,我将指出,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%的开发人员可能永远不会在需要此功能的平台上工作(或者根本没有任何影响) .

  • 0

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

    现在,假设读者读取指针,读取新数据,然后再返回并再次读取指针,发现指针没有告诉指针没有再次更新,所以通过 consume 语义他可以't use a cached copy of the data but has to read it again from memory. (Or to think of it another way, the hardware and compiler can't推测性地在读取指针之前移动数据的读取 . )

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

  • 39

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

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

相关问题