为什么编译器不会合并冗余的std::atomic写操作?

64

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

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

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

这是在 编译器探索器 中的代码。


21
如果 f 只是众多写入 y 的线程中的一条,而还有其他线程在读取 y,那么如果编译器将写操作合并为单个写操作,则程序的行为可能会出现意外变化。 - Some programmer dude
24
在以前的版本中,这种行为并不是被保证的,所以它并不会使得优化无效。 - nwp
10
一个非常实际的论点是:对于编译器来说,在一般情况下很难推断出存储器中的冗余,而对于编写代码的人来说避免这种冗余写入应该是很容易的,那么为什么编译器编写者要费心添加这样的优化呢? - 463035818_is_not_a_number
17
无法在第二次和第三次存储之间编写一个将y设置为42的C++程序。你可以编写一个仅执行存储操作的程序,也许会有好运气,但无法保证它。不可能确定这种情况从未发生是因为多余的写入被删除了,还是只是由于时间不幸而导致的,因此优化是有效的。即使确实发生了这种情况,你也无法知道是在第一次、第二次还是第三次之前发生的。 - nwp
22
平淡的答案是,可能从来没有看到过足够类似的代码,以至于优化器编写者决定费心为其编写优化。 - TripeHound
显示剩余13条评论
9个回答

53
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 = 1y = 3之间没有来自其他线程的加载或存储,好像所有三个存储都按照全局顺序依次发生。
它不依赖于目标架构或硬件;就像松散原子操作的编译时重排序即使针对强序x86也是允许的。编译器不必保留您从编译的硬件中预期的任何内容,因此需要屏障。这些屏障可能会编译成零个汇编指令。

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

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

最明显的问题是进度条。将循环中的存储下沉(不包含其他原子操作)并将它们全部折叠成一个会导致进度条一直停留在0,然后在最后一刻跳到100%。

在没有C++11 std::atomic的情况下,无法阻止编译器在不想要的情况下执行此操作,因此目前编译器选择从不将多个原子操作合并为一个。(将它们全部合并为一个操作不会改变它们相对于彼此的顺序。)

编译器编写者正确地注意到程序员期望每次源代码执行y.store()时都会将原子存储实际发生到内存中。(请参见本问题的大多数其他答案,这些答案声称必须分别执行存储,因为可能有读取器等待看到中间值。)即它违反了最小惊讶原则

但是,在某些情况下,这将非常有帮助,例如避免在循环中无用的shared_ptr引用计数增加/减少。

显然,任何重新排序或合并都不能违反任何其他排序规则。例如,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在SO答案中指出的那样,volatileatomic已经共享了一些要求,但它们是不同的)。另请参见cppreference上std::memory_ordervolatile的关系
volatile对象的访问不允许被优化掉(因为它们可能是内存映射IO寄存器,例如)。
使用volatile atomic<T>基本上解决了进度条问题,但这有点丑陋,并且如果/当C++决定使用不同的语法来控制优化时,看起来可能很傻。
我认为我们可以有信心,编译器在没有一种控制方式之前不会开始进行此优化。希望它将是某种可选项(例如memory_order_release_coalesce),不会改变现有代码C++11/14编译为C++whatever时的行为。但它可能像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次存储。特别是如果存储只是relaxedrelease而不是seq_cst。(这将在else分支中的长循环之后执行)。 volatile确实阻止了问题中讨论的合并,但这表明atomic<>上的其他优化对于实际性能也可能存在问题。
其他不进行优化的原因包括:还没有人编写复杂的代码,使得编译器可以安全地执行这些优化(而不会出错)。但这并不足够,因为N4455指出LLVM已经实现或很容易实现它提到的几种优化。

对于程序员来说,令人困惑的原因是可信的。无锁代码本来就很难写正确。

在使用原子操作时不要随意浪费:它们不便宜,且目前不会进行优化。然而,使用std::shared_ptr<T>时,往往很难避免冗余的原子操作,因为它没有非原子版本(尽管这里的一个答案给出了一种为gcc定义shared_ptr_unsynchronized<T>的简单方法)。


