为什么存在易变?

时间:2008-09-16 13:59:46

标签: c++ volatile c++-faq

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

就我而言,我从未故意需要它。

19 个答案:

答案 0 :(得分:239)

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

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,优化器将循环视为无用(该伙伴永远不会设置值!他很疯狂,摆脱代码!)并且我的代码将在没有获取信号量的情况下继续进行,导致以后出现问题

答案 1 :(得分:77)

在开发嵌入式系统或设备驱动程序时需要

volatile,您需要读取或写入内存映射的硬件设备。特定设备寄存器的内容可能随时更改,因此您需要volatile关键字以确保编译器不会优化此类访问。

答案 2 :(得分:68)

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

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

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

答案 3 :(得分:43)

来自Dan Saks的嵌入式系统文章:

“volatile对象的值可能会自发地改变。也就是说,当你声明一个对象是volatile时,你告诉编译器该对象可能会改变状态,即使程序中的任何语句都没有改变它。“

链接到Saks先生关于volatile关键字的两篇精彩文章:

http://www.embedded.com/columns/programmingpointers/174300478 http://www.embedded.com/columns/programmingpointers/175801310

答案 4 :(得分:23)

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

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

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

LONG __cdecl InterlockedIncrement(
  __inout  LONG volatile *Addend
);

答案 5 :(得分:10)

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

  

ISO / IEC 9899:2011§7.14.1.1signal函数

     

¶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要宽容得多,但仍有局限性(其中一个限制是标准I / O库 - printf()等 - 不能使用安全地)。

答案 6 :(得分:10)

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

答案 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重载中的强制转换是合法的,因为我们知道bar无论如何都不会传递非易失性T。但volatile版本严格更糟,因此如果非易失性f可用,则永远不会在重载解析中选择。

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

答案 8 :(得分:7)

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

答案 9 :(得分:7)

为嵌入式开发,我有一个循环来检查可以在中断处理程序中更改的变量。如果没有“volatile”,循环就会变成noop - 就编译器而言,变量永远不会改变,因此它会优化检查。

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

答案 10 :(得分:6)

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

答案 11 :(得分:4)

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

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

请考虑以下情况

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

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

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

1)打开优化时,代码可能无法正常工作。

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

Volatile: A programmer’s best friend

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

答案 12 :(得分:2)

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

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

答案 13 :(得分:2)

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

如前所述,volatile关键字有助于处理

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

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

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

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

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

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

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

答案 14 :(得分:2)

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

确切的测序细节很重要,具体取决于目标平台和应用领域。该标准没有提供特别详细的控制,而是选择了一个简单的模型:如果使用不合格using ptA as this的左值完成访问序列,则编译器可能会重新排列并合并它们认为合适的值。如果使用ptA->限定的左值完成操作,那么质量实现应提供针对其预期平台和应用程序域的代码可能需要的任何其他顺序保证,而不必要求使用非标准语法。

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

  1. 将互斥量的获取和发布置于编译器无法内联且无法对其应用“完整程序优化”的功能中。

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

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

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

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

  1. 确保在需要获取或发布的所有地方执行符合register要求的写操作。

获取基本的“切换互斥体”需要进行volatile的读取(以查看其是否已准备就绪),也不需要进行volatile的写入(另一端也不会尝试)重新获取它,直到将其退还为止),但是必须执行无意义的volatile写操作仍然比gcc或clang下可用的任何选项都要好。

答案 15 :(得分:1)

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

答案 16 :(得分: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循环现在可以用于繁忙的等待。

答案 17 :(得分:1)

我想引用赫伯萨特的GotW #95的话,可以帮助理解volatile变量的含义:

<块引用>

C++ volatile 变量(在 C#Java 等语言中没有类似物)总是超出了本文和任何其他关于内存模型和同步。这是因为 C++ volatile 变量根本与线程或通信无关,也不与这些东西交互。相反,C++ volatile 变量应该被视为进入语言之外的不同世界的门户——根据定义,一个内存位置不遵守语言的内存模型,因为该内存位置是由硬件访问的(例如,由子卡写入),有多个地址,或者“奇怪”且超出语言范围。因此,C++ volatile 变量普遍是每个关于同步的指南的例外,因为使用普通工具(互斥锁、原子等)总是天生“活泼”和不可同步的,并且更普遍地存在于所有正常之外语言和编译器,包括它们通常不能被编译器优化(因为不允许编译器知道它们的语义;volatile int vi; 的行为可能不像正常的 int,你可以甚至假设像 vi = 5; int read_back = vi; 这样的代码肯定会导致 read_back == 5,或者像 int i = vi; int j = vi; 这样的代码读取 vi 两次将导致 i == j,如果 {例如,{1}} 是一个硬件计数器)。

答案 18 :(得分:0)

其他答案已经提到了避免某些优化,以便:

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

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

易失性读取就像一个输入操作(例如scanf或对cin的使用):该值似乎来自程序外部,因此任何具有对值的依赖需要在此之后开始

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

因此可以使用一对易失性的读/写操作来驯服基准测试并使时间测量变得有意义

在没有波动的情况下,您的计算可以由编译器在开始之前进行,因为没有什么可以阻止对诸如时间测量等功能的计算进行重新排序