什么是 volatile 关键字做什么?在C++中它解决了什么问题?

就我而言,我从来没有故意需要它。

有帮助吗?

解决方案

volatile 如果您正在从内存中的某个位置读取数据(例如,完全独立的进程/设备/任何可能写入的内容),则需要。

我曾经在直接 C 语言的多处理器系统中使用双端口 RAM。我们使用硬件管理的 16 位值作为信号量来了解其他人何时完成。本质上我们是这样做的:

void waitForSemaphore()
{
   volatile uint16_t* semPtr = WELL_KNOWN_SEM_ADDR;/*well known address to my semaphore*/
   while ((*semPtr) != IS_OK_FOR_ME_TO_PROCEED);
}

没有 volatile, ,优化器认为循环毫无用处(这家伙从不设置该值!他疯了,摆脱那个代码!)并且我的代码将在没有获取信号量的情况下继续进行,从而导致以后出现问题。

其他提示

volatile 在开发嵌入式系统或设备驱动程序时需要读取或写入内存映射的硬件设备。特定设备寄存器的内容可能随时更改,因此您需要 volatile 关键字以确保此类访问不会被编译器优化掉。

一些处理器具有精度超过 64 位的浮点寄存器(例如没有 SSE 的 32 位 x86,请参阅 Peter 的评论)。这样,如果您对双精度数字运行多个运算,您实际上会得到比将每个中间结果截断为 64 位更高精度的答案。

这通常很好,但这意味着根据编译器分配寄存器和优化的方式,对完全相同的输入执行完全相同的操作会得到不同的结果。如果需要一致性,则可以使用 volatile 关键字强制每个操作返回内存。

它对于一些没有代数意义但减少浮点误差的算法也很有用,例如卡汉求和。从代数上来说,它是一个 nop,因此除非某些中间变量是易失性的,否则它通常会被错误地优化。

来自一个 “像承诺一样不稳定” 丹·萨克斯的文章:

(...) 易失性对象是指其值可能会自发改变的对象。也就是说,当您将一个对象声明为易失性对象时,您就告诉编译器该对象可能会更改状态,即使程序中似乎没有任何语句会更改它。”

以下是他的三篇文章的链接 volatile 关键词:

实现无锁数据结构时必须使用 volatile。否则,编译器可以自由地优化对变量的访问,这将改变语义。

换句话说,易失性告诉编译器对该变量的访问必须对应于物理内存读/写操作。

例如,InterlockedIncrement 在 Win32 API 中是这样声明的:

LONG __cdecl InterlockedIncrement(
  __inout  LONG volatile *Addend
);

我在 20 世纪 90 年代初开发的一个大型应用程序包含使用 setjmp 和 longjmp 的基于 C 的异常处理。对于需要将值保留在充当“catch”子句的代码块中的变量,必须使用 volatile 关键字,以免这些变量存储在寄存器中并被 longjmp 擦除。

在标准C中,使用的地方之一 volatile 带有信号处理程序。事实上,在标准 C 中,您在信号处理程序中可以安全地做的就是修改 volatile sig_atomic_t 变量,或快速退出。事实上,据我所知,这是标准 C 中唯一使用 volatile 需要避免未定义的行为。

ISO/IEC 9899:2011 §7.14.1.1 signal 功能

¶5 如果信号不是由于调用 abort 或者 raise 函数,如果信号处理程序指的是具有静态或线程存储持续时间的任何对象,则该行为是不确定的 volatile sig_atomic_t, ,或信号处理程序调用标准库中的任何功能 abort 函数,则 _Exit 函数,则 quick_exit 函数,或者 signal 第一个参数的功能等于与导致处理程序调用的信号相对应的信号号。此外,如果这样的调用 signal 功能导致sig_err返回,值 errno 是不确定的。252)

252) 如果异步信号处理程序生成任何信号,则行为未定义。

这意味着在标准 C 中,您可以编写:

static volatile sig_atomic_t sig_num = 0;

static void sig_handler(int signum)
{
    signal(signum, sig_handler);
    sig_num = signum;
}

没有什么其他的了。

POSIX 对于信号处理程序中可以执行的操作要宽松得多,但仍然存在限制(限制之一是标准 I/O 库 — printf() 等 - 不能安全使用)。

在针对嵌入式开发时,我有一个循环来检查可以在中断处理程序中更改的变量。如果没有“易失性”,循环就会变成空操作 - 据编译器所知,变量永远不会改变,因此它会优化检查。

同样的事情也适用于在更传统的环境中可能在不同线程中更改的变量,但我们经常进行同步调用,因此编译器在优化方面并不是那么自由。

当编译器坚持优化我希望在单步执行代码时能够看到的变量时,我在调试版本中使用了它。

除了按预期使用它之外,易失性还用于(模板)元编程。它可用于防止意外重载,因为 volatile 属性(如 const)参与重载决策。

template <typename T> 
class Foo {
  std::enable_if_t<sizeof(T)==4, void> f(T& t) 
  { std::cout << 1 << t; }
  void f(T volatile& t) 
  { std::cout << 2 << const_cast<T&>(t); }

  void bar() { T t; f(t); }
};

