为什么不编译器合并多余的std :: atomic写道?

我想知道为什么没有编译器准备将相同的值连续写入一个单一的primefacesvariables,例如:

#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); } 

我试过的每一个编译器都会发出上面的三次。 什么合法的,无竞争的观察者可以看到上面的代码和单个写入的优化版本(即不适用“as-if”规则)之间的区别?

如果variables是易变的,那么显然没有优化是适用的。 什么阻止了我的情况?

这是编译器资源pipe理器中的代码。

如所写的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旋转的观察者(具有primefaces载荷或CAS)将看到y == 2 。 一个依赖于此的程序将会产生数据竞争错误,但是只有种类繁多的错误types,而不是C ++未定义行为types的数据竞争。 (这只是与非primefacesvariablesUB)。 预计有时会看到它的程序并不一定是越野车。 (请看下面的re:进度条。)

任何在C ++抽象机器上可能的sorting都可以在编译时挑选出来,因为sorting总是会发生的 。 这是行动中的假设规则。 在这种情况下,就好像所有三个商店都按照全局顺序背靠背发生, y=1y=3之间没有其他线程的加载或存储。

它不依赖于目标架构或硬件; 就像放宽primefaces操作的编译时重新sorting也是允许的,即使在针对强sorting的x86时也是如此。 编译器不需要保留任何你想要的编译硬件的想法,所以你需要屏障。 这些障碍可能编译成零asm指令。


那为什么编译器不这样做呢?

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

最明显的一个问题是进度条 。 将商店从一个循环(不包含其他primefaces操作)中剔除并将它们全部合并为一个会导致进度条停留在0,然后在最后完成100%。

没有C ++ 11 std::atomic方法来阻止它们在你不需要的情况下执行它,所以现在编译器只是简单地select从不将多个primefaces操作合并成一个。 (将它们合并成一个操作不会相互改变顺序)

编译器编写者已经正确地注意到,程序员期望每当源代码执行y.store()时,一个primefaces存储实际上将发生在内存中。 (请参阅这个问题的大部分其他答案,声称商店需要单独发生,因为可能的读者等待看到一个中间值。)它违反了最less惊喜的原则 。

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

显然任何重新sorting或合并都不能违反任何其他sorting规则。 例如, num++; num--; num++; num--; 对运行时和编译时重新sorting仍然是完全的障碍,即使它不再在num处触及内存。


目前正在讨论如何扩展std::atomic API以使程序员能够控制这样的优化,在这种情况下,编译器可以在有用的时候进行优化,即使是在仔细编写的代码中,这些代码也不是故意低效的。 以下工作小组讨论/build议链接中提到了一些有用的优化案例:

理查德·霍奇斯(Richard Hodges)的回答也可以讨论关于这个问题的可能性num ++是'int num'的primefaces吗? (见评论)。 另请参阅我对同一问题的回答的最后一部分, 我更详细地论述了这种优化是允许的。 (在这里保持简短,因为那些C ++工作组链接已经承认当前编写的标准确实允许它,而且目前的编译器并不是特意优化的。)


在当前的标准中, volatile atomic<int> y将是确保不允许优化存储的一种方法。 (正如Herb Sutter在一个SO答案中指出的那样 , volatileatomic已经有一些要求,但是它们是不同的)。 另请参阅std::memory_order与 cppreference 上volatile的关系 。

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

不要把你所有的atomicvariables改为volatile atomic , 标准委员会可能会select其他的东西(因为volatile atomic是丑陋的,并且滥用了volatile的含义)。 我想我们可以确信编译器不会开始做这种优化,直到有一种方法来控制它。 希望这将是一种selectjoin(就像一个memory_order_release_coalesce )不会改变现有的代码C ++ memory_order_release_coalesce代码编译为C ++时的行为。 但它可能像wg21 / p0062中的提议,用[[brittle_atomic]]标记不要优化的情况。

wg21 / p0062警告说,即使是volatile atomic也不能解决所有问题,并且不鼓励它用于这个目的 。 它给出了这个例子:

 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. 