2
@PeteC:是的,我认为重要的是要意识到优化是被允许的,不这样做是一个质量问题,而不是符合标准的问题,并且在未来的标准中可能会有所改变。 - Peter Cordes
3
在 Duff's Device 中,输出寄存器肯定会被声明为 volatile(这是 volatile 的经典案例),因此输出将如预期一样。 - PeteC
1
@PeteC:鉴于C和C++等语言被用于各种不同的目的,一些特定领域的程序和应用需要的语义在其他地方可能无法支持;语言本身将它们是否应该得到支持作为QoI问题而搁置,但如果某个特定领域的程序员发现某种行为令人惊讶,那么这是一个相当好的迹象,表明该领域中的高质量实现不应该以这种方式行事,除非有明确的要求。语言规则本身并不完整,没有POLA的话,它不能满足所有目的的使用。 - supercat
1
“允许编译器安全地进行这些优化(从未出错)” 检测有界成本计算是微不足道的(任何没有循环或跳转且没有轮廓函数调用的代码都是微不足道的);只有在仅有微不足道成本代码之间发生冗余原子操作时,合并这些操作似乎是微不足道的。我相信这将处理一些“shared_ptr”风格的松散增量后跟释放减量。 - curiousguy
1
@SouravKannanthaB:正确。分离的存储意味着它们可能在实际实现中看到中间值,但不能保证每次都发生,甚至可能根本不可观察。请注意,我回答中的部分句子描述了其他答案提出的声明。我的回答并不表示这是C++标准的要求。在你引用的部分之前的词是“声称”。 - Peter Cordes
显示剩余18条评论

46

您在谈论死代码消除。

虽然可以消除原子性的死存储,但是很难证明原子存储是否符合这样的条件。

传统的编译器优化(如死代码消除)可以应用于原子操作,即使是顺序一致的原子操作。
优化器必须小心避免跨越同步点进行此操作,因为另一个执行线程可以观察或修改内存,这意味着传统的优化器在考虑对原子操作进行优化时需要考虑更多的中介指令。
在死代码消除的情况下,仅证明原子存储后支配和别名另一个存储是不足的。

引用自N4455 No Sane Compiler Would Optimize Atomics

在一般情况下,原子DSE的问题在于需要查找同步点,我理解这个术语指的是代码中存在一个先前发生关系,即指线程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)

但如果代码被优化,等待查看y = 2的线程B(例如使用CAS)将永远不会观察到它。

然而,在我看来,B循环并CAS y = 2是数据竞争,因为两个线程之间没有总序。
执行A的指令在B的循环之前执行是可以被观察到(即允许的),因此编译器可以优化为y.store(3, std::memory_order_seq_cst)

如果线程A和B在线程A的存储操作之间进行同步,则不允许进行优化(可能导致B观察到y = 2)。

证明不存在这样的同步是困难的,因为这涉及考虑更广泛的范围,并考虑架构的所有怪异之处。

据我所知,由于原子操作相对较新且难以推理出内存排序,可见性和同步,编译器在建立更强大的框架以检测和理解必要条件之前不会对原子操作执行所有可能的优化。

我认为你的例子是上面给出的计数线程的简化版本,因为它没有其他线程或任何同步点,据我所见,我认为编译器可能已优化这三个存储操作。