这是合法的;两个重载都可能是可调用的并且执行几乎相同的操作。剧中的演员阵容 volatile 重载是合法的,因为我们知道 bar 不会传递非易失性 T 反正。这 volatile 不过,版本严格来说更糟糕,因此如果非易失性,则永远不要在重载决策中选择 f 可用。

请注意,代码实际上从未依赖于 volatile 内存访问。

  1. 您必须使用它来实现自旋锁以及一些(全部?)无锁数据结构
  2. 将其与原子操作/指令一起使用
  3. 帮助我克服了编译器的错误(优化过程中错误生成的代码)

volatile 关键字的目的是防止编译器对可能以编译器无法确定的方式更改的对象应用任何优化。

对象声明为 volatile 在优化中被省略,因为它们的值可以随时被当前代码范围之外的代码更改。系统总是读取a的当前值 volatile 即使先前的指令要求来自同一对象的值,也不会在请求时将其值保留在临时寄存器中。

考虑以下情况

1) 由中断服务程序在作用域外修改的全局变量。

2) 多线程应用程序中的全局变量。

如果我们不使用 volatile 限定符,可能会出现以下问题

1) 当优化打开时,代码可能无法按预期工作。

2) 当启用和使用中断时,代码可能无法按预期工作。

易挥发的:程序员最好的朋友

https://en.wikipedia.org/wiki/Volatile_(computer_programming)

除了 volatile 关键字用于告诉编译器不要优化对某些变量(可以由线程或中断例程修改)的访问之外,它还可以 用于消除一些编译器错误 -- 是的,可以 ---.

例如,我在嵌入式平台上工作,编译器对变量的值做出了一些错误的假设。如果代码没有优化,程序将正常运行。通过优化(确实需要优化,因为这是一个关键的例程),代码将无法正常工作。唯一的解决方案(尽管不是很正确)是将“错误”变量声明为易失性。

即使没有,你的程序似乎也能工作 volatile 关键词?或许这就是原因:

正如前面提到的 volatile 关键字有助于类似的情况

volatile int* p = ...;  // point to some memory
while( *p!=0 ) {}  // loop until the memory becomes zero

但一旦调用外部或非内联函数,似乎几乎没有效果。例如。:

while( *p!=0 ) { g(); }

然后有或没有 volatile 生成几乎相同的结果。

只要 g() 可以完全内联,编译器就可以看到正在发生的一切,从而可以进行优化。但是,当程序调用编译器无法看到正在发生的情况的地方时,编译器再做出任何假设都是不安全的。因此,编译器将生成始终直接从内存读取的代码。

但要小心有一天,当你的函数 g() 变成内联时(由于显式更改或由于编译器/链接器的聪明才智),如果你忘记了,你的代码可能会崩溃 volatile 关键词!

因此我建议添加 volatile 即使您的程序似乎没有关键字也可以工作。它使未来变化的意图更加清晰、更加有力。

在 C 的早期,编译器会将所有读取和写入左值的操作解释为内存操作,并按照与代码中出现的读取和写入相同的顺序执行。如果给予编译器一定的自由度来重新排序和合并操作,在许多情况下效率可以大大提高,但这样做存在一个问题。甚至操作也经常以某种顺序指定,仅仅是因为有必要在 一些 顺序,因此程序员选择了许多同样好的替代方案之一,但情况并非总是如此。有时某些操作按特定顺序发生很重要。

到底哪些测序细节是重要的,取决于目标平台和应用领域。该标准没有提供特别详细的控制,而是选择了一个简单的模型:如果一系列访问是使用不合格的左值完成的 volatile, ,编译器可以根据需要重新排序和合并它们。如果一个动作是通过 volatile-限定左值,质量实现应该提供针对其预期平台和应用程序领域的代码可能需要的任何附加排序保证,而不必要求使用非标准语法。

不幸的是,许多编译器没有确定程序员需要什么保证,而是选择提供标准规定的最低限度的保证。这使得 volatile 远没有它应有的用处。例如,在 gcc 或 clang 上,需要实现基本“移交互斥体”的程序员必须执行以下操作:已获取并释放互斥体的任务在其他任务完成之前不会再次执行此操作)有四件事:

  1. 将互斥体的获取和释放放在编译器无法内联且无法应用全程序优化的函数中。

  2. 将互斥锁保护的所有对象限定为 volatile--如果所有访问都发生在获取互斥体之后和释放它之前,则不需要这样做。

  3. 使用优化级别 0 强制编译器生成代码,就像所有不合格的对象一样 registervolatile.

  4. 使用特定于 gcc 的指令。

相比之下,当使用更适合系统编程的更高质量的编译器(例如icc)时,人们会有另一种选择:

  1. 确保 volatile- 在需要获取或释放的任何地方执行合格的写入。

获取基本的“移交互斥体”需要 volatile 阅读(看看它是否准备好),并且不应该需要 volatile 也写(对方不会尝试重新获取它,直到它被交还),但必须执行无意义的操作 volatile write 仍然比 gcc 或 clang 下可用的任何选项更好。

我应该提醒您的一个用途是,在信号处理函数中,如果您想访问/修改全局变量(例如,将其标记为 exit = true),则必须将该变量声明为“易失性”。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top