即使使用volatile atomic<int> y ,编译器也可以将y.store()if/else取出,只做一次,因为它仍然只是在存储中执行相同的值。 (这将在else分支中的长循环之后)。

volatile会停止问题中讨论的聚合,但是这指出对atomic<>其他优化对于实际性能也是有问题的。


不优化的其他原因包括:没有人编写复杂的代码,使编译器能够安全地进行这些优化(没有发生错误)。 这是不够的,因为N4455表示LLVM已经实现或可以轻松实现它提到的几个优化。

然而,令人困惑的程序员的理由当然是合理的。 无锁代码很难正确写入。

不要随意使用primefaces武器:它们并不便宜,不会优化太多(目前根本没有)。 尽pipe避免使用std::shared_ptr<T>进行多余的primefaces操作并不总是那么容易,因为它没有非primefaces版本(尽pipe这里的一个答案提供了一个简单的方法来为gcc定义一个shared_ptr_unsynchronized<T> )。

你指的是消除死锁。

primefaces商店不是被禁止的,但是很难certificateprimefaces商店是合格的。

传统的编译器优化(比如死存储消除)可以在primefaces操作上执行,甚至可以按顺序执行。
优化器必须小心避免在同步点之间这样做,因为另一个执行线程可以观察或修改内存,这意味着传统的优化必须考虑比通常在考虑优化primefaces操作时更多的中介指令。
在消除死店的情况下,仅仅certificate一个primefaces商店后期支配另一个商店以消除另一个商店是不够的。

从N4455没有Sane编译器会优化primefaces

在一般情况下,primefacesDSE的问题在于它涉及寻找同步点,在我的理解中这个术语意味着在发生的代码中的点– 在线程A上的指令和另一个线程B上的指令之间的关系之前

考虑由线程A执行的这个代码:

 y.store(1, std::memory_order_seq_cst); y.store(2, std::memory_order_seq_cst); y.store(3, std::memory_order_seq_cst); 

可以优化为y.store(3, std::memory_order_seq_cst)吗?

如果一个线程B正在等待看到y = 2 (比如用一个CAS),那么就不会观察到代码是否被优化了。

但是,根据我的理解,在y = 2上进行B循环和CASsing是一个数据竞争,因为在两个线程的指令之间没有总的顺序。
在B的循环可观察(即允许)之前执行A的指令的执行,因此编译器可以优化为y.store(3, std::memory_order_seq_cst)

如果线程A和线程B在线程A中的存储之间以某种方式同步,则优化将不被允许(将导致部分顺序,可能导致B潜在地观察y = 2 )。

certificate没有这样的同步是困难的,因为它涉及考虑更广泛的范围并考虑到架构的所有怪癖。

就我的理解而言,由于primefaces操作的年龄相对较小,以及关于内存sorting,可见性和同步的推理的困难,编译器不会对primefaces执行所有可能的优化,直到用于检测和理解必要的更强大的框架条件是build立的。

我相信你的例子是上面给出的计数线程的简化,因为它没有任何其他线程或任何同步点,我所看到的,我想编译器可以优化三个商店。

当您在一个线程中更改primefaces的值时,其他某个线程可能正在检查它并根据primefaces的值执行操作。 你给出的例子非常特殊,编译器开发人员不认为值得优化。 但是,如果一个线程正在为例如0等primefaces设置连续的值,另一个线程可能会在primefaces的值所指示的槽中放入某些东西。

总之,因为标准(例如[intro.multithread]中的20左右的[intro.multithread] )不允许。

发生之前,必须履行保证之前,除其他事项外,排除重新sorting或合并写作(第19段甚至明确表示重新sorting)。

如果你的线程一个接一个地写三个值给内存(比如1,2,3),一个不同的线程可能会读取这个值。 例如,如果线程中断(或者即使同时运行),并且另一个线程写入该位置,则观察线程必须以与发生的顺序完全相同的顺序来查看操作(通过调度或重合,或者无论什么原因)。 这是一个保证。

如果你只写了一半(甚至只有一个),这怎么可能呢? 事实并非如此。

