为什么在C中需要使用volatile?


Answers:


423

易失性告诉编译器不要优化与易失性变量有关的任何事情。

至少有三个使用它的常见原因,所有这些情况都涉及变量的值可以更改而无需可见代码采取行动的情况。当另一个正在运行的线程也使用该变量时;或存在可能更改变量值的信号处理程序时。

假设您有一小部分硬件映射到某处的RAM中,并且具有两个地址:命令端口和数据端口:

typedef struct
{
  int command;
  int data;
  int isbusy;
} MyHardwareGadget;

现在您要发送一些命令:

void SendCommand (MyHardwareGadget * gadget, int command, int data)
{
  // wait while the gadget is busy:
  while (gadget->isbusy)
  {
    // do nothing here.
  }
  // set data first:
  gadget->data    = data;
  // writing the command starts the action:
  gadget->command = command;
}

看起来很简单,但可能会失败,因为编译器可以自由更改写入数据和命令的顺序。这将导致我们的小工具发布具有先前数据值的命令。还要看看忙循环时的等待。那将被优化。编译器将尝试变得更聪明,只读取一次isbusy的值,然后进入无限循环。那不是你想要的。

解决此问题的方法是将指针小工具声明为volatile。这样,编译器被迫执行您编写的操作。它不能删除内存分配,也不能在寄存器中缓存变量,也不能更改分配顺序:

这是正确的版本:

   void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
    {
      // wait while the gadget is busy:
      while (gadget->isbusy)
      {
        // do nothing here.
      }
      // set data first:
      gadget->data    = data;
      // writing the command starts the action:
      gadget->command = command;
    }

46
就个人而言,我更希望整数大小是明确的,例如在与硬件对话时使用int8 / int16 / int32。不错的答案;))
tonylo

22
是的,您应该声明具有固定寄存器大小的内容,但是,嘿,这只是一个例子。
尼尔斯·派宾布林克

69
当您使用不受并发保护的数据时,线程代码中也需要挥发。是的,确实有很多时间要做,例如,您可以编写线程安全的循环消息队列,而无需显式的并发保护,但是它将需要使用volatile。
戈登·里格利

14
认真阅读C规范。易失性仅在内存映射的设备I / O或异步中断功能接触的内存上具有已定义的行为。它没有关于线程的任何内容,并且优化了对多个线程接触的内存的访问的编译器是一致的。
迅速

17
@tolomea:完全错误。可悲的是17个人不知道。volatile不是内存屏障。它仅与基于不可见副作用的假设而在优化过程中避免代码省略有关
v.oddou

187

volatile实际上,C语言中的C语言是为了不自动缓存变量的值而存在的。它会告诉编译器不要缓存该变量的值。因此,它将volatile在每次遇到主变量时生成代码以从主内存中获取给定变量的值。之所以使用此机制,是因为该值可以随时通过OS或任何中断进行修改。因此,使用volatile有助于我们每次重新获得价值。


变成现实了吗?“ volatile”最初不是从C ++借来的吗?好吧,我似乎还记得...
语法错误

这并不是全部可变的-如果指定为可变的,它也禁止重新排序
。。– FaceBro

