为什么挥发性存在?


222

那是什么 volatile关键词呢?在C ++中,它可以解决什么问题?

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


以下是有关Singleton模式的波动性的有趣讨论:aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
Chessguy

3
有一种引人入胜的技术,可让您的编译器检测可能依赖于volatile关键字的竞争条件,您可以在http://www.ddj.com/cpp/184403766上进行了解。
诺甘

这是一个很好的资源,上面有一个示例,介绍了何时volatile可以有效使用,并以通俗易懂的方式组合在一起。链接:publications.gbdirect.co.uk/c_book/chapter8/...
优化的编码器

我将其用于无锁代码/双重检查锁定
paulm,2014年

对我来说,volatilefriend关键字有用。
acegs

Answers:


268

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,则优化器将认为该循环是无用的(该家伙从未设置过该值!他真是疯了,摆脱了该代码!),我的代码将在没有获得信号量的情况下继续进行,导致以后出现问题。


在这种情况下,如果uint16_t* volatile semPtr改写会怎样?这应将指针标记为易失性(而不是所指向的值),以便对指针本身进行检查,例如semPtr == SOME_ADDR可能无法优化。但是,这也意味着一个不稳定的指向值。没有?
2014年

@Zyl不,不是。实际上,您的建议可能会发生。但是从理论上讲,最终可以使用一种编译器来优化对值的访问,因为它决定这些值均未更改。而且,如果您打算将volatile应用于该值而不是指针,那么您将被搞砸。同样,这不太可能,但是最好还是善于做正确的事,而不是利用今天碰巧起作用的行为。
iheanyi

1
@Doug T.一个更好的解释是这样
machineaddict

3
@curiousguy它没有错误地决定。它根据给出的信息做出了正确的推论。如果您未能将某些内容标记为volatile,则编译器可以自由地假定它不是volatile。这就是编译器在优化代码时所做的事情。如果有更多信息,即所说数据实际上是易变的,则程序员有责任提供该信息。有问题的编译器声称的内容实际上只是不好的编程。
iheanyi

1
@curiousguy不,仅仅因为volatile关键字出现一次并不意味着一切突然变得不稳定。我给出了一个场景,在该场景中,编译器执行正确的操作并获得与程序员错误预期相反的结果。就像“最烦人的解析”不是编译器错误的征兆一样,这里也不是。
iheanyi

82

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


9
这不仅对嵌入式系统有效,而且对所有设备驱动程序开发均有效。
MladenJanković08年

我唯一一次需要在8位ISA总线上读取两次相同的地址的情况-编译器有一个错误并忽略了它(Zortech C ++早期)
Martin Beckett,2009年

挥发性物质很少足以控制外部设备。对于现代MMIO,它的语义是错误的:您必须使太多的对象易失,这会影响优化。但是,现代的MMIO的行为就像普通的内存一样,直到设置了标志为止,因此不需要volatile。许多驱动程序从未使用过volatile。
curiousguy19年

69

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

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

对于某些没有代数意义但减少浮点误差的算法(例如Kahan求和),它也很有用。从代数角度讲,这是一个小问题,因此除非某些中间变量是可变的,否则它经常会被错误地优化。


5
在计算数值导数时,也要确保x + h-x == h,并将hh = x + h-x定义为易失性,以便可以计算出适当的增量,这也很有用。
Alexandre C. 2010年

5
+1,以我的经验确实如此,在某些情况下,浮点计算在Debug和Release中产生了不同的结果,因此为一种配置编写的单元测试对另一种配置失败。我们通过声明一个浮点变量volatile double而不是just double来解决该问题,以确保在继续进行进一步的计算之前将其从FPU精度截断为64位(RAM)精度。由于浮点误差的进一步放大,结果大为不同。
Serge Rogatch

