依据c/c++语法,const能够呈现的当地,volatile简直也都能够呈现。可是,const润饰的目标其值不能改动,而volatile润饰的目标其值能够随意地改动,也便是说,volatile目标值或许会改动,即便没有任何代码去改动它。在这一点上,最典型的比方便是内存映射的设备寄存器和多线程中的同享目标。懂得运用volatile也是一门小小的艺术。运用volatile束缚符能够阻挠编译器对代码过火优化避免呈现一些你意想不到的状况,达不到预期的成果;过频地运用volatile很或许会添加代码尺度和下降功用。下面举个比方来阐明volatile在优化中的奇妙效果。
1.阻挠编译器优化
ARM Evaluator-7T模仿单机板运用根据内存映射的设备寄存器叫特别寄存器,用来
操控和交互外围设备。CPU对内存的操作能够做到按位进行,而特别寄存器是4字节对齐并占四个字节。你能够象unsigned int变量相同操作特别寄存器(有些人或许更喜爱uint32_t,以为这样表现寄存器占用4个字节的特色。uint32_t在C99 头文件中有界说)。而这儿,为了表现寄存器自身作为寄存器的含义而非它的物理含义的,咱们做如下界说:
typedef uint32_t special_register;
Evaluator-7T板子上有一个按钮(能够以为是外设之一)。按下该按钮能够对IOPDATA寄存器第8方位1,相反,开释按钮会将该位从头清0。咱们运用枚举办法为IOPDATA寄存器的第8方位界说一个掩码mask:
enum { button = 0x100 };
IOPDATA寄存器对应的地址为0x3FF5008,咱们能够用宏形象地界说IOPDATA:
#define IOPDATA (*(special_register *)0x03FF5008)
有了这个界说,咱们履行下面的循环就能够使CPU一向等候该按钮被按下:
while ((IOPDATA button) == 0)
;
可是这个希望有必要建立在编译器不对代码进行优化的条件假定之上。假如编译器优化这段代码,那么它会以为在这个循环中没有什么会改动 IOPDATA而且以为条件判别成果总是真或假,终究优化的成果是只对(IOPDATA button)==0判别一次,之后的循环都不在对其进行判别,其等同于:
if ((IOPDATA button) == 0)
for (;;)
;
明显,假如条件判别成果为真(那么之后都会以为是真),那么这段代码将会堕入死循环。假如判别为假,那么循环就此结束。能够看出,优化的代码功率更高,由于每次循环比较本来的履行时间要短。不幸的是,这段优化代码使得它底子就不能呼应按钮的每次动作。那么,怎样处理这个问题呢?处理的要害便是不要让编译器优化这段代码,运用volatile就能够办到这一点。咱们修正前面关于IOPDATA的宏界说:
#define IOPDATA (*(special_register volatile *)0x03FF5008)
这个界说将IOPDATA 界说为volatile类型的寄存器。volatile隐含地告知编译器特别寄存器或许会改动内容,即便没有任何显式地代码去改动它的内容。这样一来,编译器就不对IOPDATA作优化,而是每次都去拜访IOPDATA,这其实正是咱们所希望的。
2.无意中下降了功率
有时候,假如不注意的话,运用volatile会无意中下降代码功率。举个比方。Evaluator-7T有一个七段数码显现器见下图:
在IOPDATA 寄存器中第10到16位用来操控显现器的每一段。比方第10位便是用来操控顶部的那段显现,置1则点亮它,清0则平息它。咱们能够界说一个掩码mask来掩盖从第10到16的一切位:
enum { display = 0x1FC00 };
假定变量b用来操控这7段显现器的每一段显现,而且b的值现已你想要设置值(预备用来显现哪几段和平息哪几段,其它无关的位均为0)。那么你想要改动设置新的显现办法的操作便是:
IOPDATA = b;
可是这种赋值或许会改动第10到16位之外的其它位,这是咱们不希望的。所以,选用下面的办法更好:
IOPDATA |= b
可是,运用 |= 并不能平息那些现已点亮的显现段(1 | 0 -> 1),所以咱们能够用下面的函数到达意图:
void display_put(uint32_t b)
{
IOPDATA = ~display;
IOPDATA |= b;
}
不过,或许没想到的是这样的操作在无意中下降了代码功率。由于咱们界说IOPDATA为
volatile类型,它阻挠了编译器对代码的优化,要求任何读写IOPDATA的操作都死死板板地进行。IOPDATA = ~display的等价表现为IOPDATA = IOPDATA ~display,也便是先从IOPDATA读出内容然后与上~display,最后又回写IOPDATA。同理,IOPDATA |=b也有类似的进程。整个进程别离有2次读IOPDATA和2次写IOPDATA的操作。假如IOPDATA不运用volatile,那么编译器会要求将IOPDATA ~display的成果放在CPU寄存器中,直到完结IOPDATA |= b操作才写回特别寄存器IOPDATA。明显后者较之前者别离省掉了1次读IOPDATA和1次I写OPDATA的耗时操作(外设操作是最耗时的),功率要高许多。假如你想运用volatile但又能使能优化功用,你能够将函数作如下的修正:
void display_put(uint32_t b)
{
register uint32_t temp = IOPDATA;
temp = ~display;
temp |= b;
IOPDATA = temp;
}
这样做有点烦琐,下面的等效办法更简略:
void display_put(uint32_t b)
{
IOPDATA = (IOPDATA ~display) | b;
}
定论:从该比方看出,它并不鼓舞运用volatile,即便要用也要很当心,由于volatile或许在无意中下降了代码功率,而你却无法发觉。可是,咱们说,不鼓舞并不是说就不能或不要用,而是要懂得何时用,怎样用好它。其所谓智用了。