C ++ volatile关键字是否引入了内存栅栏?

时间:2014-10-10 19:51:36

标签: c++ multithreading c++11 volatile

我理解volatile通知编译器可能会更改该值,但为了完成此功能,编译器是否需要引入内存栅栏才能使其正常工作?

根据我的理解,易失性对象的操作顺序不能重新排序,必须保留。这似乎暗示一些内存栅栏是必要的,并且没有真正解决这个问题的方法。我说的是对的吗?


this related question

上有一个有趣的讨论

Jonathan Wakely writes

  

...对不同的volatile变量的访问不能被重新排序   编译器,只要它们出现在单独的完整表达式中......对   volatile对于线程安全无用,但不是因为他的原因   给人。这不是因为编译器可能会重新排序访问   易失性对象,但因为CPU可能会重新排序它们。原子   操作和内存障碍阻止编译器和CPU   重新排序

David Schwartz回复in the comments

  

......从C ++标准的角度来看,没有区别,   编译器之间做什么和编译器发出的   导致硬件执行某些操作的说明。如果CPU可能   重新排序对挥发物的访问,然后标准不要求   他们的命令得以保留。 ...

     

... C ++标准没有对什么做出任何区别   重新排序。你不能争辩说CPU可以重新排序   可观察的效果,这样就可以了 - C ++标准定义了它们   订购为可观察的。编译器符合C ++标准   一个平台,如果它生成的代码,使平台做的事情   标准要求。如果标准要求访问挥发物而不是   重新排序,然后重新排序的平台不符合要求。 ...

     

我的观点是,如果C ++标准禁止编译器   重新排序访问不同的挥发物,理论上的   此类访问的顺序是程序可观察行为的一部分,   那么它还要求编译器发出禁止CPU的代码   这样做。标准没有区分什么   编译器和编译器生成的代码使CPU做什么。

这确实产生两个问题:它们中的任何一个是“正确的”吗?实际的实现到底做了什么?

12 个答案:

答案 0 :(得分:52)

不要解释volatile的作用,请允许我解释您应该何时使用volatile

  • 在信号处理程序内部时。因为写入volatile变量几乎是标准允许您在信号处理程序中执行的唯一操作。从C ++ 11开始,您可以使用std::atomic来实现此目的,但前提是原子是无锁的。
  • 处理setjmp according to Intel
  • 直接处理硬件时,您希望确保编译器不会优化您的读取或写入。

例如:

volatile int *foo = some_memory_mapped_device;
while (*foo)
    ; // wait until *foo turns false

如果没有volatile说明符,编译器可以完全优化循环。 volatile说明符告诉编译器它可能不会假设2个后续读取返回相同的值。

请注意volatile与线程无关。如果有一个不同的线程写入*foo,则上述示例不起作用,因为不涉及获取操作。