如果你的线程写出1 -1 -1,而另一个线程偶尔写出2或3呢? 如果第三个线程观察到位置并等待某个特定的值,那么这个值就不会出现,因为它被优化了呢?

如果商店(和货物也)不按要求执行,则不可能提供给予的保证。 所有这些,并以相同的顺序。

注:我打算评论这个,但有点太罗嗦。

一个有趣的事实是,这种行为不在C ++的数据竞赛中。

第14页的注释21很有意思: http : //www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3690.pdf (我强调):

如果一个程序的执行包含一个数据竞争,如果它包含在不同线程中的两个冲突的行为,其中至less有一个不是primefaces的

同样在第11页注5:

“轻松”的primefaces操作不是同步操作,尽pipe像同步操作一样,它们不能促成数据竞争。

所以对于一个primefaces来说,一个冲突的行为从来就不是数据竞争 – 就C ++标准而言。

这些操作都是primefaces的(特别是放松),但这里没有数据竞争!

我同意在任何(合理的)平台上这两者之间没有可靠/可预测的差异:

 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); } 

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

但在定义提供的C ++内存模型中,它不是数据竞赛。

我不能很容易地理解为什么提供这个定义,但它确实交给开发者几张卡片,以便他们可能知道(在他们的平台上)在统计上工作的线程之间进行随意的交stream。

例如,设置一个值3次然后读取它将显示该位置的某种程度的争用。 这样的方法不是确定性的,但许多有效的并行algorithm不是确定性的。 例如,一个超时try_lock_until()总是一个竞争条件,但仍然是一个有用的技术。

看起来,C ++标准为你提供了关于“数据竞赛”的确定性,但是允许某些有趣的游戏和竞赛条件,这些竞赛条件最终分析了不同的东西。

简而言之,标准似乎指定了其他线程可能会看到3次设置值的“锤击”效果,其他线程必须能够看到这种效果(即使它们有时可能不会!)。 在这种情况下,其他线程可能在某些情况下看到的几乎所有的现代平台。

一个实际的模式用例,如果线程在不依赖或修改y更新之间做了一些重要的事情,可能是:*线程2读取y的值来检查线程1做了多less进展。

因此,也许线程1应该加载configuration文件作为步骤1,将其parsing的内容放入数据结构中作为步骤2,并显示主窗口为步骤3,而线程2正在等待步骤2完成,以便它可以并行执行另一个依赖于数据结构的任务。 (当然,这个例子需要获取/释放语义,而不是放松的顺序。)

我敢肯定,一致的实现允许线程1在任何中间步骤不更新y ,虽然我没有沉溺于语言标准,如果它不支持另一个线程轮询y可能永远不会看到的硬件,我会感到震惊值2。

但是,这是一个假设的情况,它可能是优化状态更新优化。 也许编译器会来这里说编译器为什么不这样做,但是有一个可能的原因就是让你自己在脚下开枪,或者至less让自己停留在脚趾上。

让我们走远离三家商店的病理情况,彼此相邻。 我们假设在商店之间进行了一些不平凡的工作,而且这样的工作根本不涉及(所以数据path分析可以确定三个商店实际上是多余的,至less在这个线程内),本身并没有引入任何内存障碍(所以别的东西不会强迫其他线程看到存储)。 现在,其他线程很可能有机会在商店之间完成工作,也许其他线程操纵y ,并且这个线程有一些理由需要将它重置为1(第二商店)。 如果前两家商店被放弃,那会改变行为。

编译器编写者不能只执行优化。 他们还必须说服自己,在编译器编写者打算应用它的情况下,优化是有效的,它不会被应用于无效的情况,也不会破坏事实上被破坏的代码,而是“工作“在其他实现。 这可能比优化本身更多的工作。

另一方面,我可以想象,在实践中(这是在应该做的工作,而不是基准程序),这种优化将节省很less的执行时间。

所以一个编译器编写者会看成本,然后看看好处和风险,也许会决定反对它。

