【问题标题】:Why don't compilers merge redundant std::atomic writes?为什么编译器不合并冗余的 std::atomic 写入?
【发布时间】:2018-02-08 03:59:42
【问题描述】:

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

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

我尝试过的每个编译器都会发出上述写入 3 次。什么合法的、无种族歧视的观察者可以看到上述代码与单次写入的优化版本之间的差异(即不适用“as-if”规则)?

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

这是compiler explorer中的代码。

【问题讨论】:

  • 如果f 只是写给y 的许多线程中的一个,而还有其他线程从y 读取呢?如果编译器将写入合并为一次写入,则程序的行为可能会意外更改。
  • @Someprogrammerdude 这种行为之前没有得到保证,所以它不会使优化无效。
  • 一个非常实用的论点是:对于编译器来说,在一般情况下很难推断存储的冗余,而对于编写代码的人来说,避免这种冗余写入应该是微不足道的,那么编译器作者为什么要费心添加这样的优化呢?
  • @RichardCritten 没有办法编写一个 C++ 程序,将y 设置为42 在第二和第三存储之间。你可以编写一个只做存储的程序,也许你很幸运,但没有办法保证它。无法判断它是否从未发生过,因为删除了冗余写入,或者因为您只是遇到了不幸的时机,因此优化是有效的。即使它确实发生了,你也无法知道,因为它可能发生在第一个、第二个或第三个之前。
  • 平淡无奇的回答是,可能从来没有足够多的代码看起来像这样让任何优化器编写者决定为它编写优化而烦恼。

标签: c++ multithreading c++11 compiler-optimization stdatomic


【解决方案1】:

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

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


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

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

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

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

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

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

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


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

另请参阅 Richard Hodges 对Can num++ be atomic for 'int num'? 的回答中关于同一主题的讨论(请参阅 cmets)。另请参阅my answer 的最后一部分对同一问题,我在其中更详细地争论这种优化是允许的。 (这里简短一点,因为那些 C++ 工作组链接已经承认当前编写的标准确实允许这样做,并且当前编译器只是没有故意优化。)


在当前标准中,volatile atomic&lt;int&gt; y 将是确保不允许对其进行优化的一种方法。 (正如Herb Sutter points out in an SO answervolatileatomic 已经共享一些要求,但它们是不同的)。另请参阅 cppreference 上的 std::memory_order's relationship with volatile

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

使用volatile atomic&lt;T&gt; 主要解决了进度条问题,但如果/当 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&lt;int&gt; y,编译器也可以将y.store()if/else 中提取出来,并且只执行一次,因为它仍然只执行1 个具有相同值的存储。 (这将在 else 分支中的长循环之后)。特别是如果商店只有relaxedrelease 而不是seq_cst

volatile 确实停止了问题中讨论的合并,但这指出atomic&lt;&gt; 的其他优化也可能对实际性能产生问题。


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

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

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

【讨论】:

  • @PeteC:是的,我认为重要的是要认识到允许优化,不这样做是 QOI 问题,而不是标准合规问题,并且在未来的标准中可能会发生一些变化.
  • @EricTowers 不,在 Duff 的设备中,输出寄存器肯定会被声明为 volatile(这是 volatile 的教科书案例),并且输出将符合预期。
  • @PeteC:鉴于使用 C 和 C++ 等语言的用途范围很广,某些目标和应用程序领域的程序通常需要并非所有地方都支持的语义;该语言本身提出了何时应该将它们作为 QoI 问题来支持的问题,但是如果特定领域的程序员会发现一个令人惊讶的行为,这是一个很好的迹象,表明该领域的质量实现不应该以这种方式表现,除非明确要求.如果没有 POLA,语言规则本身还不够完整,无法使该语言适用于所有用途。
  • @curiousguy:同意,质量实现可能不会用昂贵的计算重新排序volatile,即使他们很想通过两个分支中的共同尾巴这样做。但是标准允许我们不想要的行为,因此至少标准委员会试图改进这是一个问题。你可以把它留在那儿,并说已经有可能制作一个严格符合 C++ 的实现,这对于低级系统编程几乎没有用处,但其中很多是通过违反大多数代码所做的假设,比如整数类型不'没有填充。不是优化。
  • "允许编译器安全地进行这些优化(永远不会出错)" 检测有界成本计算是微不足道的(任何没有循环或 goto 且没有大纲的代码有趣的电话是微不足道的);合并冗余原子操作之间只有微不足道的成本代码似乎微不足道。这将处理一些 shared_ptr 风格的放松 incr,然后是发布 decr 我相信。