在所有其他情况下,volatile的使用应该被认为是不可移植的,除了处理前C ++ 11编译器和编译器扩展(例如msvc' s { {1}}开关,默认情况下在X86 / I64下启用。

答案 1 :(得分:21)

  

C ++ volatile关键字是否引入了内存栅栏?

符合规范的C ++编译器不需要引入内存栅栏。您的特定编译器可能;将您的问题转发给编译器的作者。

" volatile"的功能在C ++中与线程无关。请记住," volatile"的目的是禁用编译器优化,以便从外部条件改变的寄存器读取不被优化。是否由不同CPU上的不同线程写入的内存地址是由于外部条件而发生变化的寄存器?不。再次,如果一些编译器作者选择来处理由不同CPU上的不同线程写入的内存地址,就好像它们是由于外生条件而改变的寄存器,那就是他们的业务;他们不需要这样做。它们也不是必需的 - 即使它确实引入了内存栅栏 - 例如,确保每个线程都能看到易失性读写的一致顺序。

事实上,volatile在C / C ++中的线程化几乎没用。最佳做法是避免它。

此外:内存屏障是特定处理器体系结构的实现细节。在C#中,volatile明确 设计用于多线程,规范并未说明将引入半个围栏,因为程序可能在第一个没有围栏的架构上运行地点。更确切地说,规范对于编译器,运行时和CPU将避免哪些优化以确定某些(极弱)约束如何排序某些副作用的某些(极弱)保证。在实践中,这些优化通过使用半栅栏来消除,但这是一个实施细节,可能会在未来发生变化。

您关心任何语言中与多线程有关的volatile的语义这一事实表明您正在考虑跨线程共享内存。考虑一下就不这样做。它使您的程序更难理解,更有可能包含微妙的,不可能重现的错误。

答案 2 :(得分:12)

首先,C ++标准不保证正确排序非原子读/写所需的内存障碍。建议将易变变量用于MMIO,信号处理等。在大多数实现中, volatile 对多线程无用,一般不推荐使用。< / p>

关于volatile访问的实现,这是编译器的选择。

article ,描述 gcc 行为表明您无法使用易失性对象作为内存屏障来对易失性内存进行一系列写入。< / p>

关于 icc 行为,我发现此 source 还告诉volatile不保证订购内存访问。

Microsoft VS2013 编译器具有不同的行为。这个 documentation 解释了volatile如何强制执行Release / Acquire语义,并允许在多线程应用程序的锁定/发布中使用volatile对象。

需要考虑的另一个方面是相同的编译器可能具有不同的行为。根据目标硬件架构来实现易失性。关于MSVS 2013编译器的 post 清楚地说明了针对ARM平台使用volatile进行编译的细节。

所以我的答案是:

  

C ++ volatile关键字是否引入了内存栅栏?

将是:不保证,可能不是,但有些编译器可能会这样做。你不应该依赖它的事实。

答案 3 :(得分:12)

David忽略的是c ++标准指定了几个线程在特定情况下交互的行为,而其他所有线程都会导致未定义的行为。如果不使用原子变量,则必须定义涉及至少一次写入的竞争条件。

因此,编译器完全有权放弃任何同步指令,因为您的cpu只会注意到由于缺少同步而出现未定义行为的程序的差异。

答案 4 :(得分:7)

据我所知,编译器只在Itanium体系结构上插入一个内存栅栏。

volatile关键字最适合用于异步更改,例如信号处理程序和内存映射寄存器;它通常是用于多线程编程的错误工具。

答案 5 :(得分:6)

这取决于“编译器”是哪个编译器。自2005年以来,Visual C ++就是这样做的。但是标准并没有要求它,因此其他一些编译器也没有。

答案 6 :(得分:5)

它没有必要。易失性不是同步原语。它只是禁用优化,即您在一个线程中获得可预测的读写序列,其顺序与抽象机器规定的顺序相同。但是,在不同的线程中,读取和写入首先没有顺序,所以说保留或不保留它们的顺序是没有意义的。可以通过同步原语建立theads之间的顺序,没有它们就可以获得UB。

关于记忆障碍的一些解释。典型的CPU具有多个级别的内存访问权限。有一个内存管道,几个级别的缓存,然后是RAM等。

Membar指令冲洗管道。它们不会改变执行读写的顺序,它只会强制在给定时刻执行优秀的执行。它对多线程程序很有用,但不是很多。

缓存通常在CPU之间自动连贯。如果想确保缓存与RAM同步,则需要缓存刷新。它与membar非常不同。

答案 7 :(得分:5)

这主要来自内存,基于预C ++ 11,没有线程。但 参与讨论委员会中的线程,我可以这么说 委员会从未有过volatile可以用来做的意图 线程之间的同步。微软提出了它,但提案 没带。

volatile的关键规范是对volatile的访问表示 “可观察行为”,就像IO一样。以同样的方式编译器不能 重新排序或删除特定的IO,它无法重新排序或删除对a的访问 volatile对象(或更准确地说,通过左值表达式访问 不稳定的合格类型)。事实上,挥发性的最初意图是 支持内存映射IO。然而,与此相关的“问题”在于它 实现定义了什么构成了“易变访问”。还有很多 编译器实现它,好像定义是“一个读取或 写入内存已被执行“。这是合法的,尽管没用 定义, if 实现指定它。 (我还没找到实际的 任何编译器的规范。)

可以说(这是我接受的一个论点),这违反了意图 标准,因为除非硬件将地址识别为内存映射 IO,并禁止任何重新排序等,你甚至不能使用volatile作为内存 映射IO,至少在Sparc或Intel架构上。从来没有,没有 我看过的编译器(Sun CC,g ++和MSC)确实输出了任何fence或membar 说明。 (大约在微软提出扩展规则的时候 volatile,我认为他们的一些编译器实现了他们的提议,并且做到了 发出易失性访问的fence指令。我最近没有证实 编译器会这样做,但如果它依赖于某些编译器,我不会感到惊讶 选项。我检查的版本 - 我认为是VS6.0 - 没有发出 然而,围栏。)

答案 8 :(得分:4)

编译器需要在volatile次访问时引入一个内存栅栏,当且仅当在标准工作中指定volatile时才需要使用setjmp信号该特定平台上的处理程序等等。

请注意,某些编译器确实超出了C ++标准所要求的范围,以使volatile在这些平台上更强大或更有用。便携式代码不应依赖volatile来执行C ++标准中指定的任何操作。

答案 9 :(得分:2)

我总是在中断服务程序中使用volatile,例如ISR(通常是汇编代码)修改了一些内存位置,在中断上下文之外运行的更高级代码通过指向volatile的指针访问内存位置。

我为RAM和内存映射IO执行此操作。

基于此处的讨论,似乎这仍然是volatile的有效用法,但与多线程或CPU没有任何关系。如果微控制器的编译器知道&#34;不能进行任何其他访问(例如,每个访问都在片上,没有缓存,而且只有一个核心)我会认为内存栅栏根本不是隐含的,编译器只是需要防止某些优化。

当我们将更多东西堆积到&#34;系统中时#34;执行目标代码几乎所有的赌注都是关闭的,至少我是如何看待这个讨论的。编译器如何涵盖所有基础?

答案 10 :(得分:0)

我认为关于易失性和指令重新排序的混淆源于CPU重新排序的2个概念:

  1. 无序执行。
  2. 其他CPU看到的内存读/写顺序(在某种意义上重新排序,每个CPU可能会看到不同的顺序)。
  3. Volatile会影响编译器生成代码的方式(假设是单线程执行)(包括中断)。它并不意味着有关内存屏障指令的任何内容,但它却阻止编译器执行与内存访问相关的某些优化。
    一个典型的例子是从内存中重新获取一个值,而不是使用一个缓存在寄存器中的值。

    无序执行

    CPU可以无序地执行指令/推测性地提供最终结果可能在原始代码中发生的情况。 CPU可以执行编译器中不允许的转换,因为编译器只能执行在所有情况下都正确的转换。 相反,CPU可以检查这些优化的有效性,如果结果不正确,则可以退出。

    其他CPU看到的内存读/写顺序

    指令序列的最终结果,即有效顺序,必须与编译器生成的代码的语义一致。但是,CPU选择的实际执行顺序可能不同。 其他CPU(每个CPU可以有不同的视图)中看到的有效顺序可能受到内存障碍的限制 我不确定有多少有效和实际的订单会有所不同,因为我不知道内存障碍可以阻止CPU执行无序执行的程度。

    来源:

答案 11 :(得分:0)

关键字volatile实质上意味着读取和写入对象应该完全按照程序编写执行,而不是以任何方式进行优化。二进制代码应该遵循C或C ++代码:读取它的负载,存储写入的存储。

这也意味着不应该期望读取产生可预测的值:编译器在写入相同的volatile对象后不应立即假设读取任何内容:

volatile int i;
i = 1;
int j = i; 
if (j == 1) // not assumed to be true

volatile可能是中最重要的工具; C是高级汇编语言&#34;工具箱

声明对象volatile是否足以确保处理异步更改的代码行为取决于平台:不同的CPU为正常的内存读写提供不同级别的保证同步。除非您是该领域的专家,否则您可能不应该尝试编写这种低级多线程代码。

Atomic原语为多线程提供了一个很好的更高级别的对象视图,可以很容易地推理代码。几乎所有程序员都应该使用原子基元或原语来提供互斥,如互斥,读写锁,信号量或其他阻塞原语。