神奇的C语言十八:volatile的应用



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次。编译器很自豪,但是却让程序出错了。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值