10.3 volatile修饰符
volatile修饰符的重要性对于从事嵌入式编程的程序员来说是不言而喻的,对volatile的了解程度常常被不少公司作为招聘嵌入式编程人员时衡量一个应聘者是否合格的参考标准之一。为什么volatile如此重要呢?因为嵌入式编程人员要经常同中断、底层硬件等打交道,这些都用到volatile,因此嵌入式程序员必须掌握volatile的使用。
其实就像读者所熟悉的const一样,volatile是一个类型修饰符。在开始讲解volatile之前,先讲解一个后面要用到的函数,知道如何使用此函数的读者可以跳过这部分内容。
原型:
int gettimeofday(struct timevaltv,struct timezonetz);
头文件:#include<sys/time.h>。
功能:获取当前时间。
返回值:如果成功获取当前时间,那么返回0,否则返回-1,错误代码存于errno中。
gettimeofday()会把目前的时间通过tv所指的结构返回,并将当地时区的信息放到tz所指的结构中。
timeval结构定义为:
struct timeval{
long tv_sec;
long tv_usec;
};
timezone结构定义为:
struct timezone{
int tz_minuteswest;
int tz_dsttime;
};
先介绍timeval结构体,其中,tv_sec存放的是秒,tv_usec存放的是微秒。timezone成员变量我们很少使用,在此简单地介绍。timezone结构体在gettimeofday()函数中的作用是把当地时区的信息存放到tz所指的结构中,其中,tz_minuteswest变量中存放的是与格林威治时间的时差,tz_dsttime存放的则是时间的修正方式。在此主要关注前一个成员变量timeval,后一个timezone在此不使用,因此在使用gettimeofday()函数的时候把后一个参数设为NULL。下面先来看一段简单的代码。
include<stdio.h>
include<sys/time.h>
int main(int argc,char*argv[])
{
struct timeval start,end;
gettimeofday(&start,NULL);/测试起始时间/
double timeuse;
int j;
for(j=0;j<1000000;j++)
;
gettimeofday(&end,NULL);/测试终止时间/
timeuse=1000000*(end.tv_sec-start.tv_sec)+end.tv_usec-start.tv_usec;
timeuse/=1000000;
printf("运行时间为:%f\n",timeuse);
return 0;
}
运行结果:
root@ubuntu:/home#./p
运行时间为:0.002736
现在简单地分析一下以上代码,通过end.tv_sec-start.tv_sec得到终止时间与起始时间之间以秒为单位的时间间隔,然后通过end.tv_usec-start.tv_usec得到终止时间与起始时间之间以微妙为单位的时间间隔。由于时间单位的原因,在此将由(end.tv_sec-start.tv_sec)得到的结果乘以1 000 000转换为微秒进行计算,之后再使用“timeuse/=1000000;”将其转换为秒。了解了如何通过gettimeofday()函数来测试start到end代码之间的运行时间,接下来介绍volatile修饰符。
通常,为了防止代码中一个变量在意想不到的情况下被改变,会将变量定义为volatile,从而使编译器不会自作主张地去“动”这个变量的值。准确地说就是,每次用到这个变量时都必须重新从内存中直接读取这个变量的值,而不是使用保存在寄存器中的备份。
在举例之前,先大概介绍一下Debug和Release编译模式的区别。通常,Debug模式被称为调试版本,它包含调试信息,并且不需要做任何优化,便于程序员调试程序。Release模式被称为发布版本,它往往需要进行各种优化,使程序在代码大小和运行速度上都是最优的,以便用户很好地使用。大致地知道了Debug和Release编译模式的区别之后,下面来看一段代码。
include<stdio.h>
void main()
{
int a=12;
printf("a的值为:%d\n",a);
__asm{mov dword ptr[ebp-4],0h}
int b=a;
printf("b的值为:%d\n",b);
}
先分析上面的代码,其中使用了一句__asm{mov dword ptr[ebp-4],0h}来修改变量a在内存中的值。前面已经讲解了Debug和Release编译模式的区别,现在来对比一下运行结果,在编译的时候别忘了选择编译运行的模式。
使用Debug编译模式的运行结果为:
a的值为:12
b的值为:0
使用Release编译模式的运行结果为:
a的值为:12
b的值为:12
从上面的运行结果可以发现,在Release模式下进行优化后b的值变为12,但是使用Debug模式时b的值为0。为什么会出现这样的情况呢?先不说答案,再来看看下面一段代码(注:使用VC++6.0编译运行)。
include<stdio.h>
void main()
{
int volatile a=12;
printf("a的值为:%d\n",a);
__asm{mov dword ptr[ebp-4],0h}
int b=a;
printf("b的值为:%d\n",b);
}
使用Debug编译模式的运行结果为:
a的值为:12
b的值为:0
使用Release编译模式的运行结果为:
a的值为:12
b的值为:0
我们发现,在这种情况下不管是使用Debug模式还是使用Release模式都是一样的结果。现在来分析此前介绍的Debug和Release编译模式的区别。
先分析上一段代码,由于在Debug模式下并没有对代码进行优化,因此每次在代码中使用a值的时候都是从它的内存地址直接读取的,在使用“__asm{mov dword ptr[ebp-4],0h}”语句改变了a值之后,接下来再次使用a值时从内存中直接读取,得到的是更新后的a值;但是在Release模式下运行代码的时候,发现b值为a之前的值,而不是更新后的a值,这是由于编译器在优化的过程中做了优化处理。编译器发现在对a赋值后没有再次改变a值,因此编译器把a值备份到一个寄存器中,在之后的操作中再次使用a值时就直接操作这个寄存器,而不去读取a的内存地址,因为读取寄存器的速度要快于直接读取内存的速度,这就使得读到的a值为之前的12,而不是更新后的0。
在第二段代码中使用了一个volatile修饰符,这样不管在什么模式下得到的都是更新后的a的值,因为volatile修饰符的作用就是告诉编译器不要对它所修饰的变量进行任何优化,每次取值都要直接从内存地址得到。从这里可以看出,对于代码中那些易变量,最好使用volatile进行修饰,以得到每次对其更新后的值。为了加深下读者的印象,再来看下面的一段代码。
include<stdio.h>
include<sys/time.h>
int main(int argc,char*argv[])
{
struct timeval start,end;
gettimeofday(&start,NULL);/测试起始时间/
double timeuse;
int j;
for(j=0;j<10000000;j++)
;
gettimeofday(&end,NULL);/测试终止时间/
timeuse=1000000*(end.tv_sec-start.tv_sec)+end.tv_usec-start.tv_usec;
timeuse/=1000000;
printf("运行时间为:%f\n",timeuse);
return 0;
}
与之前测试时间的代码一样,这里只是增大了for循环的次数。
先来看不使用优化的结果:
root@ubuntu:/home#gcc time.c-o p
root@ubuntu:/home#./p
运行时间为:0.028260
而使用了优化的运行结果是:
root@ubuntu:/home#gcc-o p time.c-O2
root@ubuntu:/home#./p
运行时间为:0.000001
从结果可以明显看出差距如此之大,但是如果在上面的代码中将“int j;”修改为“int volatile j;”,如下:
include<stdio.h>
include<sys/time.h>
int main(int argc,char*argv[])
{
struct timeval start,end;
gettimeofday(&start,NULL);/测试起始时间/
double timeuse;
int volatile j;
for(j=0;j<10000000;j++)
;
gettimeofday(&end,NULL);/测试终止时间/
timeuse=1000000*(end.tv_sec-start.tv_sec)+end.tv_usec-start.tv_usec;
timeuse/=1000000;
printf("运行时间为:%f\n",timeuse);
return 0;
}
不使用优化的运行结果为:
root@ubuntu:/home#gcc time.c-o p
root@ubuntu:/home#./p
运行时间为:0.027647
而使用了优化的运行结果为:
root@ubuntu:/home#gcc-o p time.c-O2
root@ubuntu:/home#./p
运行时间为:0.027390
可以发现,此时不管是否使用优化语句,运行时间只有微小的差异,这微小的差异是计算机本身所导致的。因此通过对比可知,使用了volatile的变量在使用优化语句时,for循环并没有得到优化,因为for循环执行的是一个空操作,通常,使用了优化语句使这个for循环被优化后,根本就不执行,就好比编译器在编译的过程中将i的值设置为大于或者等于10 000 000的一个数,使for循环语句不会执行。但是由于使用了volatile,使编译器不会自作主张地改变i值,因此循环体得到了执行。举这个例子的目的是让读者牢记,如果定义了volatile变量,那么它就不会被编译器优化。
volatile还有哪些值得注意的地方呢?由于访问寄存器的速度要快过直接访问内存的速度,因此编译器一般都会减少对内存的访问,但是如果对变量加上volatile修饰,则编译器会保证对此变量的读写操作都不被优化。这样说可能有些抽象了,再看下面的代码,在此只简要地给出几步。
main()
{
int i=o;
while(i==0)
{
……
}
}
分析以上代码,如果没有在while循环体结构中改变i的值,那么编译器在编译的过程中会将i的值备份到一个寄存器中,每次执行判断语句时就从该寄存器取值,这将是一个死循环,除非做如下的修改:
main()
{
int volatile i=o;
while(i==0)
{
……
}
}
我们在i的前面加上了volatile,假设在while循环体内执行的是跟前面一段代码完全一样的操作,这时就不能说此时的while循环是一个死循环了,因为编译器不会再对i值进行“备份”操作了,每次执行判断的时候都会直接从i的内存地址中读取,一旦其值发生变化,就退出循环体。
最后介绍volatile在实际应用中的适用场景有以下几个。
中断服务程序中修改的供其他程序检测的变量需要加volatile;
多任务环境下各任务间共享的标志应该加volatile;
存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同的意义。