补遗篇之volatile

  Cvolatile关键字在程序操作变量时,强制读写变量所在内存,以阻止编译器对某些特殊变量的错误优化。反过来,只有靠程序员用volatile过滤一些特殊情况后,编译器才能大胆优化。volatile作用可总结为:阻止三种情形下的两种编译器优化

两种编译器优化

    a. 数据流分析优化:编译器分析程序中变量在哪里赋值、哪里使用、哪里失效,根据分析结果消除多余的变量读取和赋值步骤,如:

    int a = 10;

    ......//其他代码,里面没有对a的读操作

    a = 20;

    开启了优化选项的编译器能够根据a赋值和使用情况,推断a=10无效,直接忽略这条语句。

    b. 寄存器缓存技术:把频繁访问的变量缓存到某寄存器,之后就通过此寄存器访问该变量,而不再通过内存总线。由于寄存器比内存快很多,这种智能优化可显著提高性能。

三种不能优化的情形

    某些变量会在可见代码外而不是被程序本身赋值改变,这时编译器根据显式代码采取的上面两种优化可能导致错误结果,这些特殊变量主要出现在以下三种情况:

    a. MMIOMemory 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中两次读取信号,赋给ab。如果p不声明为volatile,编译器会自作聪明认为两次*p值一样(普通内存的确如此,因为中间没赋值)b=*p时无需通过p指针读取真实外设IO值,可用a=*p时保存在某寄存器的值代替。但外界信号可能随时变化,一旦在a=*pb=*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 ",a);

      __asm {   mov dword ptr [ebp-4], 20h  }   //改变内存中i的值为20h32,而优化器并不知道   

      int b = i;

      printf("i2= %d ",b);

    }

    以上代码在VC debug模式运行,输出i1=10i2=32release重生之大文豪模式输出:i1 = 10i2 = 10。因为release模式下编译器默认开启优化,在b=i时取之前a=i时读到寄存器缓存值,而内嵌汇编操作不被编译器注意。如果定义volatile int i=10,其他不变,debugrelease都输出:i1=10i2=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,即使iISR_XX里被改为1do_xxx也不会被调用。强制从内存读取i,就要定义volatile int i;

    不过即使用volatile避免了错误优化,也不能像上例那样用volatile全局变量去同步线程。因为volatile变量不满足原子性和顺序性,除非加锁保护,而加锁就不需要volatile了:锁可以保证临界区串行,也可以实现内存屏障(barrier),保证临界区内的全局变量为最新值而不是寄存器缓存,和volatile作用相同。关于volatile和线程同步超出范围,不详述。

原文地址:https://www.cnblogs.com/jiangye/p/3496435.html