2
你提到了N4455,但是你似乎对N4455的解释与我完全不同。即使是N4455中的第一个例子也比你的例子更复杂(添加而不是直接存储),而且该例子被描述为“非争议性”(即可以进行优化)。鉴于N4455还指出LLVM实现了其中提到的一些优化,可以肯定最简单的优化已经被实现了。 - MSalters
@MSalters 我认为N4455只是一个草案,实际上只有一种优化被列为已实现(我无法复现它)。我认为第一个示例与我的并没有真正的区别:两者都应该是可优化的,但事实并非如此。然而,虽然我对其内部工作原理有所了解,但我在C++标准方面并不很有根据。毫无疑问,你的理解比我更好!如果您发现这个答案中存在无法修复的缺陷,请告诉我,我绝不想传播错误信息! - Margaret Bloom
3
据我理解,编译器可以进行优化,但目前选择不这样做,因为那会违反程序员对进度条等方面的期望。需要新的语法来允许程序员进行选择。现有的标准允许在C++抽象机上可能发生的任何重新排序都可以被选为始终发生的顺序(在编译时),但这是不可取的。请参见http://wg21.link/p0062。 - Peter Cordes
@MSalters:发布了我的答案,因为这个问题有太多错误的答案。(除非我错了。)这个答案对于中心问题是正确的,但不优化的原因是编译器故意不这样做作为实现质量问题,即使在它们可以轻松发现优化的情况下也是如此。 - Peter Cordes
3
@MargaretBloom:1)在这里,顺序一致与松散并不重要(只有其他内存位置涉及时差异才相关)。2)在你的“y==2”检查示例中,存在我所谓的逻辑竞争,但不存在数据竞争。这是非常重要的区别。想想“未指定”的行为与“未定义”的行为:可能会看到“y==2”,也可能不会,但没有鼻妖。3)对于单个原子操作,总是有一个完全的操作顺序(即使使用“松散”的方式)。只是这个顺序可能不可预测。4)我同意原子操作可能会让人感到非常困惑。;-) - Arne Vogel
显示剩余7条评论

7
当您在一个线程中更改原子的值时,其他一些线程可能会检查它并根据原子的值执行操作。您提供的示例非常具体,编译器开发人员认为没有优化的价值。但是,如果一个线程为原子设置连续值,例如:012等,则另一个线程可能会将某些内容放入由原子指示的插槽中。

3
一个例子是进度条从一个 atomic 中获取当前状态,而工作线程则在没有其他同步的情况下更新了 atomic。该优化将允许编译器仅写一次100%,而不进行冗余写入,这样就可以让进度条显示进度。是否应该允许这种优化存在争议。 - nwp
也许这个例子并不是逐字发生的,而是经过了大量的优化,比如内联和常量传播。无论如何,你的意思是可以合并,但不值得费事吗? - Deduplicator
5
标准确实允许这样做。C++抽象机器上可能发生的任何重新排序都可以在编译时选择作为始终发生的事情。这违反了程序员对进度条等事物的期望(将原子存储下沉到不涉及任何其他原子变量的循环中,因为对非原子变量的并发访问是未定义行为)。目前,编译器选择不进行优化,尽管他们可以。希望会有新的语法来控制何时允许这样做。http://wg21.link/p0062和http://wg21.link/n4455。 - Peter Cordes

5
注意:我原本想评论一下,但是太啰嗦了。
有一个有趣的事实是,依据C++标准,这种行为并不算数据竞争。在第14页的注21很有意思:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3690.pdf (我强调的部分):
如果程序中包含两个不同线程中的冲突操作,其中至少一个不是原子操作,则程序的执行包含数据竞争。
同时在第11页的注5中也提到:
“松散”的原子操作虽然不能导致数据竞争,但它们不是同步操作。
因此,在C++标准中,对于原子变量的冲突操作从未被视为数据竞争。
尽管所有这些操作都是原子的(特别是"松散"的原子操作),但在这里并不存在数据竞争。
我同意,在任何(合理的)平台上这两者之间没有可靠、可预测的区别。
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++内存模型提供的定义中,它不是数据竞争。

我不能轻易地理解为什么提供了这样的定义,但它确实为开发人员提供了一些方法来在线程之间进行杂乱的通信,他们可能知道(在他们的平台上)会统计工作。

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

似乎C++标准为您提供了关于“数据竞争”的确定性,但允许某些竞争条件的乐趣,这在最终分析中是不同的事情。

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


4
没人说这是一场数据竞争。 - LWimsey
1
@LWimsey 的确不是数据竞争。 这就是重点。 C ++ 标准关注的是数据竞争。 因此,有关 OP 中无竞争观察者的推理是不相关的。 C ++ 对具有竞争的观察者没有问题,例如 try_lock_for 等东西都会引发竞争! 为什么编译器不对此进行优化的答案是因为它具有定义明确的语义(可能是有竞争的或其他),而标准希望这些情况发生。 - Persixty
1
在原子负载y上旋转并寻找y==2是一种竞争条件(这可能是OP在谈论无竞争观察者时所考虑的)。这只是普通的错误类型的竞争,而不是C++未定义行为类型的竞争。 - Peter Cordes