由于包含在一个std :: atomic对象中的variables需要从多个线程中访问,所以应该期望它们的行为至less像使用volatile关键字一样声明。

在CPU架构引入caching行之前,这是标准和推荐的做法。

有人可能会说,std :: atomic <>是多核时代的volatilevariables。 正如在C / C ++中所定义的那样, volatile只能够同步来自单个线程的primefaces读取,而ISR修改variables(在这种情况下实际上是从主线程看到的primefaces写入)。

我个人感到宽慰的是没有编译器会优化掉写入一个primefacesvariables。 如果写入被优化掉了,那么如何保证每个写入操作都可能被其他线程中的读者看到? 不要忘记,这也是std :: atomic <>契约的一部分。

考虑这段代码,其结果会受到编译器疯狂优化的很大影响。

 #include <atomic> #include <thread> static const int N{ 1000000 }; std::atomic<int> flag{1}; std::atomic<bool> do_run { true }; void write_1() { while (do_run.load()) { flag = 1; flag = 1; flag = 1; flag = 1; flag = 1; flag = 1; flag = 1; flag = 1; flag = 1; flag = 1; flag = 1; flag = 1; flag = 1; flag = 1; flag = 1; flag = 1; } } void write_0() { while (do_run.load()) { flag = -1; flag = -1; flag = -1; flag = -1; } } int main(int argc, char** argv) { int counter{}; std::thread t0(&write_0); std::thread t1(&write_1); for (int i = 0; i < N; ++i) { counter += flag; std::this_thread::yield(); } do_run = false; t0.join(); t1.join(); return counter; } 

[编辑]起初,我并没有推进, volatile是实施primefaces的核心,但…

由于似乎对是否与primefaces有关的volatile有疑问,我调查了这个问题。 这里是来自VS2017 stl的primefaces实现。 正如我猜测的那样,volatile关键字无处不在。

 // from file atomic, line 264... // TEMPLATE CLASS _Atomic_impl template<unsigned _Bytes> struct _Atomic_impl { // struct for managing locks around operations on atomic types typedef _Uint1_t _My_int; // "1 byte" means "no alignment required" constexpr _Atomic_impl() _NOEXCEPT : _My_flag(0) { // default constructor } bool _Is_lock_free() const volatile { // operations that use locks are not lock-free return (false); } void _Store(void *_Tgt, const void *_Src, memory_order _Order) volatile { // lock and store _Atomic_copy(&_My_flag, _Bytes, _Tgt, _Src, _Order); } void _Load(void *_Tgt, const void *_Src, memory_order _Order) const volatile { // lock and load _Atomic_copy(&_My_flag, _Bytes, _Tgt, _Src, _Order); } void _Exchange(void *_Left, void *_Right, memory_order _Order) volatile { // lock and exchange _Atomic_exchange(&_My_flag, _Bytes, _Left, _Right, _Order); } bool _Compare_exchange_weak( void *_Tgt, void *_Exp, const void *_Value, memory_order _Order1, memory_order _Order2) volatile { // lock and compare/exchange return (_Atomic_compare_exchange_weak( &_My_flag, _Bytes, _Tgt, _Exp, _Value, _Order1, _Order2)); } bool _Compare_exchange_strong( void *_Tgt, void *_Exp, const void *_Value, memory_order _Order1, memory_order _Order2) volatile { // lock and compare/exchange return (_Atomic_compare_exchange_strong( &_My_flag, _Bytes, _Tgt, _Exp, _Value, _Order1, _Order2)); } private: mutable _Atomic_flag_t _My_flag; }; 

MS stl中的所有专业化的关键function都使用volatile。

这是关键function之一的声明:

  inline int _Atomic_compare_exchange_strong_8(volatile _Uint8_t *_Tgt, _Uint8_t *_Exp, _Uint8_t _Value, memory_order _Order1, memory_order _Order2) 

你会注意到所需的volatile uint8_t*保存在std :: atomic中包含的值。 这个模式可以在整个MS std :: atomic <>实现中观察到,这里没有理由让gcc团队,也没有任何其他的stl提供者做了不同的做法。