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

我知道volatile告诉编译器该值可能会改变,但是为了完成这个function,编译器是否需要引入一个内存栏来使其工作?

根据我的理解,易失性对象的操作顺序不能重新sorting,必须保留。 这似乎意味着一些记忆栅栏是必要的,并没有真正的解决方法。 我说得对吗?


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

乔纳森·Wakely写道 :

…对独立的volatilevariables的访问不能被编译器重新sorting,只要它们出现在单独的完整expression式中…正确的说,volatile对于线程安全来说是没有用处的,但不是出于他给出的原因。 这不是因为编译器可能会重新访问易失性对象,而是因为CPU可能会对它们进行重新sorting。 primefaces操作和内存屏障阻止了编译器和CPU重新sorting

David Schwartz 在评论中回应:

…从C ++标准的angular度来看,编译器在做某些事情和编译器发出的指令之间没有什么区别,这些指令会导致硬件做某些事情。 如果CPU可能将访问重新sorting为挥发性物质,则标准不要求保留其顺序。 …

… C ++标准对重新sorting的内容没有任何区别。 而且你不能争辩说,CPU可以重新sorting它们没有可观察到的效果,所以没关系 – C ++标准定义它们的顺序是可观察的。 如果编译器生成的代码使平台达到标准所要求的水平,则编译器在平台上符合C ++标准。 如果标准要求访问挥发性物质不被重新sorting,那么重新sorting的平台不符合标准。 …

我的观点是,如果C ++标准禁止编译器重新sorting访问不同的挥发性物质,理论上这种访问的顺序是程序可观察行为的一部分,那么它也要求编译器发出禁止CPU执行的代码所以。 该标准并没有区分编译器的function和编译器的生成代码使CPU做什么。

这产生了两个问题:他们中的任何一个是“正确的”? 实际的实现真的在做什么?

不要解释什么是volatile ,请允许我解释什么时候应该使用volatile

  • 当在信号处理程序中。 因为写入volatilevariables几乎是标准允许你在信号处理程序中做的唯一的事情。 由于C ++ 11,你可以使用std::atomic来达到这个目的,但是只有当primefaces是无锁的。
  • 根据Intel处理setjmp
  • 直接与硬件打交道时,要确保编译器不会优化读取或写入操作。

例如:

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

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

请注意, volatile与线程无关。 上面的例子是不行的,如果有不同的线程写入*foo因为没有涉及获取操作。

在所有其他情况下, volatile使用应该被认为是不可移植的,除了处理pre-C ++ 11编译器和编译器扩展(如msvc的/volatile:ms开关,这是默认启用的X86 / I64)。

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

符合规范的C ++编译器不需要引入内存栏。 您的特定编译器可能; 把你的问题引导到你的编译器的作者。

C ++中的“volatile”函数与线程无关。 请记住,“易失性”的目的是禁用编译器优化,以便从由于外部条件而改变的寄存器中读取不被优化。 是由另一个CPU上的另一个线程写入的内存地址,由于外部条件而正在改变的寄存器? 再说一遍,如果一些编译器的作者select将不同的CPU上的不同线程写入的内存地址当作由于外部条件而改变的寄存器,那就是他们的业务; 他们不需要这样做。 例如,它们也不需要(即使它引入了内存围栏),以确保每个线程看到一致的易失性读写顺序。

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

此外:内存隔离是特定处理器体系结构的实现细节。 在C#中,为了multithreading显式devisevolatile,规范并没有说将引入半围墙,因为程序可能运行在一个没有围墙的架构上。 相反,规范对编译器,运行时和CPU放弃什么样的优化(某些副作用将被sorting)的某些(极其微弱的)限制作出了某些(极其微弱的)保证。 在实践中,这些优化通过使用半围栏来消除,但是这是未来实现细节的变化。

你关心任何语言中volatile的语义,因为它们涉及到multithreading,这表明你正在考虑跨线程共享内存。 考虑不要这样做。 这使得你的程序难以理解,而且更容易包含微妙的,不可能再现的错误。

David所忽略的是,c ++标准指定了几个线程只在特定情况下进行交互的行为,而其他所有情况都会导致未定义的行为。 如果不使用primefacesvariables,至less包含一个写入的竞态条件是未定义的。

因此,编译器是完全放弃任何同步指令的权利,因为你的cpu只会注意到由于缺less同步而显示未定义行为的程序的差异。

首先,C ++标准不能保证正确sorting非primefaces读/写所需的内存屏障。 build议使用非易失性variables与MMIO,信号处理等一起使用。在大多数实现中, volatile对于multithreading是没有用的,一般不build议这样做。

关于易失性访问的实现,这是编译器的select。

这篇描述gcc行为的文章显示,你不能使用一个易失性对象作为内存屏障来命令一系列写入易失性内存。