您对“现代”的定义有些偏离。只有避免SSE / SSE2的32位x86代码受此影响,即使在10年前,它也不是“现代”的。MIPS / ARM / POWER都具有64位硬件寄存器,带有SSE2的x86也是如此。C ++实现x86-64始终使用SSE2,编译器也可以选择g++ -mfpmath=sse将其用于32位x86。您可以使用gcc -ffloat-store强制四舍五入到处使用的x87时甚至,也可以设置的x87精度53位尾数:randomascii.wordpress.com/2012/03/21/...
彼得·科德斯

但是仍然是一个很好的答案,对于过时的x87代码生成,您可以volatile用来强制在一些特定位置进行舍入而不会损失到处的好处。
彼得·科德斯

1
还是将不准确与不一致混淆?
Chipster,

48

摘自丹·萨克斯(Dan Saks)的“作为承诺的承诺”

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

以下是他有关该volatile关键字的三篇文章的链接:


23

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

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

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

LONG __cdecl InterlockedIncrement(
  __inout  LONG volatile *Addend
);

您绝对不需要声明变量volatile即可使用InterlockedIncrement。
curiousguy18年

现在C ++ 11提供了这个答案,std::atomic<LONG>因此您可以更安全地编写无锁代码,而不会出现优化纯负载/纯存储,重新排序或进行其他操作的问题。
彼得·科德斯

10

我在1990年代初曾使用的一个大型应用程序包含使用setjmp和longjmp的基于C的异常处理。volatile关键字对于变量的值是必需的,这些变量的值需要保留在用作“ catch”子句的代码块中,以免将这些var存储在寄存器中并由longjmp清除。


10

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

ISO / IEC 9899:2011§7.14.1.1 signal功能

¶5如果信号不是通过调用abortraise函数的结果发生的,则如果信号处理程序引用具有静态或线程存储持续时间的任何对象,而该对象不是非锁定原子对象,则该行为是不确定的,除非通过分配值声明为的对象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对您可以在信号处理程序中执行的操作更加宽容,但仍然存在局限性(其中一项局限性是printf()无法安全使用Standard I / O库等)。


7

为嵌入式开发,我有一个循环来检查可以在中断处理程序中更改的变量。如果没有“ volatile”,则循环将成为无条件操作-就编译器所知,该变量永不更改,因此可以优化检查。

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


7

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


7

除了按预期方式使用它以外,volatile还用于(模板)元编程中。它可以用来防止意外重载,因为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超载是合法的,因为我们知道巴不会通过非易失性T反正。volatile但是,该版本严格较差,因此,如果有非易失性f可用,切勿在过载分辨率中选择。

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


您可以举一个例子来详细说明吗?确实可以帮助我更好地理解。谢谢!
batbrat

易失性过载中的强制转换”强制转换是显式转换。这是一个SYNTAX构造。许多人对此感到困惑(甚至是标准作者)。
curiousguy18年

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

5
最好使用库,编译器内部函数或内联汇编代码。挥发物不可靠。
Zan Lynx

1
1和2都使用了原子操作,但是volatile不提供原子语义,并且原子的特定于平台的实现将取代使用volatile的需要,因此对于1和2,我不同意,您不需要使用volatile。

谁对提供原子语义的volatile说什么?我说过您需要对原子操作使用volatile,如果您认为这不是真的,请查看win32 API互锁操作的声明(此人也在其回答中对此做了解释)
MladenJanković

4

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

volatile优化过程中会忽略声明为的对象,因为它们的值可以随时通过当前代码范围之外的代码进行更改。系统始终volatile从存储位置读取对象的当前值,而不是在请求时将其值保存在临时寄存器中,即使先前的指令要求从同一对象获取值。

考虑以下情况

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

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

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

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

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

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

https://zh.wikipedia.org/wiki/Volatile_(计算机编程)


您发布的链接已经过时,并且无法反映当前的最佳做法。
蒂姆·塞吉

2

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

