gpt4 book ai didi

c++ - 为什么编译器不合并冗余的 std::atomic 写入?

转载 作者:行者123 更新时间:2023-12-02 02:34:25 25 4
gpt4 key购买 nike

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

#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”规则不适用)?

如果变量是可变的,那么显然没有优化是适用的。在我的情况下是什么阻止了它?

这是 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++ 未定义行为类型的数据竞争。 (它只是带有非原子变量的 UB)。期望有时会看到它的程序甚至不一定有缺陷。 (见下文:进度条。)

在 C++ 抽象机器上可能的任何排序都可以被选择(在编译时)作为总是发生的排序 .这是实际中的 as-if 规则。在这种情况下,就好像所有三个存储都以全局顺序背靠背发生,在 y=1 之间没有发生来自其他线程的加载或存储。和 y=3 .

它不依赖于目标架构或硬件;就像 compile-time reordering即使针对强有序 x86,也允许放松原子操作。编译器不必保留您在考虑要编译的硬件时可能期望的任何内容,因此您需要障碍。屏障可以编译成零汇编指令。

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

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

最明显的问题是进度条 .将存储从循环中取出(不包含其他原子操作)并将它们全部折叠为一个将导致进度条保持在 0,然后在最后变为 100%。

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

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

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

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

正在讨论延长std::atomic API 使程序员能够控制此类优化,此时编译器将能够在有用时进行优化,即使在并非故意低效的精心编写的代码中也会发生这种情况。以下工作组讨论/提案链接中提到了一些有用的优化案例示例:
  • http://wg21.link/n4455 :N4455 没有理智的编译器会优化原子
  • 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 answervolatileatomic 已经共享了一些要求,但它们是不同的)。另见 std::memory_order 's relationship with volatile 关于 cppreference。

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

    使用 volatile atomic<T>主要修复了进度条问题,但如果/当 C++ 决定使用不同的语法来控制优化以便编译器可以在实践中开始这样做时,它有点丑陋并且可能在几年后看起来很愚蠢。

    我认为我们可以确信编译器不会开始进行这种优化,直到有一种方法可以控制它。希望它是某种选择加入(如 memory_order_release_coalesce ),在编译为 C++ 时不会改变现有代码 C++11/14 代码的行为。但它可能类似于 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只做一次,因为它仍然在做 1 个具有相同值的商店。 (这将在 else 分支中的长循环之后)。特别是如果店铺只有 relaxedrelease而不是 seq_cst .
    volatile确实停止了问题中讨论的合并,但这指出 atomic<> 上的其他优化对于实际性能也可能存在问题。

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

    不过,让程序员感到困惑的原因当然是合理的。首先,无锁代码很难正确编写。

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

    关于c++ - 为什么编译器不合并冗余的 std::atomic 写入?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45960387/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com