当前位置: 首页 > 知识库问答 >
问题:

为什么编译器不合并冗余的STD::Atomic写?

夏侯衡
2023-03-14
#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);
}

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

下面是编译器资源管理器中的代码。

共有1个答案

岑鸣
2023-03-14

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。依赖于此的程序将有数据竞争bug,但只有普通bug类型的竞争,而不是C++未定义行为类型的数据竞争。(它是UB,只有非原子变量)。一个期望有时看到它的程序甚至不一定是错误的。(参见下面的进度条。)

C++抽象机器上可能的任何排序都可以(在编译时)作为总是发生的排序。这就是行动中的好像规则。在本例中,就好像所有三个存储都是按照全局顺序背靠背发生的,在y=1y=3之间没有来自其他线程的加载或存储。

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

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

C++11std::atomain没有办法阻止它们在您不需要的情况下执行此操作,所以目前编译器只需选择从不将多个原子操作合并为一个操作。(将它们合并到一个操作中不会改变它们相对于彼此的顺序。)

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

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

目前正在讨论如何扩展std::AtomicAPI,使程序员能够控制这样的优化,这时编译器将能够在有用的时候进行优化,即使是在精心编写的并非故意低效的代码中也可以进行优化。以下工作组讨论/提案链接中提到了一些有益的优化案例:

    null

在当前标准中,volatile Atomic y 将是确保不允许对其存储进行优化的一种方法。(正如Herb Sutter在SO回答中指出的那样,volatileAtomic已经共享了一些需求,但它们是不同的)。另请参见std::memory_order与CPPreference上volatile的关系。

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

使用volatile atomain 主要解决了进度条问题,但是这有点难看,如果C++决定使用不同的语法来控制优化,那么几年后可能会看起来很傻,这样编译器就可以在实践中开始这样做了。

我认为我们可以相信编译器不会开始进行这种优化,直到有一种方法来控制它。希望它是某种选择(比如memory_order_release_coalesce),在编译为C++时不会改变现有代码C++11/14代码的行为。但它可能与WG21/P0062中的建议类似:tag don-optimized cases with[[brittle_atomaby]]

WG21/P0062警告说,即使volatile atomain也不能解决所有问题,并不鼓励将其用于此目的。它给出了这个例子:

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 y ,也允许编译器将y.store()if/else中取出,并且只执行一次,因为它仍然使用相同的值执行1个存储。(这将在else分支的长循环之后)。尤其是如果存储区只有replacerelease而不是seq_cst

volatile确实停止了问题中讨论的合并,但这指出了Atomic<>上的其他优化对于实际性能也有问题。

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

不过,让程序员困惑的理由当然是合理的。一开始,无锁代码就很难正确地编写。

 类似资料:
  • 在过去的几个月里,我一直在学习C语言并使用终端。我的代码使用g和c11编译并运行得很好,但在过去几天里它开始出现错误,此后我在编译时遇到了问题。我唯一可以编译和运行的程序依赖于旧的C标准。 我第一次遇到的错误包括 尝试使用ecg$g-o stoi_试验stoi_试验编译。cpp-std=c 11 大堆cpp:13:22:错误:命名空间“std”中没有名为“stoi”的成员;你是说“阿托伊”吗?in

  • 我知道是一个原子对象。但是原子化到什么程度呢?据我所知,操作可以是原子的。使一个对象原子化到底是什么意思?例如,如果有两个线程同时执行以下代码: 那么整个操作(例如)是原子操作吗?还是对变量atomic(so)进行了更改?

  • 上面的代码使用不同的编译器会有不同的结果。这是编译器的错误还是我漏掉了什么? 叮叮当当 1 1 (https://godbolt.org/z/s43T55rxq) msvc 1 1 (https://godbolt.org/z/YnKfKh41q) 全球循环 0 1 (https://godbolt.org/z/91xdfv93c)

  • 问题内容: 我正在尝试使用泛型实现以下结构。收到编译器错误,无法找出原因。 这个想法是译者使用T作为字典中键的类型。例如,可以是字符串或枚举。子类提供具体的字典。 但是它失败,因为:“类型’String’不符合协议’Hashable’” 但是String符合Hashable。它也不适用于Int,后者也符合Hashable。 如果删除类型约束,则仅用于测试(在此我还必须禁用字典,因为我不能在其中使用

  • 我有一个简单的测试设置,如 但当我尝试编译测试时,我会遇到53个错误,比如 实际上并没有传达任何关于问题所在的有用信息。我只能假设在我的构建中没有正确配置某些内容。sbt文件或其他地方。 这段代码确实曾经工作过,在我清理东西的过程中,事情发生了变化,现在它被破坏了,没有好的诊断。 有人能提出要找的东西吗?