例如,我在嵌入式平台上工作,当时编译器对变量的值进行了一些错误的假设。如果未对代码进行优化,则程序可以正常运行。使用优化(由于这是关键例程,确实需要进行优化),因此代码无法正常工作。唯一的解决方案(尽管不是很正确)是将“ faulty”变量声明为volatile。


3
认为编译器没有优化对挥发物的访问的想法是一个错误的假设。该标准对优化一无所知。要求编译器遵守该标准的规定,但可以自由进行不干扰正常行为的任何优化。
总站

3
根据我的经验,gcc臂中所有优化“错误”的99.9%是程序员的错误。不知道这是否适用于此答案。只是关于一般主题的
骚扰

@Terminus“ 编译器没有优化对volatile的访问的想法是一个错误的假设 ”源?
curiousguy

2

您的程序即使没有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即使您的程序似乎无法正常运行,我也建议添加关键字。对于未来的变化,它使意图更清晰,更强大。


请注意,函数可以内联其代码,同时仍生成对Outline函数的引用(在链接时解析)。部分内联递归函数就是这种情况。函数还可以通过编译器“内联”其语义,即编译器假定副作用和结果在可能的副作用和可能的结果范围内(根据其源代码),而仍未内联。这基于“有效的一个定义规则”,该规则规定实体的所有定义应有效等效(如果不完全相同)。
curiousguy

通过使用volatile合格的函数指针void (* volatile fun_ptr)() = fun; fun_ptr();
避免

2

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

到底哪些测序细节很重要,取决于目标平台和应用领域。该标准没有提供特别详细的控制,而是选择了一个简单的模型:如果使用不合格的左值完成访问序列,则volatile编译器可能会重新排列并合并它们认为合适的值。如果使用volatile-qualified左值执行某项操作,则质量实现应提供针对目标平台和应用程序域的代码可能需要的任何其他顺序保证,而无需使用非标准语法。

不幸的是,许多编译器没有确定程序员需要什么保证,而是选择提供标准要求的最低限度保证。这使它volatile的实用性大大降低。例如,在gcc或clang上,需要实现基本的“手动互斥量”的程序员(其中一个已经获得并释放互斥量的任务将不会再执行此操作,直到另一个任务已经这样做)。四件事:

  1. 将获取和释放互斥锁放在编译器无法内联并且无法对其应用“完整程序优化”的函数中。

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

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

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

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

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

获取基本的“交接互斥体”需要进行volatile读取(以查看其是否已准备就绪),并且也不需要进行volatile写入(另一方在交回之前不会尝试重新获取它),但是必须执行无意义的volatile写入仍然比gcc或clang下可用的任何选项都要好。


1

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


1

所有答案都很好。但最重要的是,我想分享一个例子。

下面是一个小cpp程序:

#include <iostream>

int x;

int main(){
    char buf[50];
    x = 8;

    if(x == 8)
        printf("x is 8\n");
    else
        sprintf(buf, "x is not 8\n");

    x=1000;
    while(x > 5)
        x--;
    return 0;
}

现在,让我们生成上述代码的程序集(我将仅粘贴程序集中与此处相关的部分):

生成程序集的命令:

g++ -S -O3 -c -fverbose-asm -Wa,-adhln assembly.cpp

和组装:

main:
.LFB1594:
    subq    $40, %rsp    #,
    .seh_stackalloc 40
    .seh_endprologue
 # assembly.cpp:5: int main(){
    call    __main   #
 # assembly.cpp:10:         printf("x is 8\n");
    leaq    .LC0(%rip), %rcx     #,
 # assembly.cpp:7:     x = 8;
    movl    $8, x(%rip)  #, x
 # assembly.cpp:10:         printf("x is 8\n");
    call    _ZL6printfPKcz.constprop.0   #
 # assembly.cpp:18: }
    xorl    %eax, %eax   #
    movl    $5, x(%rip)  #, x
    addq    $40, %rsp    #,
    ret 
    .seh_endproc
    .p2align 4,,15
    .def    _GLOBAL__sub_I_x;   .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_x