【解决方案2】:

您指的是消除死店。

不禁止消除原子死存储,但更难证明原子存储符合条件。

传统的编译器优化,例如死存储消除,可以在原子操作上执行,甚至是顺序一致的操作。
优化器必须小心避免跨同步点这样做,因为另一个执行线程可以观察或修改内存,这意味着传统优化必须考虑比通常在考虑优化时更多的干预指令原子操作。
在消除死存储的情况下,仅证明一个原子存储后支配并别名另一个存储以消除另一个存储是不够的。

来自N4455 No Sane Compiler Would Optimize Atomics

在一般情况下,原子 DSE 的问题在于它涉及寻找同步点,在我的理解中,这个术语是指代码中指令之间存在 happen-before 关系的点在线程 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 是一种数据竞争,因为两个线程的指令之间没有总顺序。
A 的指令在 B 的循环之前执行的执行是可观察的(即允许的),因此编译器可以优化到 y.store(3, std::memory_order_seq_cst)

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

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

就我的理解而言,由于原子操作的年龄相对较小,并且在推理内存排序、可见性和同步方面存在困难,编译器不会对原子进行所有可能的优化,直到一个更强大的框架来检测和理解必要条件是建立的。

我相信您的示例是上面给出的计数线程的简化,因为它没有任何其他线程或任何同步点,据我所见,我想编译器可以优化三个存储。

【讨论】:

  • 您指的是 N4455,但似乎对 N4455 的解释与我完全不同。即使是 N4455 中的第一个示例也比您的示例更复杂(添加而不是直接存储),并且该示例被描述为“无争议”(可以进行优化)。鉴于 N4455 还声明 LLVM 实现了一些提到的优化,可以肯定地假设最简单的优化肯定已经实现。
  • @MSalters 我虽然老实说 N4455 是一个草稿,但只有一个优化被列为已实施 (I wasn't able to reproduce it)。我相信第一个例子与我的并没有什么不同:两者都应该是可优化的,但不是。然而,虽然我了解它是如何在幕后工作的,但我对 C++ 标准语言的基础并不好。你的理解肯定比我的好!我永远不想传播错误信息,如果您在此答案中发现无法修复的缺陷,请告诉我!
  • 嗯,可能需要稍微了解一下那里发生的事情。至于 N4455 是草稿:那不是重点;它从编译器开发人员的角度为我们提供了一个内部视图。这也意味着他们正在使用我们还没有的代码库;)
  • @MSalters:据我了解,编译器可以优化,但现在选择不优化,因为这会违反程序员对进度条之类的期望。需要新的语法来允许程序员选择。所编写的标准允许选择(在编译时)C++ 抽象机器上可能发生的任何可能的重新排序作为 总是 发生的排序,但这是不可取的。另见wg21.link/p0062
  • @MargaretBloom: 1) 顺序一致与放松在这里无关紧要(差异仅在 其他 内存位置起作用时才相关)。 2) 在您的y==2 检查示例中,我称之为逻辑竞争,但没有数据竞争。这是一个非常重要的区别。想想“未指定”与“未定义”的行为:可能会看到y==2,或者可能不会,但没有鼻恶魔。 3) 单个原子上的操作总是有一个总顺序(即使是relaxed)。顺序可能无法预测。 4)我同意原子可能非常混乱。 ;-)
【解决方案3】:

当您在一个线程中更改原子的值时,其他一些线程可能正在检查它并根据原子的值执行操作。您提供的示例非常具体,以至于编译器开发人员认为它不值得优化。但是,如果一个线程正在设置,例如一个原子的连续值:012 等,另一个线程可能在原子值指示的槽中放入一些东西。