4
@FaceBro:的目的volatile是使编译器可以优化代码,同时仍然允许程序员获得无需这种优化即可实现的语义。该标准的作者期望,质量实现将支持给定目标平台和应用程序领域有用的任何语义,并且不希望编译器作者寻求提供符合该标准且质量不是100%的最低质量的语义。愚蠢的(请注意,标准的作者在原理上明确承认……
supercat

1
...一个实现有可能在没有足够好的质量的情况下符合标准,以至于实际上不适合任何目的,但是他们认为没有必要防止这种情况的发生。
超级猫

1
@syntaxerror当C比C ++早十年以上时(无论是第一个版本还是第一个标准),如何从C ++借用它?
phuclv '18

178

volatile信号处理程序的另一个用途是。如果您有这样的代码:

int quit = 0;
while (!quit)
{
    /* very small loop which is completely visible to the compiler */
}

允许编译器注意循环主体不接触quit变量并将循环转换为while (true)循环。即使quit变量被设置为上信号处理程序SIGINTSIGTERM; 编译器没有办法知道。

但是,如果quit声明了变量volatile,则编译器每次都必须加载它,因为可以在其他地方对其进行修改。在这种情况下,这正是您想要的。


当您说“编译器每次都被迫加载它时,就像编译器决定优化某个变量而我们不将变量声明为易失性一样,在运行时某些变量被加载到不在内存中的CPU寄存器中” ?
阿米特·辛格·托马尔2015年

1
@AmitSinghTomar它的意思是:每次代码检查值时,都会重新加载它。否则,允许编译器假定未引用该变量的函数无法对其进行修改,因此,假设CesarB打算不设置上述循环quit,则编译器可以将其优化为一个常量循环,并假定quit在迭代之间没有办法改变。注意:这不一定是实际线程安全编程的良好替代品。
underscore_d

如果quit是一个全局变量,那么编译器将不会优化while循环,对吗?
Pierre G.

2
@PierreG。不,除非另有说明,否则编译器始终可以假定代码是单线程的。也就是说,在没有volatile或其他标记的情况下,即使变量是全局变量,也将假定该变量一旦进入循环,循环外便不会有任何修改。
CesarB

1
@PierreG。是的,尝试例如编译extern int global; void fn(void) { while (global != 0) { } }gcc -O3 -S和看所产生的汇编文件,我的机器上它movl global(%rip), %eax; testl %eax, %eax; je .L1; .L4: jmp .L4,即如果全局不为零,则为无限循环。然后尝试添加volatile并查看差异。
CesarB

60

volatile告诉编译器您的变量可能会通过其他方式(而不是访问它的代码)进行更改。例如,它可能是I / O映射的内存位置。如果在这种情况下未指定,则可以优化某些变量访问,例如,可以将其内容保存在寄存器中,并且不会再次读回存储位置。


30

请参阅Andrei Alexandrescu的这篇文章,“ volatile-多线程程序员的最好朋友

挥发性关键字进行设计,以防止可能使代码在某些异步事件的存在不正确的编译器优化。例如,如果将原始变量声明为 volatile,则不允许编译器将其缓存在寄存器中-这是一种常见的优化方法,如果在多个线程之间共享该变量,那将是灾难性的。因此,通常的规则是,如果必须在多个线程之间共享原始类型的变量,则将这些变量声明为volatile。但是实际上您可以使用此关键字做更多的事情:您可以使用它来捕获不是线程安全的代码,并且可以在编译时执行。本文说明了它是如何完成的。该解决方案涉及一个简单的智能指针,该指针也使序列化关键代码段变得容易。

本文适用于CC++

另请参阅Scott Meyers和Andrei Alexandrescu 的文章“ C ++和双重检查锁定的风险 ”:

因此,在处理某些内存位置(例如,内存映射端口或ISR引用的内存[中断服务例程])时,必须暂停某些优化。存在volatile来指定对此类位置的特殊处理,具体来说:(1)volatile变量的内容“不稳定”(可以通过编译器未知的方式更改),(2)所有对volatile数据的写入都是“可观察的”,因此它们必须认真执行,并且(3)对易失性数据进行的所有操作均应按其在源代码中出现的顺序执行。前两个规则确保正确的读写。最后一个允许实现混合输入和输出的I / O协议。非正式地,这就是C和C ++的易失性保证。


如果从未使用过该值,该标准是否指定读取是否被视为“可观察到的行为”?我的印象是应该如此,但是当我声称它在其他地方时,有人挑战了我的引用。在我看来,在任何可能读取易失性变量有任何影响的平台上,都应该要求编译器生成代码,该代码执行每个指示的读取都必须精确地执行一次;如果没有该要求,将很难编写生成可预测的读取序列的代码。
supercat

@supercat:根据第一篇文章,“如果在变量上使用volatile修饰符,编译器将不会在寄存器中缓存该变量-每次访问都将达到该变量的实际内存位置。” 另外,在c99标准的第6.7.3.6节中,它说:“具有volatile限定类型的对象可以以实现方式未知的方式进行修改,或者具有其他未知的副作用。” 它进一步暗示易失性变量可能不会缓存在寄存器中,并且所有读取和写入都必须相对于序列点按顺序执行,实际上它们是可观察的。
罗伯特·S·巴恩斯

后一篇文章确实明确指出,阅读是副作用。前者表明读取不能乱序进行,但似乎并不排除它们被完全消除的可能性。
supercat

“不允许编译器将其缓存在寄存器中”-大多数RISC体系结构都是在寄存器机上进行的,因此任何读-修改-写操作必须将对象缓存在寄存器中。volatile不保证原子性。
对于这个网站来说太老实了

1
@Olaf:将某些内容加载到寄存器中与缓存不同。缓存会影响加载或存储的数量或其时间。
超级猫

28

我的简单解释是:

在某些情况下,基于逻辑或代码,编译器将优化它认为不会改变的变量。的volatile关键字,可避免一个可变被优化。

例如:

bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
    // execute logic for the scenario where the USB isn't connected 
}

