关于c ++:为什么存在volatile?

关于c ++:为什么存在volatile?

Why does volatile exist?

volatile关键字有什么作用? 在C ++中它解决了什么问题?

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


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

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

1
2
3
4
5
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关键字强制每个操作返回内存。

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


来自Dan Saks的"挥发性如诺"文章:

(...) a volatile object is one whose value might change spontaneously. That is, when you declare an object to be volatile, you're telling the compiler that the object might change state even though no statements in the program appear to change it."

以下是关于volatile关键字的三篇文章的链接:

  • 明智地使用volatile
  • 准确放置挥发物
  • 挥发作为承诺

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

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

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

1
2
3
LONG __cdecl InterlockedIncrement(
  __inout  LONG volatile *Addend
);

我在20世纪90年代早期使用的大型应用程序包含使用setjmp和longjmp的基于C的异常处理。 volatile的关键字对于需要在作为"catch"子句的代码块中保留的变量是必要的,以免这些变量存储在寄存器中并被longjmp消灭。


在标准C中,使用volatile的一个地方是信号处理程序。实际上,在标准C中,您可以安全地在信号处理程序中执行的操作是修改volatile sig_atomic_t变量,或快速退出。实际上,AFAIK,它是标准C中唯一需要使用volatile来避免未定义行为的地方。

ISO/IEC 9899:2011 §7.14.1.1 The signal function

?5 If the signal occurs other than as the result of calling the abort or raise function, the
behavior is undefined if the signal handler refers to any object with static or thread
storage duration that is not a lock-free atomic object other than by assigning a value to an
object declared as volatile sig_atomic_t, or the signal handler calls any function
in the standard library other than the abort function, the _Exit function, the
quick_exit function, or the signal function with the first argument equal to the
signal number corresponding to the signal that caused the invocation of the handler.
Furthermore, if such a call to the signal function results in a SIG_ERR return, the
value of errno is indeterminate.252)

252) If any signal is generated by an asynchronous signal handler, the behavior is undefined.

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

1
2
3
4
5
6
7
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",循环就会变成noop - 就编译器而言,变量永远不会改变,因此它会优化检查。

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


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


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

1
2
3
4
5
6
7
8
9
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内存访问。


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

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

    声明为volatile的对象在优化中被省略,因为它们的值可以随时由当前代码范围之外的代码更改。系统始终从内存位置读取volatile对象的当前值,而不是将其值保存在请求点的临时寄存器中,即使前一条指令要求来自同一对象的值也是如此。

    考虑以下情况

    1)由范围外的中断服务例程修改的全局变量。

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

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

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

    2)启用和使用中断时,代码可能无法正常工作。

    易失性:程序员最好的朋友

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


    在C的早期,编译器会将读取和写入左值作为内存操作的所有操作解释为与代码中出现的读写相同的序列。如果编制者获得一定程度的自由来重新订购和整合运营,那么在许多情况下效率可以大大提高,但是存在问题。甚至操作通常仅按某种顺序指定,因为有必要按某种顺序指定它们,因此程序员选择了许多同样好的替代方案中的一种,但情况并非总是如此。有时某些操作以特定顺序发生是很重要的。

    确切地说,排序的哪些细节很重要,具体取决于目标平台和应用领域。标准选择了一个简单的模型,而不是提供特别详细的控制:如果使用不合格的左值(volatile)完成访问序列,编译器可以根据需要重新排序和合并它们。如果使用volatile限定的左值进行操作,则质量实现应提供针对其预期平台和应用程序字段的代码可能需要的任何其他排序保证,而不必要求使用非标准语法。

    不幸的是,许多编制者选择提供标准规定的最低限度保证,而不是确定程序员需要什么样的保证。这使得volatile的用处远不如它应该有用。例如,在gcc或clang上,需要实现基本"切换互斥锁"的程序员[已经获取并释放互斥锁的任务在其他任务完成之前不再执行此操作]必须执行一个四件事:

  • 将互斥锁的获取和释放放在编译器无法内联的函数中,并且不能应用整个程序优化。

  • 将互斥锁保护的所有对象限定为volatile - 如果在获取互斥锁之后和释放互斥锁之前发生所有访问,则不应该这样做。

  • 使用优化级别0强制编译器生成代码,就好像所有非限定register的对象都是volatile一样。

  • 使用特定于gcc的指令。

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

  • 确保在每个需要获取或释放的位置执行volatile限定的写入。
  • 获取一个基本的"切换互斥锁"需要一个volatile读取(看看它是否准备就绪),并且不应该要求volatile写入(另一方不会尝试重新获取它直到它是但是,必须执行无意义的volatile写入仍然比gcc或clang下的任何可用选项更好。


    即使没有volatile关键字,您的程序似乎也能正常工作?也许这就是原因:

    如前所述,volatile关键字有助于处理类似的情况

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

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

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

    然后在有或没有volatile的情况下产生几乎相同的结果。

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

    但要注意当天,当函数g()变为内联时(由于显式更改或由于编译器/链接器的聪明性),如果忘记volatile关键字,代码可能会中断!

    因此,我建议添加volatile关键字,即使您的程序似乎没有。它使意图在未来的变化方面更清晰,更健壮。


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

    例如,我在嵌入式平台上工作的是编译器对变量值进行了一些错误的分析。如果代码没有优化,程序将运行正常。通过优化(这是真正需要的,因为它是一个关键的例程)代码将无法正常工作。唯一的解决方案(虽然不是很正确)是将'faulty'变量声明为volatile。


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


    推荐阅读