c++ volatile用法

volatile关键字

volatile是c/c++中一个鲜为人知的关键字,该关键字告诉编译器不要持有变量的临时拷贝,它可以适用于基础类型

如:int,char,long......也适用于C的结构和C++的类。当对结构或者类对象使用volatile修饰的时候,结构或者类的所有成员都会被视为volatile.使用volatile并不会否定对CRITICAL_SECTION,Mutex,Event等同步对象的需要

例如:


1int i;
2= i + 3;

无论如何,总是会有一小段时间,i会被放在一个寄存器中,因为算术运算只能在寄存器中进行。一般来说,volatitle关键字适用于行与行之间,而不是放在行内。

我们先来实现一个简单的函数,来观察一下由编译器产生出来的汇编代码中的不足之处,并观察volatile关键字如何修正这个不足之处。在这个函数体内存在一个busy loop(所谓busy loop也叫做busy waits,是一种高度浪费CPU时间的循环方法)


1void getKey(char* pch)
2{
3 while (*pch == 0);
4}

当你在VC开发环境中将最优化选项都关闭之后,编译这个程序,将获得以下结果(汇编代码)


 1;       while (*pch == 0)
 2$L27
 3 ; Load the address stored in pch
 4 mov eax, DWORD PTR _pch$[ebp]
 5 ; Load the character into the EAX register
 6 movsx eax, BYTE PTR [eax]
 7 ; Compare the value to zero
 8 test eax, eax
 9 ; If not zero, exit loop
10 jne $L28
11 ;
12 jmp $L27
13$L28
14;}

这段没有优化的代码不断的载入适当的地址,载入地址中的内容,测试结果。效率相当的低,但是结果非常准确现在我们再来看看将编译器的所有最优化选项开关都打开以后,重新编译程序,生成的汇编代码,和上面的代码

比较一下有什么不同


 1;
 2 ; Load the address stored in pch
 3 mov eax, DWORD PTR _pch$[esp-4]
 4 ; Load the character into the AL register
 5 movsx al, BYTE PTR [eax]
 6while (*pch == 0)
 7 ; Compare the value in the AL register to zero
 8 test al, al
 9 ; If still zero, try again
10 je SHORT $L84
11 ;
12;}

从代码的长度就可以看出来,比没有优化的情况要短的多。需要注意的是编译器把MOV指令放到了循环之外。这在单线程中是一个非常好的优化,但是,在多线程应用程序中,如果另一个线程改变了变量的值,则循环永远不会结束。被测试的值永远被放在寄存器中,所以该段代码在多线程的情况下,存在一个巨大的BUG。解决方法是重新

写一次getKey函数,并把参数pch声明为volatile,代码如下:


1void getKey(volatile char* pch)
2{
3 while (*pch == 0) ;
4}

这次的修改对于非最优化的版本没有任何影响,下面请看最优化后的结果:


 1;{
 2 ; Load the address stored in pch
 3 mov eax, DWORD PTR _pch$[esp-4]
 4;       while (*pch == 0)
 5$L84:
 6 ; Directly compare the value to zero
 7 cmp BYTE PTR [eax], 0
 8 ; If still zero, try again
 9 je SHORT $L84
10 ;
11;}

这次的修改结果比较完美,地址不会改变,所以地址声明被移动到循环之外。地址内容是volatile,所以每次循环之中它不断的被重新检查。把一个const volatile变量作为参数传递给函数是合法的。如此的声明意味着函数不能改变变量的值,但是变量的值却可以被另一个线程在任何时间改变掉。


一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
1) 并行设备的硬件寄存器(如:状态寄存器)
2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3) 多线程应用中被几个任务共享的变量
回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。
1)一个参数既可以是const还可以是volatile吗?解释为什么。
2); 一个指针可以是volatile 吗?解释为什么。
3); 下面的函数有什么错误:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
下面是答案:
1)是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2); 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3) 这段代码有点变态。这段代码的目的是用来返指针*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;
}


C和C++中的volatile[编辑]

在C,以及C++中,volatile关键字的作用[1]

  • 允许访问内存映射设备
  • 允许在setjmplongjmp之间使用变量
  • 允许在信号处理函数中使用sig_atomic_t变量

根据相关的标准(C,C++,POSIX,WIN32)和目前绝大多数实现,对volatile变量的操作并不是原子的,也不能用来为线程建立严格的happens-before关系。volatile关键字就像便携式线程构建一样基本没什么用处[2][3][4][5][6]

C语言中MMIO的例子[编辑]

在这里例子中,代码将foo的值设置为0。然后开始不断地轮询它的值直到它变成255

static int foo;
 
void bar(void) {
    foo = 0;
 
    while (foo != 255)
         ;
}

一个执行优化的编译器会提示没有代码能修改foo的值,并假设它永远都只会是0.因此编译器将用类似下列的无限循环替换函数体:

void bar_optimized(void) {
    foo = 0;
 
    while (true)
         ;
}

但是,foo可能指向一个随时都能被计算机系统其他部分修改的地址,例如一个连接到中央处理器的设备的硬體暫存器,上面的代码永远检测不到这样的修改。如果不使用volatile关键字,编译器将假设当前程序是系统中唯一能改变这个值部分(这是到目前为止最广泛的一种情况)。 为了阻止编译器像上面那样优化代码,需要使用volatile关键字:

static volatile int foo;
 
void bar (void) {
    foo = 0;
 
    while (foo != 255)
        ;
}

这样修改以后循环条件就不会被优化掉,当值改变的时候系统将会检测到。

C语言中的优化对比[编辑]

下面的C程序和后面的汇编代码展示了volatile关键字如何影响编译器的输出。这里使用的编译器是GCC。


你可能感兴趣的:(C/C++)