根据以上代码,编译器可能会认为usb_interface_flag定义为0,而在while循环中,它将永远为零。经过优化后,编译器将一直将其视为while(true)永久循环,从而导致无限循环。

为了避免这种情况,我们将标志声明为volatile,我们告诉编译器该值可能会被外部接口或程序的其他模块更改,即,请不要对其进行优化。这就是volatile的用例。


19

以下是挥发物的边际用途。假设您要计算函数的数值导数f

double der_f(double x)
{
    static const double h = 1e-3;
    return (f(x + h) - f(x)) / h;
}

问题在于,由于舍入误差,x+h-x通常不等于h。想一想:当您减去非常接近的数字时,您会损失很多有效数字,这可能会破坏导数的计算(请考虑1.00001-1)。可能的解决方法可能是

double der_f2(double x)
{
    static const double h = 1e-3;
    double hh = x + h - x;
    return (f(x + hh) - f(x)) / hh;
}

但取决于您的平台和编译器开关,该功能的第二行可能会被积极优化的编译器抹去。所以你写

    volatile double hh = x + h;
    hh -= x;

强制编译器读取包含hh的内存位置,从而丧失最终的优化机会。


使用hhh在导数公式中有什么区别?在hh计算时,最后一个公式像第一个公式一样使用它,没有区别。也许应该(f(x+h) - f(x))/hh吗?
谢尔盖·朱可夫

2
h和之间的区别hhhh通过运算被截断为2的某个负幂x + h - x。在这种情况下,x + hhx不同的准确hh。您也可以采用公式,由于x + hx + hh相等,公式将给出相同的结果(分母在这里很重要)。
Alexandre C.

3
是不是更可读的方式来写这个x1=x+h; d = (f(x1)-f(x))/(x1-x)呢?无需使用挥发物。
谢尔盖·朱可夫

编译器可以清除函数第二行的任何引用吗?
CoffeeTableEspresso 18/09/25

@CoffeeTableEspresso:不,对不起。我对浮点数了解得越多,我相信就越多,编译器只有在明确告知时才允许使用-ffast-math或等效进行优化。
Alexandre C.

11

有两种用途。这些在嵌入式开发中更经常使用。

  1. 编译器不会优化使用由volatile关键字定义的变量的函数

  2. 易失性用于访问RAM,ROM等中的确切内存位置。这通常用于控制内存映射的设备,访问CPU寄存器并定位特定的内存位置。

请参见带有装配清单的示例。 回复:嵌入式开发中C“易失性”关键字的使用


“编译器不会优化使用由volatile关键字定义的变量的函数”-这是绝对错误的。
对于这个网站来说太老实了


10

我将提到另一种情况,其中挥发物很重要。

假设您对文件进行内存映射以获得更快的I / O,并且该文件可以在后台更改(例如,该文件不在本地硬盘上,而是由另一台计算机通过网络提供)。

如果您通过指向非易失性对象的指针(在源代码级别)访问内存映射文件的数据,则编译器生成的代码可以多次获取同一数据,而无需您意识到。

如果该数据发生更改,则您的程序可能会使用两个或多个不同版本的数据,并进入不一致状态。如果程序处理不受信任的文件或来自不受信任位置的文件,则不仅可能导致程序在逻辑上不正确的行为,而且还可能导致程序中的可利用安全漏洞。

