volatile

字面意思为 易变的、无定性的、 无常性的、不稳定的; 也就是说 使用volatile修饰的变量是易变的, 遇到这个关键词声明的变量, 编译器对访问变量的代码不再进行优化, 从而可以提供对特殊地址的稳定访问。变量如果加了 volatile 修饰,则会从内存重新装载内容,而不是直接从寄存器拷贝内容。

volatile 的作用 是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

特点:
1.告诉编译器不做任何优化
2.用volatile定义的变量会在程序外被改变,每次使用都要在原始内存地址读取数据,不能被备份

缺点:
使用过多会降低代码性能

使用场合:
1.中断服务程序中为其他程序检测的变量,要用volaite
2.多任务环境下各个任务间共享的标志,用volatile(操作系统)
3.存储器映射的硬件寄存器用volatile,因为每次对它的读写都可能有不同意义

分析

# 代码1
int i = 0;
int k =i;
int j = i;

# 代码2
volatile k = 1;
int l = k;

# 代码2得到的优化代码 gcc -O1 -S demo.c -o demo.s

        movl    $1, -4(%rsp)  
        movl    -4(%rsp), %eax
        movl    -4(%rsp), %eax
        movl    $0, %eax

对于代码1,它的执行过程是 i = 0, 0 ->reg寄存器,k = reg值,就是说,k=i实际上是从寄存器里读出来的;
对于代码2,执行过程是 i = 0, k = *(int *)(&i); 它会从i的内存地址上去读,当然值会非常准确,但是效率会比从寄存器中读要慢很多。

编译器优化介绍

由于内存访问速度远不及CPU处理速度,为提高机器整体性能,在硬件上引入硬件高速缓存Cache,加速对内存的访问。另外在现代CPU中指令的执行并不一定严格按照顺序执行,没有相关性的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度。以上是硬件级别的优化。再看软件一级的优化:一种是在编写代码时由程序员优化,另一种是由编译器进行优化。编译器优化常用的方法有:将内存变量缓存到寄存器;调整指令顺序充分利用CPU指令流水线,常见的是重新排序读写指令。对常规内存进行优化的时候,这些优化是透明的,而且效率很好。由编译器优化或者硬件重新排序引起的问题的解决办法是在从硬件(或者其他处理器)的角度看必须以特定顺序执行的操作之间设置内存屏障(memory barrier),Linux 提供了一个宏解决编译器的执行顺序问题。
void Barrier(void)
这个函数通知编译器插入一个内存屏障,但对硬件无效,编译后的代码会把当前CPU寄存器中的所有修改过的数值存入内存,需要这些数据的时候再重新从内存中读出。

volatile总是与优化有关,编译器有一种技术叫做数据流分析,分析程序中的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常量传播等优化,进一步可以消除一些代码。但有时这些优化不是程序所需要的,这时可以用volatile关键字禁止做这些优化。

相关问题

  1. 一个参数既可以是const还可以是volatile吗?

可以,例如只读的状态寄存器。它是 volatile 因为它可能被意想不到地改变。它是 const 因为 程序不应该试图去修改它

  1. 一个指针可以是 volatile 吗?

可以,当一个中服务子程序修改一个指向一个 buffer 的指针时。

  1. 以下代码有什么错误
int square(volatile int *ptr) 
{ 
    return *ptr * *ptr; 
}

这段代码的目的是用来返指针ptr指向值的平方,但是,由于ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int *ptr)  
    { 
         int a,b; 
         a = *ptr; 
         b = *ptr; 
         return a * b; 
     }

由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:


long square(volatile int *ptr)  
{ 
    int a; 
    a = *ptr; 
    return a * a; 
}

volatile 不能保证原子性

volatile 能够防止重排序、线程可见性,不能保证原子性,非线程安全
修改volatile变量分为四步:

  • 读取volatile的值
  • 修改变量值
  • local值写回
  • 插入内存屏障,即lock指令,让其他线程可见

可以看出前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改。原子性需要使用锁来保证。

文档更新时间: 2021-03-11 16:24   作者:周国强