关于ICC的行为,我发现这个来源也说挥发性不保证sorting内存访问。

微软的VS2013编译器有不同的行为。 本文档解释了volatile如何强制实现Release / Acquire语义,并使易失性对象能够在multithreading应用程序的locking/释放中使用。

另一个需要考虑的方面是相同的编译器可能会有不同的行为。 根据目标硬件架构而变化 。 这篇关于MSVS 2013编译器的文章清楚地说明了针对ARM平台编译volatile的具体细节。

所以我的答案是:

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

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

就我所知,编译器只在Itanium架构上插入内存栏。

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

这取决于哪个编译器“编译器”是。 Visual C ++自2005年起就开始使用它。但是标准并不要求它,所以其他一些编译器不需要。

这主要来自内存,并且基于pre-C ++ 11,没有线程。 但是参与了关于线程的讨论,我可以说,委员会从来没有意图使用volatile来实现线程之间的同步。 微软提出这个build议,但提案没有提出。

volatile的关键规范是对volatile的访问表示“可观察的行为”,就像IO一样。 以同样的方式编译器不能重新sorting或删除特定的IO,它不能重新sorting或删除对volatile对象的访问(或者更准确地说,通过具有volatile限定types的左值expression式访问)。 volatile的原意实际上是支持内存映射IO。 然而,这个“问题”在于,它是由什么构成“易变的访问”的实现定义的。 许多编译器实现它,就好像定义是“执行读或写内存的指令”一样。 这是一个合法的,尽pipe无用的定义, 如果实现指定它。 (我还没有find任何编译器的实际规范。)

可以说(这是我接受的论点),这违反了标准的意图,因为除非硬件将地址识别为内存映射IO,并禁止任何重新sorting等,否则甚至不能使用内存映射IO的volatile,至less在Sparc或Intel架构上。 从来没有,我看过的所有电子邮件服务器(Sun CC,g ++和MSC)都没有输出任何围栏或膜片指令。 (关于微软提议扩展volatile的规则的时候,我认为他们的一些编译器实现了他们的提议,并且发出了针对volatile访问的fence指令,但是我没有validation最近编译器做了什么,但是如果它依赖于一些编译器选项,我检查的版本 – 我认为这是VS6.0 – 但是没有发出篱笆)。

它不需要。 易失性不是同步原语。 它只是禁用优化,也就是说,按照抽象机器规定的顺序,在线程中获得可预测的读写顺序。 但是在不同的线程中读写并没有先后次序,说到保存或不保存它们的顺序是没有意义的。 可以通过同步原语build立两者之间的顺序,没有它们就可以得到UB。

关于记忆障碍的一些解释。 一个典型的CPU有几个级别的内存访问。 有一个内存pipe道,几个级别的caching,然后RAM等

Membar指令刷新pipe道。 它们不会改变读取和写入的执行顺序,只会迫使某些特定的时间执行。 这对于multithreading程序是有用的,但不是很多。

高速caching通常在CPU之间自动一致。 如果要确保高速caching与RAM同步,则需要高速caching刷新。 这是一个非常不同的元素。

编译器需要引入围绕volatile访问的内存围栏,并且只有在特定平台上的标准工作( setjmp ,signal handlers等)中指定用于volatile的用途时才需要。

请注意,一些编译器确实超出了C ++标准所要求的范围,以便使这些平台上的volatile变得更加强大或有用。 可移植代码不应该依赖volatile来做超出C ++标准规定的内容。

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

我这样做RAM和内存映射IO。

根据这里的讨论,似乎这仍然是volatile的有效用法,但与multithreading或CPU没有任何关系。 如果一个微控制器的编译器“知道”不能有任何其他的访问(例如,每一个是片上的,没有caching,只有一个核心),我认为一个内存栅栏并不是完全隐含的,编译器只是需要防止某些优化。

当我们把更多的东西放到执行目标代码的“系统”中时,几乎所有的赌注都没有了,至less我是这样读的。 编译器如何涵盖所有的基础?

我认为围绕易失性和指令重新sorting的困惑源于CPU重sorting的两个概念:

  1. 乱序执行。
  2. 由其他CPU看到的内存读取/写入顺序(从某种意义上说,重新sorting每个CPU可能会看到不同的顺序)。

易失性影响编译器如何生成代码,假定单线程执行(包括中断)。 这并不意味着内存屏障指令的任何内容,但它却排除了编译器执行某些与内存访问相关的优化。
一个典型的例子是从内存中重新获取一个值,而不是使用caching在寄存器中的值。

乱序执行

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

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

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

资料来源:

  • 内存障碍
  • LLVM:primefaces
  • ACCESS_ONCE()和编译器错误