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

Nat*_*mal 80 c++ multithreading volatile c++11

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

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


这个相关问题上有一个有趣的讨论

乔纳森威克利写道:

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

大卫·施瓦茨回答的评论:

...从C++标准的角度来看,编译器执行某些操作与编译器发出导致硬件执行某些操作的指令之间没有区别.如果CPU可能重新排序对volatiles的访问,则标准不要求保留其订单....

... C++标准没有对重新排序有什么区别.你不能争辩说CPU可以重新排序它们没有可观察到的影响,所以没关系--C++标准将它们的顺序定义为可观察的.如果编译器生成的代码使平台能够满足标准要求,则编译器在平台上符合C++标准.如果标准要求对挥发物的访问不能重新排序,则重新排序它们的平台不符合要求....

我的观点是,如果C++标准禁止编译器重新排序对不同易失性的访问,理论上这种访问的顺序是程序可观察行为的一部分,那么它还要求编译器发出禁止CPU执行的代码所以.该标准没有区分编译器的作用以及编译器生成的代码使CPU执行的操作.

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

Ste*_*fan 52

而不是解释是什么volatile,让我解释你应该使用什么volatile.

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

例如:

volatile int *foo = some_memory_mapped_device;
while (*foo)
    ; // wait until *foo turns false
Run Code Online (Sandbox Code Playgroud)

如果没有说明volatile符,则允许编译器完全优化循环.该volatile说明符告诉它可能不承担后续2读取返回的值相同的编译器.

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

在所有其他情况下,volatile除了处理前C++ 11编译器和编译器扩展(例如msvc的/volatile:ms交换机,默认情况下在X86/I64下启用)之外,应该将其视为不可移植并且不再通过代码审查.

  • 它比"可能不会假设2个后续读取返回相同的值"更严格.即使您只读过一次和/或抛弃值,也必须进行读取. (4认同)

Eri*_*ert 21

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

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

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

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

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

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

  • "在C/C++中,volatile几乎没用." 一点也不!您有一个非常以用户模式 ​​- 桌面为中心的世界观......但是大多数C和C++代码都在嵌入式系统上运行,其中内存映射I/O非常需要volatile. (15认同)
  • 保持易失性访问的原因不仅仅是因为外源条件可以改变内存位置.访问本身可以触发进一步的操作.例如,读取提前FIFO或清除中断标志是很常见的. (11认同)
  • 这种编辑是一个明显的改进,但你的解释仍然过于集中在"记忆可能会外生改变".`volatile`语义比那更强,编译器必须生成每个请求的访问(1.9/8,1.9/12),而不是简单地保证最终检测到外生变化(1.10/27).在内存映射I/O的世界中,内存读取可以具有任意关联逻辑,如属性getter.您不会根据您为"volatile"声明的规则优化对属性getter的调用,标准也不允许这样做. (7认同)
  • @DavidSchwartz标准显然无法保证内存映射IO的工作原理.但内存映射IO是为什么`volatile`被引入C标准的原因.但是,由于标准不能指定"访问"中实际发生的事情,因此它表示"构成对具有volatile限定类型的对象的访问权限是实现定义的".今天有太多的实现没有提供对访问的有用定义,即使它符合该字母,IMHO也违反了标准的精神. (4认同)
  • @BenVoigt:用于有效处理线程问题的无用是我的意图. (3认同)
  • @Klaus:用于有效处理线程问题的无用是我的意图.你已经正确地重新说明了我的第二段的前两句,所以我们就此达成一致. (2认同)

VAn*_*rei 12

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

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

文章,描述了GCC的行为表明,您不能使用挥发性对象作为内存屏障命令写入非易失性存储器的序列.

关于icc行为,我发现这个来源还告诉volatile不保证订购内存访问.

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

需要考虑的另一个方面是相同的编译器可能具有不同的行为.根据目标硬件架构而变化.这篇关于MSVS 2013编译器的帖子明确说明了针对ARM平台使用volatile编译的细节.

所以我的答案是:

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

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

  • 它不会阻止优化,它只是阻止编译器改变超出某些约束的加载和存储. (2认同)

Voo*_*Voo 12

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

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

  • 很好地解释了,谢谢.只要程序没有未定义的行为,标准只定义了对易失性的易失性的访问顺序. (5认同)
  • 如果程序有数据竞争,那么标准对程序的可观察行为没有要求.编译器不应该为易失性访问添加障碍,以防止程序中出现的数据争用,这是程序员的工作,可以通过使用显式障碍或原子操作. (4认同)
  • 这是完全错误的,或者至少它忽略了必要的.`volatile`与线程无关; 它的最初目的是支持内存映射IO.至少在某些处理器上,支持内存映射IO需要使用围栏.(编译器不会这样做,但这是一个不同的问题.) (2认同)

Die*_*Epp 7

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

volatile关键字是真正最好用于异步变化,例如,信号处理器和存储器映射的寄存器; 它通常是用于多线程编程的错误工具.


Ben*_*igt 6

这取决于"编译器"是哪个编译器.自2005年以来,Visual C++就是这样做的.但是标准并没有要求它,所以其他一些编译器则不需要它.


n. *_* m. 5

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

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

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

缓存通常在CPU之间自动一致.如果想确保缓存与RAM同步,则需要缓存刷新.它与膜非常不同.


Jam*_*nze 5

这很大程度上来自内存,基于前C++ 11,没有线程.但是参与了关于委托中线程的讨论,我可以说委员会从来没有一个volatile可以用于线程之间同步的意图.微软提出了这个建议,但提案没有提出.

关键规范volatile是对volatile的访问表示"可观察行为",就像IO一样.以同样的方式,编译器无法重新排序或删除特定IO,它无法重新排序或删除对volatile对象的访问(或者更准确地说,通过具有volatile限定类型的左值表达式进行访问).事实上,volatile的原始意图是支持内存映射IO.然而,与此相关的"问题"在于实现定义了什么构成了"易变访问".许多编译器实现它,好像定义是"一个读取或写入内存的指令已被执行".如果实现指定了,那么这是一个合法的,尽管是无用的定义.(我还没有找到任何编译器的实际规范.)

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