【讨论】:

  • 这方面的一个例子是一个进度条,它从atomic 获取当前状态,而工作线程执行一些工作并更新atomic 而无需其他同步。优化将允许编译器只写一次 100% 而不会进行冗余写入,这会使进度条不显示进度。是否应该允许这样的优化是有争议的。
  • 也许这个例子不是逐字出现的,而是在内联和常量传播等优化之后才出现的。无论如何,你是说可以合并,但不值得费心?
  • @nwp:所写的标准确实允许这样做。在 C++ 抽象机器上可能发生的任何重新排序都可以在编译时选择,就像 总是 发生的那样。这违反了程序员对进度条之类的期望(将原子存储从不触及任何其他原子变量的循环中删除,因为对非原子变量的并发访问是 UB)。目前,编译器选择不优化,即使他们可以。希望会有新的语法来控制何时允许这样做。 wg21.link/p0062wg21.link/n4455.
【解决方案4】:

注意:我打算对此发表评论,但它有点太罗嗦了。

一个有趣的事实是,从 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 次值的“锤击”效果,其他线程必须能够看到该效果(即使有时他们可能看不到!)。 几乎所有其他线程在某些情况下可能会遇到锤击的现代平台就是这种情况。

【讨论】:

  • 没有人说这是一场数据竞赛
  • @LWimsey 确实,这不是数据竞赛。这才是重点。 C++ 标准关注的是数据竞争。因此,关于 OP 中无种族观察员的推理是无关紧要的。 C++ 对暴露种族的观察者没有问题,而且确实像 try_lock_for 邀请赛车这样的事情!关于编译器为什么不优化的答案是因为它定义了语义(种族或其他)并且标准希望这些发生(无论它们可能是什么)。
  • y 的原子负载上旋转以寻找y==2 是一种竞争条件(这可能是 OP 在谈论无竞争观察者时所想到的)。不过,这只是种类繁多的错误竞赛,而不是 C++ 未定义行为。
【解决方案5】:

简而言之,因为标准(例如 [intro.multithread] 中 20 左右和以下的段落)不允许这样做。

必须满足发生前发生的保证,其中排除了重新排序或合并写入(第 19 段甚至明确说明了重新排序)。

如果您的线程一个接一个地向内存写入三个值(例如 1、2 和 3),则另一个线程可能会读取该值。例如,如果您的线程被中断(或者即使它同时运行)并且另一个线程写入该位置,那么观察线程必须以与它们发生时完全相同的顺序查看操作(无论是通过调度或巧合,或任何原因)。那是保证。

如果您只执行一半的写入(甚至只执行一次写入),这怎么可能?不是。

如果您的线程改为写出 1 -1 -1 但另一个线程偶尔会写出 2 或 3,该怎么办?如果第三个线程观察到该位置并等待一个特定的值,该值由于已优化而从未出现过怎么办?

如果未按要求执行存储(和加载),则无法提供所提供的保证。所有这些,并且以相同的顺序。

【讨论】:

  • 优化不会违反发生前的保证。在另一个例子中,它们可能是,但不是在这个例子中。显然可以为 OP 的示例提供保证。没有重新排序,因此该部分与问题无关。
  • @Damon 您能否更具体地说明文本中的哪些部分不允许这种优化?
  • @OrangeDog 所以它不太可能逐字出现。尽管它可能来自于不断传播、内联和任何数量的其他优化。
  • 您是说有一些东西不允许合并 [intro.multithread] 中的写入。 请引用它。我找不到它。
  • @Deduplicator:没有这样的语言可以保证其他线程有时必须从另一个线程的写入序列中看到中间值。编译器避免这种优化的事实是一个实现质量问题,直到 C++ 标准委员会添加一种方法来选择性地允许它,因为它可能是一个问题。请参阅my answer 以获取一些标准工作组提案的链接,这些提案支持这种解释是允许的。
【解决方案6】:

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

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

我很确定符合标准的实现允许线程 1 在任何中间步骤不更新 y — 虽然我没有仔细研究语言标准,但如果它不支持另一个线程所在的硬件,我会感到震惊轮询y 可能永远不会看到值 2。

