其他分享
首页 > 其他分享> > c – 为什么编译器没有合并多余的std :: atomic写入?

c – 为什么编译器没有合并多余的std :: atomic写入?

作者:互联网

我想知道为什么没有编译器准备将相同值的连续写入合并到单个原子变量,例如:

#include <atomic>
std::atomic<int> y(0);
void f() {
  auto order = std::memory_order_relaxed;
  y.store(1, order);
  y.store(1, order);
  y.store(1, order);
}

我尝试过的每个编译器都会发出三次上面的编写.什么合法的,无种族的观察者可以看到上述代码与具有单次写入的优化版本之间的差异(即,不是“假设”规则适用)?

如果变量是易变的,那么显然不适用优化.在我的情况下有什么阻止它?

这是compiler explorer中的代码.

解决方法:

所写的C 11 / C 14标准确实允许将三个商店折叠/合并成一个最终值的商店.即使在这样的情况下:

  y.store(1, order);
  y.store(2, order);
  y.store(3, order); // inlining + constant-folding could produce this in real code

该标准并不保证旋转y(带有原子载荷或CAS)的观察者会看到y == 2.依赖于此的程序会产生数据竞争错误,但只有花园种类的种类错误种类,而不是C Undefined Behavior类型的数据竞争. (只有非原子变量才是UB).期望有时看到它的程序不一定是错误的. (见下文:进度条.)

可以在C抽象机器上选择任何可能的排序(在编译时)作为始终发生的排序.这是执行中的as-if规则.在这种情况下,就好像所有三个商店在全局订单中背靠背地发生一样,在y = 1和y = 3之间没有发生其他线程的加载或存储.

它不依赖于目标架构或硬件;就像compile-time reordering的轻松原子操作一样,即使是针对强烈排序的x86也是如此.编译器不必保留您考虑所编译的硬件时可能期望的任何内容,因此您需要屏障.障碍可以编译为零asm指令.

那么为什么编译器不进行这种优化呢?

这是一个实施质量问题,可以改变实际硬件上观察到的性能/行为.

最明显的问题是进度条.将商店从一个循环中沉没(不包含其他原子操作)并将它们全部折叠成一个将导致进度条保持在0然后在结束时达到100%.

在你不想要它的情况下,没有C 11 std :: atomic方法阻止它们这样做,所以现在编译器只选择永远不会将多个原子操作合并为一个. (将它们合并为一个操作不会改变它们相对于彼此的顺序.)

编译器编写者已经正确地注意到程序员期望每次源执行y.store()时,原子存储实际上会发生在内存中. (参见这个问题的大多数其他答案,声称商店需要单独发生,因为可能的读者等待看到中间值.)即它违反了principle of least surprise.

但是,有些情况下它会非常有用,例如在循环中避免无用的shared_ptr ref count inc / dec.

显然,任何重新排序或合并都不能违反任何其他订购规则.例如,num; num–;仍然必须完全阻止运行时和编译时重新排序,即使它不再触及num的内存.

正在讨论扩展std :: atomic API以使程序员能够控制这种优化,此时编译器将能够在有用时进行优化,即使在非故意低效的精心编写的代码中也可能发生这种情况.以下工作组讨论/提案链接中提到了一些有用的优化案例示例:

> http://wg21.link/n4455:N4455没有Sane编译器将优化原子
> http://wg21.link/p0062:WG21 / P0062R1:编译器什么时候应该优化原子?

另请参阅有关Richard Hodges对Can num++ be atomic for ‘int num’?的回答的相同主题的讨论(请参阅注释).另见my answer的最后一节到同一个问题,我更详细地论证了这种优化是允许的. (在此处简短说明,因为那些C工作组链接已经确认了当前编写的标准允许它,并且当前的编译器不会故意进行优化.)

在当前标准内,volatile atomic< int> y将是确保不允许对其进行优化的一种方法. (正如Herb Sutter points out in an SO answer,volatile和atomic已经分享了一些要求,但它们是不同的).另见关于cppreference的std::memory_order‘s relationship with volatile.

不允许对易失性对象的访问进行优化(例如,因为它们可能是内存映射的IO寄存器).

使用挥发性原子< T>大多数修复了进度条的问题,但它有点难看,如果/当C决定控制优化的不同语法时,它可能在几年内看起来很傻,因此编译器可以在实践中开始这样做.

我认为我们可以确信编译器在有办法控制它之前不会开始进行这种优化.希望它会成为某种选择(如memory_order_release_coalesce),当编译为C时,它不会改变现有代码C 11/14代码的行为.但它可能类似于wg21 / p0062中的提议:使用[[brittle_atomic]]标记非优化案例.

wg21 / p0062警告说,即使是不稳定的原子也不会解决所有问题,并且不鼓励它用于此目的.它给出了这个例子:

if(x) {
    foo();
    y.store(0);
} else {
    bar();
    y.store(0);  // release a lock before a long-running loop
    for() {...} // loop contains no atomics or volatiles
}
// A compiler can merge the stores into a y.store(0) here.

即使使用易失性原子< int> y,允许编译器将y.store()从if / else中删除,并且只执行一次,因为它仍然使用相同的值完成1个存储. (这将在else分支中的长循环之后).特别是如果商店只是放松或释放而不是seq_cst.

volatile确实停止了问题中讨论的合并,但是这指出了对原子<>的其他优化.对于真实的表现也可能有问题.

不优化的其他原因包括:没有人编写复杂的代码,允许编译器安全地进行这些优化(不会出错).这还不够,因为N4455说LLVM已经实现或者可以轻松实现它提到的几个优化.

然而,令人困惑的程序员理由肯定是合理的.无锁代码很难在一开始就正确编写.

在你使用原子武器时不要随意:它们并不便宜而且不能进行太多优化(目前根本没有).但是,使用std :: shared_ptr< T>来避免冗余原子操作并不总是容易的,因为它没有非原子版本(尽管one of the answers here提供了一种简单的方法来为gcc定义shared_ptr_unsynchronized< T>).

标签:stdatomic,c,c11,multithreading,compiler-optimization
来源: https://codeday.me/bug/20190918/1810772.html