2
简而言之,因为标准(例如在[intro.multithread]的第20段及以下)不允许这样做。
必须满足happens-before保证,其中包括排除对写入的重新排序或合并(第19段甚至明确说明了重新排序)。
如果您的线程依次向内存写入三个值(假设是1、2和3),则另一个线程可能会读取该值。如果,例如,您的线程被中断(甚至在并发运行时),另一个线程也写入该位置,则观察线程必须按照它们发生的顺序完全看到操作(通过调度或巧合,或任何原因)。这是一项保证。
如果您只写入一半(甚至只写入一个)数据,那么如何做到这一点呢?不可能。
如果您的线程改为写出1 -1 -1,但另一个线程间歇性地写出2或3,那该怎么办?如果第三个线程观察该位置并等待一个永远不会出现的特定值,因为它被优化掉了呢?
如果不按要求执行存储(以及加载),就无法提供所给出的保证。所有这些操作都必须按相同的顺序执行。

9
这个优化并没有违反先于关系的保证。在另一个例子中可能会违反,但在这个例子中不会。很明显可以为该问题提供保证。没有重新排序,所以那部分与问题无关。 - nwp
4
@Damon,你能否更具体地说明文本中哪些部分禁止了这种优化? - LWimsey
2
@OrangeDog 所以它不太可能完全出现。尽管它可能是由常量传播、内联和任何数量的其他优化所导致的。 - Deduplicator
7
你的意思是在[intro.multithread]中有一些不允许合并写操作的规定。请引用相关条文。我找不到它。 - Deduplicator
3
@Deduplicator:不存在这样的编程语言,能够保证其他线程必须有时从另一个线程的一系列写入中看到中间值。编译器避免这种优化是一种实现质量问题,直到C++标准委员会添加一种方式来允许有选择地进行此类优化,因为它可能会成为一个问题。请参见我的回答,其中包含支持这种解释是允许的标准工作组提案的一些链接。 - Peter Cordes
显示剩余14条评论

2
一个实际使用该模式的例子是,如果线程在更新之间执行了与 y 无关或未修改的重要操作,则可能会:*线程2读取 y 的值以检查线程1的进度。
因此,也许线程1应该将配置文件作为步骤1加载,将其解析后的内容放入数据结构作为步骤2,并显示主窗口作为步骤3,而线程2正在等待步骤2完成,以便可以并行执行依赖于数据结构的另一项任务。(当然,这个例子需要获取/释放语义,不是松散排序。)
我相信符合规范的实现允许线程1在任何中间步骤中都不更新y的值——虽然我没有详细研究过语言标准,但如果它不支持在另一个线程轮询y可能永远看不到值2的硬件,那么我会感到震惊。
但是,这是一个假设的情况,可能最优化去除状态更新,可能编译器的开发人员会来到这里并说明为什么该编译器选择不这样做,但一个可能的原因是让您自己脚射击,或至少是让您在脚趾上做个标记。

2
是的,标准允许这样做,但实际编译器并不会进行这些优化,因为在像进度条更新这样的情况下没有语法来停止它们,所以这是一个实现质量问题。请参见我的答案 - Peter Cordes
@PeterCordes 很好的回答,特别是提供了实际WG讨论的链接。 - Davislor

0

编译器编写者不能仅仅执行优化。他们还必须确信在编译器编写者打算应用它的情况下,优化是有效的,在不适用的情况下不会被应用,并且不会破坏实际上已经损坏但在其他实现中“工作”的代码。这可能比优化本身更费力。

另一方面,我可以想象,在实践中(也就是在旨在完成工作而不是基准测试的程序中),这种优化将节省很少的执行时间。

因此,编译器编写者将考虑成本,然后考虑收益和风险,并可能决定反对它。


虽然答案可能不完全正确,但编译器仍然需要确保它不会破坏自己的库或其他产品...以微软编译器、他们的操作系统和Office为例。确保这样的产品不依赖于未进行此类优化的事实可能并不容易。 - Phil1970

