8.volatile 的作用与使用场景-灵析社区

菜鸟码转

1.volatile 的简介:

volatile 的作用:当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为 volatile,告知编译器不应对这样的对象进行优化。volatile 关键字修饰变量后,提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有 volatile 关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

使用 volatile 关键字试图阻止编译器过度优化,volatile 主要作用如下:

  • 阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回;(缓存一致性协议、轻量级同步)
  • 阻止编译器调整操作 volatile 变量的指令排序。

2.volatile 的作用:

  • 读取变量时,阻止编译器对缓存的优化: volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。比如声明时 volatile 变量,int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据,而且读取的数据立刻被保存。

C++

volatile int i=10;
int a = i;
...
// 其他代码,并未明确告诉编译器,对 i 进行过操作
int b = i;

volatile 修饰 i 后,表明每次使用 i 时必须从 i 的地址中读取,因而编译器生成的汇编代码会重新从 i 的地址读取数据放在 b 中。如果不加 volatile,编译器会进行优化,编译器发现两次从 i 读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中,而不是重新从 i 里面读,如果 i 是一个寄存器变量,则 i 可能已经被外部程序进行改写,因此 volatile可以保证对特殊地址的稳定访问。

  • 写入变量时,阻止编译器对指令顺序的优化: 在某些特定的场景下,比如读写寄存器或者操作某些硬件,需要按照某些特定的指令序列读写寄存器,而不能忽略其中的某些步骤,比如如下程序:

C++

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    int ra = 0;
    int rb = 0;
    ra = 0x1111;
    ra = 0x2222;
    ra = 0x3333;
    rb = ra;
    printf("%d\n", ra);
    return 0;
}

我们使用编译器优化:

shell

g++ -S -O3 test.cpp -o test

查看编译后的代码为:

C

0000000000000560 <main>:
 560:   48 83 ec 08             sub    $0x8,%rsp
 564:   ba 33 33 00 00          mov    $0x3333,%edx
 569:   bf 01 00 00 00          mov    $0x1,%edi
 56e:   31 c0                   xor    %eax,%eax
 570:   48 8d 35 8d 01 00 00    lea    0x18d(%rip),%rsi        # 704 <_IO_stdin_used+0x4>
 577:   e8 c4 ff ff ff          callq  540 <__printf_chk@plt>
 57c:   31 c0                   xor    %eax,%eax
 57e:   48 83 c4 08             add    $0x8,%rsp
 582:   c3                      retq
 583:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
 58a:   00 00 00
 58d:   0f 1f 00                nopl   (%rax)

我们可以看到编译的汇编代码,变量 ra 直接赋值为 0x3333,从而省略了中间赋值过程:

C++

ra = 0x1111;
ra = 0x2222;
ra = 0x3333;

在初始化某些硬件平台时,我们需要按照芯片手册对寄存器依次连续写入一系列的指令,上述程序中假如 ra 为一个寄存器变量,则依次为寄存器按照顺序写入特定的值来操作硬件。如果不添加 volatile 关键字,某些编译器会对上述代码进行优化,直接会省略中间的写入指令。实际在硬件在初始化中不能省略中间的指令,此时编译器的优化反而造成错误,此时我们用 volatile 修饰变量 ra,可以阻止编译器对指令顺序进行优化。

C

0000000000000560 <main>:
 560:   48 83 ec 18             sub    $0x18,%rsp
 564:   48 8d 35 b9 01 00 00    lea    0x1b9(%rip),%rsi        # 724 <_IO_stdin_used+0x4>
 56b:   bf 01 00 00 00          mov    $0x1,%edi
 570:   c7 44 24 0c 00 00 00    movl   $0x0,0xc(%rsp)
 577:   00
 578:   c7 44 24 0c 11 11 00    movl   $0x1111,0xc(%rsp)
 57f:   00
 580:   c7 44 24 0c 22 22 00    movl   $0x2222,0xc(%rsp)
 587:   00
 588:   c7 44 24 0c 33 33 00    movl   $0x3333,0xc(%rsp)
 58f:   00
 590:   8b 44 24 0c             mov    0xc(%rsp),%eax
 594:   31 c0                   xor    %eax,%eax
 596:   8b 54 24 0c             mov    0xc(%rsp),%edx
 59a:   e8 a1 ff ff ff          callq  540 <__printf_chk@plt>
 59f:   31 c0                   xor    %eax,%eax
 5a1:   48 83 c4 18             add    $0x18,%rsp
 5a5:   c3                      retq
 5a6:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
 5ad:   00 00 00

3.volatile 的应用场景:

在实际场景中除了操纵硬件需要用到 volatile 以外,更多的可能是多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程可见,此时我们就需要使用 volatile 进行修饰。一般说来,volatile 用在如下的几个地方:

  • 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
  • 多任务环境下各任务间共享的标志应该加 volatile;
  • 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能有不同意义;

阅读量:2017

点赞量:0

收藏量:0