补遗篇之volatile
C中volatile关键字在程序操作变量时,强制读写变量所在内存,以阻止编译器对某些特殊变量的错误优化。反过来,只有靠程序员用volatile过滤一些特殊情况后,编译器才能大胆优化。volatile作用可总结为:阻止三种情形下的两种编译器优化。
两种编译器优化
a. 数据流分析优化:编译器分析程序中变量在哪里赋值、哪里使用、哪里失效,根据分析结果消除多余的变量读取和赋值步骤,如:
int a = 10;
......//其他代码,里面没有对a的读操作
a = 20;
开启了优化选项的编译器能够根据a赋值和使用情况,推断a=10无效,直接忽略这条语句。
b. 寄存器缓存技术:把频繁访问的变量缓存到某寄存器,之后就通过此寄存器访问该变量,而不再通过内存总线。由于寄存器比内存快很多,这种智能优化可显著提高性能。
三种不能优化的情形
某些变量会在可见代码外而不是被程序本身赋值改变,这时编译器根据显式代码采取的上面两种优化可能导致错误结果,这些特殊变量主要出现在以下三种情况:
a. MMIO(Memory Mapped IO)操作,某些CPU把外设I/O端口映射到内存空间统一编址,之后就象访问普通内存那样访问MMIO,不需要专门I/O指令。MMIO内存的值对应于IO寄存器,会随外部信号而改变,不完全依赖于代码里的显式内存读写操作,很明显这类内存就不能用上文两种技术优化。这种volatile典型应用多出现在嵌入式驱动程序中。
b. 变量被代码内的内嵌汇编改变,而编译器无法知道内嵌汇编里变量的改变。
c. 被中断服务子程序访问到的,或者在多线程应用中被几个任务共享的全局变量。
这三种情况,必须用volatile阻止编译器“想当然”的优化,下面举例说明:
例1:
volatile int *p = get_io_addr();
int a, b;
a = *p;
......//其他代码,里面没有对p的操作
b = *p;
p是指向MMIO的指针,例1中两次读取信号,赋给a和b。如果p不声明为volatile,编译器会”自作聪明”认为两次*p值一样(普通内存的确如此,因为中间没赋值),b=*p时无需通过p指针读取真实外设IO值,可用a=*p时保存在某寄存器的值代替。但外界信号可能随时变化,一旦在a=*p和b=*p间变化,这种用寄存器代替内存的优化就会出错。
volatile同样也用于阻止MMIO写操作的优化,如果给变量赋值但后面没使用,编译器一般会忽略这次赋值操作,但MMIO赋值不同,因为CPU通过MMIO设到硬件寄存器的数据,总有意义(如驱动LED/马达等),必须用volatile以强制执行此类MMIO写操作。
volatile int *p = set_io_addr(); //对外输出控制信号的IO寄存器映射的内存地址
int j; //普通变量
*p = 1; //不被优化 i=1
*p = 3; //不被优化 i=3
j = 1; //被优化掉
j = 3; //j = 3
例2:
void main()
{
int i=10;
int a = i;
printf("i1= %d\n",a);
__asm { mov dword ptr [ebp-4], 20h } //改变内存中i的值为20h即32,而优化器并不知道
int b = i;
printf("i2= %d\n",b);
}
以上代码在VC debug模式运行,输出i1=10,i2=32。release重生之大文豪模式输出:i1 = 10,i2 = 10。因为release模式下编译器默认开启优化,在b=i时取之前a=i时读到寄存器缓存值,而内嵌汇编操作不被编译器注意。如果定义volatile int i=10,其他不变,debug和release都输出:i1=10,i2=32。这说明volatile能阻止release模式下编译器“自以为是”的优化。
例3:
多任务/中断环境下,某线程中的变量可能被其他线程或中断改变,而编译器无从获知这种改变,于是导致错误优化,如:
int i=0;
void main(void)
{...
while (1)
{
if (i) do_xxx();
}
}
void ISR_XX(void) /* Interrupt service routine. */
{
i=1;
}
程序本意希望中断发生时,main调用do_xxx函数,但编译器不知道i会被ISR_XX修改,它通过本地代码判断main函数里i从没修改,因此只执行一次从内存i到寄存器的读操作,之后每次if判断都用寄存器里i的“副本”,而副本值永远是初始值0,即使i在ISR_XX里被改为1,do_xxx也不会被调用。强制从内存读取i,就要定义volatile int i;。
不过即使用volatile避免了错误优化,也不能像上例那样用volatile全局变量去同步线程。因为volatile变量不满足原子性和顺序性,除非加锁保护,而加锁就不需要volatile了:锁可以保证临界区串行,也可以实现内存屏障(barrier),保证临界区内的全局变量为最新值而不是寄存器缓存,和volatile作用相同。关于volatile和线程同步超出范围,不详述。