代码之家  ›  专栏  ›  技术社区  ›  theschmitzer

为什么volatile存在?

  •  275
  • theschmitzer  · 技术社区  · 17 年前

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

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

    19 回复  |  直到 13 年前
        1
  •  231
  •   Flexo - Save the data dump sunny moon    14 年前

    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);
    }
    

    没有 不稳定的 ,优化器认为循环是无用的(这家伙从不设置值!他疯了,扔掉那段代码!),我的代码在没有获取信号量的情况下就会继续,这会在以后造成问题。

        2
  •  73
  •   ChrisN    13 年前

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

        3
  •  65
  •   tfinniga    10 年前

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

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

    它也适用于一些没有代数意义但减少浮点误差的算法,如Kahan求和。从代数上讲,这是一个nop,所以除非一些中间变量是易变的,否则它经常会被错误地优化。

        4
  •  41
  •   Marijn Huizendveld    17 年前

    从a “波动如承诺” Dan Saks的文章:

    (…)volatile对象是指其值可能会自发变化的对象。也就是说,当你声明一个对象为volatile时,你是在告诉编译器,即使程序中似乎没有语句改变它,该对象也可能改变状态。”

    以下是他关于 volatile 关键字:

        5
  •  23
  •   Frederik Slijkerman    17 年前

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

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

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

    LONG __cdecl InterlockedIncrement(
      __inout  LONG volatile *Addend
    );
    
        6
  •  10
  •   Jeff Doar    17 年前

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

        7
  •  10
  •   Jonathan Leffler    12 年前

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

    ISO/IEC 9899:2011§7.14.1.1 signal 功能

    5如果信号不是呼叫的结果 abort raise 功能 如果信号处理程序引用任何具有静态或线程的对象,则行为未定义 非无锁原子对象的存储持续时间,除非为 对象声明为 挥发性sig_atomict ,或者信号处理程序调用任何函数 在标准库中,而不是 夭折 功能 _Exit 功能 quick_exit 功能,或 信号 第一个参数等于 与引发处理程序调用的信号相对应的信号编号。 此外,如果向 信号 函数返回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() 等不能安全使用)。

        8
  •  8
  •   indentation    17 年前

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

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

        9
  •  7
  •   Arkadiy    17 年前

    我在调试构建中使用过它,当编译器坚持优化一个变量时,我希望在我逐步执行代码时能够看到它。

        10
  •  6
  •   Mladen Janković    17 年前

    除了按预期使用它之外,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 不管怎样。这个 不稳定的 不过,版本严格来说更差,所以如果非易失性 f 可用。

    请注意,代码实际上从不依赖于 不稳定的 存储器访问。

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

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

    对象声明为 不稳定的 因为它们的值可以随时被当前代码范围之外的代码更改。系统始终读取a的当前值 不稳定的 即使之前的指令要求从同一对象中获取值,也不要在请求时将其值保存在临时寄存器中。

    考虑以下情况

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

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

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

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

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

    Volatile: A programmer’s best friend

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

        13
  •  2
  •   INS    17 年前

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

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

    每当你需要一个值看起来来自外部并且不可预测,避免基于已知值的编译器优化,以及当一个结果实际上没有被使用但你需要计算它,或者它被使用了但你想为基准计算几次,并且你需要计算在精确的点上开始和结束时,Volatile是必不可少的。

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

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

    所以 一对易失性读/写可用于驯服基准测试,使时间测量有意义 .

    没有volatile,你的计算可以在之前由编译器启动, 因为没有什么能阻止使用时间测量等函数对计算进行重新排序 .

        14
  •  2
  •   Joachim    8 年前

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

    下面是一个小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 循环。 与…同时 由于优化,循环被完全删除,因为编译器将其视为无用的代码,因此直接分配 5 英语字母表的第24个字母 (参见 movl $5, x(%rip) ).

    当外部进程/硬件改变以下值时,就会出现问题 英语字母表的第24个字母 介于 x = 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
    

    在这里,您可以看到以下部件代码 把格式数据写成串 , printf 与…同时 生成了循环。优点是,如果 英语字母表的第24个字母 变量被一些外部程序或硬件改变, 把格式数据写成串 部分代码将被执行。同样地 与…同时 循环现在可用于繁忙等待。

        15
  •  2
  •   supercat    7 年前

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

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

        16
  •  1
  •   bugs king    8 年前

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

    如前所述 不稳定的 关键字对以下情况有帮助

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

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

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

    然后有或没有 不稳定的 产生几乎相同的结果。

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

    但要小心,当你的函数g()变为内联时(要么是由于显式更改,要么是由于编译器/链接器的聪明),如果你忘记了 不稳定的 关键字!

    因此,我建议添加 不稳定的 关键字,即使你的程序似乎没有。它使意图在未来的变化中更加清晰和稳健。

    推荐文章