-2

让我们远离三个商店紧挨着的病态案例。假设商店之间有一些非平凡的工作正在进行,而这样的工作根本不涉及y(因此数据路径分析可以确定这三个商店实际上是冗余的,至少在这个线程内),并且不会引入任何内存屏障(以免其他东西强制将商店对其他线程可见)。现在,其他线程很可能有机会在商店之间完成工作,也许这些其他线程操作y,而这个线程有某种原因需要将其重置为1(第二个商店)。如果删除前两个商店,那么行为将发生变化。


2
更改后的行为是否得到保证?优化经常会改变行为,它们倾向于使执行速度更快,这可能对时间敏感的代码产生巨大影响,但这被认为是有效的。 - nwp
但是一个写操作已经使其对其他线程可见。其他线程如何区分1次和3次写操作之间的差异呢? - nwp
3
如果你依赖于这一点,那么你的程序逻辑就出了问题。优化器的工作是在更少的努力下生成有效的输出。"线程2在存储之间没有获取时间片"是一个完全有效的结果。 - PeteC
@PeteC 所以"线程2(和3、4......)在存储之间获得很多时间片", 因为这是原子存储,其他线程必须能看到。优化器不能将可见的副作用优化掉,因为它必须假设在这两个存储之间可能发生一些事情,并且可能会执行自己的存储操作。 - Andre Kostur
2
标准是允许编译器优化掉另一个线程执行任务的窗口的。你提供的理由(例如进度条)是真正的编译器不选择这样做的原因。参见我的答案,其中提供了有关C ++标准讨论的一些链接,以便程序员可以控制优化以在有帮助时进行,而在有害时避免。 - Peter Cordes
显示剩余3条评论

-5

由于 std::atomic 对象中包含的变量预计将从多个线程访问,因此应该期望它们至少表现得像使用 volatile 关键字声明的变量。

在 CPU 架构引入缓存行等功能之前,这是标准和推荐做法。

[EDIT2] 有人可能会认为 std::atomic<> 是多核时代的 volatile 变量。根据 C/C++ 的定义,volatile 只足以同步来自单个线程的原子读取,其中 ISR 修改变量(在这种情况下,从主线程看,实际上是原子写入)。

我个人很高兴没有编译器会优化掉对原子变量的写入。如果写入被优化掉了,如何保证其他线程中的读取者可以潜在地看到每个这样的写入呢?不要忘记这也是 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对原子操作的实现是至关重要的,但是...

由于似乎有人怀疑volatile与原子操作是否有关,所以我进行了调查。以下是来自VS2017 STL的原子实现。正如我所推测的那样,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 中的所有专业都在关键函数上使用 volatile。

以下是其中一个关键函数的声明:

 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提供者以不同的方式进行操作。

10
“volatile” 与原子操作无关。 - login_not_failed
2
@login_not_failed 但是volatile与不优化内存访问有很大关系,这也是使用原子操作的一个效果。原子操作在此基础上添加了一些非常重要的保证(原子性和排序),但“不要优化掉这个!”的语义适用于两者。 - cmaster - reinstate monica
3
“volatile”和“atomic”虽然有相似之处,但是它们的作用不同。“volatile”假设你访问的是设备而不是内存,写入1、2、3可能是一个启动序列,必须按照这样的顺序到达,并且读取该位置可能会给出当前温度。“atomic”假定你正在使用常规内存,其中读取的是你上次写入的内容。 - nwp
2
volatile atomic<int> y 实际上会禁止此优化,因为它意味着存储可能具有副作用。(标准没有提到“IO设备”,但我记得它描述了volatile访问可能具有副作用。) - Peter Cordes
3
你认为VS2017的头文件不是特定于编译器的吗?/捂脸。 此外,你在答案中引用的函数正是我所说的方式在函数上使用volatileconst volatile:允许这些成员函数在volatile atomic<T>对象上使用。例如,bool _Is_lock_free() const volatile。如果他们不关心volatile atomic,他们根本不会使用volatile关键字。 - Peter Cordes
显示剩余27条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接