但是,这是一个假设的实例,优化掉状态更新可能很糟糕。也许编译器开发人员会来这里说明为什么那个编译器选择不这样做,但一个可能的原因是让你朝自己的脚开枪,或者至少让自己陷入困境。

【讨论】:

  • 是的,标准允许这样做,但真正的编译器不会进行这些优化,因为在进度条更新等情况下没有停止它们的语法,所以这是一个实施质量问题。见my answer
  • @PeterCordes 不错的答案,尤其是实际工作组讨论的链接。
【解决方案7】:

让我们远离三家商店紧挨着的病态案例。让我们假设商店之间正在做一些重要的工作,并且这些工作根本不涉及y(因此数据路径分析可以确定这三个商店实际上是多余的,至少在这个线程中),并且本身不会引入任何内存屏障(因此其他东西不会强制存储对其他线程可见)。现在很有可能其他线程有机会在商店之间完成工作,也许那些其他线程操纵y,并且该线程有一些理由需要将其重置为 1(第二个商店)。如果前两个商店被删除,那将改变行为。

【讨论】:

  • 是否保证改变的行为?优化一直在改变行为,它们往往会使执行速度更快,这会对时间敏感的代码产生巨大影响,但这被认为是有效的。
  • 原子部分改变了一切。这迫使商店对其他线程可见。 y 的三个商店必须对其他线程可见。如果y 不是原子的,那么可以肯定,优化器可以删除前两个分配,因为这个线程中没有任何东西可以看到它们已被删除,并且没有任何东西可以保证其他线程可以看到这些分配。但由于它是原子的,并且确实保证更改对其他线程可见,因此优化器无法删除该代码。 (除非以某种方式验证 everywhere else 也不使用它。)
  • 但是 1 次写入已经使其对其他线程可见。其他线程如何计算 1 次写入和 3 次写入之间的差异?
  • @AndreKostur '应该是'?如果你依赖它,你的程序逻辑就会被破坏。优化器的工作是以更少的努力产生有效的输出。 “线程 2 在商店之间没有时间片”是一个完全有效的结果。
  • 所写的标准确实允许编译器优化窗口以供另一个线程执行某些操作。您对此的推理(以及进度条之类的东西)是真正的编译器选择不进行此类优化的原因。请参阅my answer 以获取有关允许程序员控制的 C++ 标准讨论的一些链接,以便可以在有帮助的地方进行优化并在有害的地方避免。
【解决方案8】:

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

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

所以编译器编写者会先看成本,然后再看收益和风险,然后可能会决定反对。

【讨论】:

    【解决方案9】:

    由于包含在 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 提供者采取不同做法的理由。

    【讨论】:

    • volatile 与原子无关
    • @login_not_failed 但是volatile 与不优化内存访问有很大关系,这是使用原子的效果之一。 Atomics 在此之上添加了一些非常重要的保证(原子性和排序),但“不要优化它!”语义适用于两者。
    • 这是错误的。 volatile 做了 atomics 不做的事情,特别是 volatile 假设您不与内存交谈,而是与设备交谈,其中写入 1、2、3 可能是一个启动序列,必须完全像那样到达并读取该位置可能会为您提供当前温度。 atomic 假设您正在使用常规内存来阅读您上次写的内容。
    • volatile atomic&lt;int&gt; y 实际上会禁止这种优化,因为这意味着存储可能有副作用。 (标准没有提到“IO 设备”,但 IIRC 确实将 volatile 访问描述为可能有副作用的访问。)
    • 您认为 VS2017 的标头 不是 特定于编译器的吗? /掌心。此外,您在答案中引用的函数在函数上使用 volatileconst volatile,就像我所说的那样:允许在 volatile atomic&lt;T&gt; 对象上使用这些成员函数。例如bool _Is_lock_free() const volatile。如果他们不关心volatile atomic,他们根本不会使用volatile 关键字。
    猜你喜欢
    • 2012-08-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-05
    • 1970-01-01
    • 2020-08-30
    • 2011-03-19
    • 2015-06-04
    相关资源
    最近更新 更多