如果您确实要关心安全性,那么这是要考虑的重要方案。


7

易失性意味着存储可能随时更改并且可能会更改,但是某些内容超出了用户程序的控制范围。这意味着,如果您引用变量,则程序应始终检查物理地址(即映射的输入fifo),而不是以缓存的方式使用它。


没有哪个编译器使用volatile来表示“ RAM中的物理地址”或“绕过缓存”。
curiousguy18年


5

我认为,您不应期望太多volatile。为了说明这一点,请看Nils Pipenbrinck极受投票的答案中的示例。

我想说,他的榜样不适合volatilevolatile仅用于: 防止编译器进行有用且理想的优化。它与线程安全,原子访问甚至内存顺序无关。

在该示例中:

    void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
    {
      // wait while the gadget is busy:
      while (gadget->isbusy)
      {
        // do nothing here.
      }
      // set data first:
      gadget->data    = data;
      // writing the command starts the action:
      gadget->command = command;
    }

gadget->data = data之前gadget->command = command仅只有在编译器编译代码的保证。在运行时,关于处理器体系结构,处理器仍可能对数据和命令分配进行重新排序。硬件可能会获得错误的数据(假设小工具已映射到硬件I / O)。在数据和命令分配之间需要内存屏障。


2
我想说说volatile是用来防止编译器进行通常会有用且合乎需要的优化。如所写,听起来好像volatile毫无理由地降低了性能。至于是否足够,这将取决于程序员可能比编译器了解更多的系统其他方面。另一方面,如果处理器保证写入某个地址的指令将刷新CPU高速缓存,但是编译器没有提供刷新CPU所不知道的寄存器高速缓存变量的方法,则刷新高速缓存将是无用的。
超级猫

5

在丹尼斯·里奇(Dennis Ritchie)设计的语言中,对任何对象的每次访问(未使用其地址的自动对象除外)都将表现为好像计算了对象的地址,然后在该地址读取或写入了存储。这使得该语言非常强大,但是优化机会却非常有限。

虽然可能可以添加一个限定符来邀请编译器假定一个特定的对象不会以怪异的方式进行更改,但是这种假定将适用于C程序中的绝大多数对象。在这样的假设适合的所有对象上添加限定符是不切实际的。另一方面,某些程序需要使用某些假设无法满足的对象。为解决此问题,标准指出,编译器可能会假设未声明的对象volatile将不会以其编译器无法控制的方式或合理的编译器无法理解的方式来观察或更改其值。

由于各种平台可能具有不同的方式来观察或修改编译器无法控制的对象,因此适合这些平台的高质量编译器应在volatile语义的精确处理方面有所不同。不幸的是,由于该标准未能建议打算在平台上进行低级编程的高质量编译器应volatile以能够识别该平台上特定读/写操作的所有及所有相关影响的方式进行处理,因此许多编译器无法做到这一点因此,这种方式将使得以高效的方式处理后台I / O之类的事情变得更加困难,但不会被编译器的“优化”破坏。


5

简单来说,它告诉编译器不要对特定变量进行任何优化。映射到设备寄存器的变量由设备间接修改。在这种情况下,必须使用挥发物。


1
这个答案中有什么以前没有提到的新东西吗?
slfan

3

可以从已编译的代码外部更改volatile(例如,程序可能将volatile变量映射到内存映射的寄存器。)编译器不会对处理volatile变量的代码进行某些优化-例如,它将不会t将其加载到寄存器中而不将其写入内存。在处理硬件寄存器时,这一点很重要。


0

正如此处许多人正确建议的那样,volatile关键字的流行用法是跳过volatile变量的优化。

我想到的最好的好处是,在阅读完volatile之后值得一提的是-防止在a情况下变量回滚longjmp。非本地跳转。

这是什么意思?

它仅表示在进行堆栈展开后将保留最后一个值,以返回到先前的某个堆栈帧;通常在某些错误情况下。

由于它不在本问题的讨论范围之内,因此我在此不做详细介绍setjmp/longjmp,但是值得一读。以及如何使用波动率特征来保留最后一个值。


-2

它不允许编译器自动更改变量的值。volatile变量用于动态使用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.