volatile是一个不常用的关键字,但是我们最好了解这个关键字的来龙去脉,以防止自己出错。
volatile用于修饰一个变量,以告诉编译器:每次存取此变量时,都要按其地址来操作,而不要进行优化。volatile正是为了防止对某个变量的存取优化;如果没有优化,volatile关键字是没有作用的。
下面是三个编译器优化的例子,解释了为什么需要volatile关键字。
例子1
多线程程序中被多个线程存取的全局变量应该考虑添加volatile关键字。
代码:
int finished = 0;
void Thread1(void)
{
while (!finished)
{
work();
}
}
void Thread2(void)
{
while (!something_happened())
{
sleep_for_a_while();
}
finished = 1;
}
上面的代码中的两个函数是两个并发执行的线程,并且work函数没有修改finished变量。
实际运行我们会发现Thread1很可能永远不会结束。为什么会这样呢?
您肯定知道,访存开销远远大于访问寄存器,编译器也知道。在编译优化Thread1时,编译器看到循环中并没有修改finished变量的指针,所以编译器优化后的代码可能是这样子的:
void Thread1(void)
{
if (!finished)
{
while (1)
{
work();
}
}
}
优化后访存只发生了一次,比之前每次循环都要访存,其效率增加了很多!如果这是单线程应用,那么此优化是我们希望看到的。但是对于此例来说,此优化导致了错误的发生。
此时,我们将finished标记为volatile关键字即可阻止优化、避免错误。
例子2
系统程序或嵌入式程序中会编写一些ISR,即中断服务程序。还是上面的例子,如果Thread1作为系统代码,Thread2作为ISR,那么Thread1还是存在同样的问题。
例子3
这个例子没有经过我的验证,如果和您的知识有冲突,或者发现有问题,请留言告诉我,非常感谢!
端口有两种寻址方式,其一便是内存映射。对于内存映射端口寻址,如果我们多次从一个端口读数据,从编译器的角度来看就是多次从一个内存中读取数据。如果多次读取数据的过程中没有发生向那个内存单元的写入操作,那么编译器就有理由认为每次读取的内容都是相同的。
对于从一般内存读取,的确如此;但是对于从端口读取,您应该也知道大部分情况下不是这样的。
例如:
void GetStates(void)
{
char *p = (char *)0x88;
char a = *p;
char b = *p;
char c = *p;
}
假设0x88是某个端口的内存映射地址,那么每次读取的内容就不太可能一样,例如每次读取的内容分别是 传输方向 传输总字节数 是否完成标志。
但是编译器怎么看呢?编译器认为上述源码访存次数为8次,并且可以优化。优化后的内容可能如下:
void GetStates(void)
{
register char eax = *(char *)0x88;
char a = eax;
char b = eax;
char c = eax;
}
上面的伪代码的意思是,先将0x88内存储的内容读取到eax中,然后直接从eax中对a/b/c赋值。访存减少到了4次。编译器很自豪,但是却让程序出错了。