• 补遗篇之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和线程同步超出范围,不详述。

  • 相关阅读:
    0806 c#总复习
    0804 递归
    0808 html基础
    0803结构体,枚举类型
    0801out传值
    0731函数
    0730特殊集合
    0728多维数组,ArrayList集合
    js 获取url链接的任意参数
    jq dom操作
  • 原文地址:https://www.cnblogs.com/jiangye/p/3496435.html
Copyright © 2020-2023  润新知