您可以在程序集中看到未为其生成程序集代码的sprintf原因,因为编译器认为该代码不会x在程序外部更改。while循环也是如此。while循环由于优化而被完全删除,因为编译器将其视为无用的代码,因此直接分配5给了它x(请参见movl $5, x(%rip))。

如果外部进程/硬件将xx = 8;和之间的某个位置的值更改怎么办,就会发生问题if(x == 8)。我们希望else块可以工作,但是不幸的是编译器已经修剪掉了那部分。

现在,为了解决这个问题,在中assembly.cpp,让我们int x;转到volatile int x;并快速查看生成的汇编代码:

main:
.LFB1594:
    subq    $104, %rsp   #,
    .seh_stackalloc 104
    .seh_endprologue
 # assembly.cpp:5: int main(){
    call    __main   #
 # assembly.cpp:7:     x = 8;
    movl    $8, x(%rip)  #, x
 # assembly.cpp:9:     if(x == 8)
    movl    x(%rip), %eax    # x, x.1_1
 # assembly.cpp:9:     if(x == 8)
    cmpl    $8, %eax     #, x.1_1
    je  .L11     #,
 # assembly.cpp:12:         sprintf(buf, "x is not 8\n");
    leaq    32(%rsp), %rcx   #, tmp93
    leaq    .LC0(%rip), %rdx     #,
    call    _ZL7sprintfPcPKcz.constprop.0    #
.L7:
 # assembly.cpp:14:     x=1000;
    movl    $1000, x(%rip)   #, x
 # assembly.cpp:15:     while(x > 5)
    movl    x(%rip), %eax    # x, x.3_15
    cmpl    $5, %eax     #, x.3_15
    jle .L8  #,
    .p2align 4,,10
.L9:
 # assembly.cpp:16:         x--;
    movl    x(%rip), %eax    # x, x.4_3
    subl    $1, %eax     #, _4
    movl    %eax, x(%rip)    # _4, x
 # assembly.cpp:15:     while(x > 5)
    movl    x(%rip), %eax    # x, x.3_2
    cmpl    $5, %eax     #, x.3_2
    jg  .L9  #,
.L8:
 # assembly.cpp:18: }
    xorl    %eax, %eax   #
    addq    $104, %rsp   #,
    ret 
.L11:
 # assembly.cpp:10:         printf("x is 8\n");
    leaq    .LC1(%rip), %rcx     #,
    call    _ZL6printfPKcz.constprop.1   #
    jmp .L7  #
    .seh_endproc
    .p2align 4,,15
    .def    _GLOBAL__sub_I_x;   .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_x

在这里你可以看到汇编代码的sprintfprintfwhile产生循环。优点是,如果x变量由某些外部程序或硬件更改,sprintf则将执行部分代码。同样,while循环现在可以用于繁忙的等待。


0

其他答案已经提到避免进行一些优化以:

  • 使用内存映射的寄存器(或“ MMIO”)
  • 编写设备驱动程序
  • 简化程序调试
  • 使浮点计算更具确定性

每当您需要一个值似乎来自外部且不可预测并且避免基于已知的值进行编译器优化时,以及当结果未实际使用但您需要对它进行计算或使用它时,可变性都是必不可少的您想为基准测试多次计算它,并且您需要计算以精确的点开始和结束。

易失性读取就像输入操作(如scanf或使用cin):值似乎来自程序的外部,因此任何依赖于该值的计算都必须在该值之后开始

易失性写入就像输出操作(如printf或使用cout):该值似乎在程序外部传递,因此,如果该值取决于计算,则需要在完成之前完成

因此,可以使用一对易失性读/写来控制基准并使时间测量有意义

如果没有volatile,则可以由编译器之前开始计算,因为没有什么可以阻止使用诸如time measurement之类的功能对计算进行重新排序

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.