volatile关键字及编译器指令乱序总结

本文简单介绍volatile关键字的使用,进而引出编译期间内存乱序的问题,并介绍了有效防止编译器内存乱序所带来的问题的解决方法,文中简单提了下CPU指令乱序的现象,但并无深刻讨论。     程序员

如下是我搭建的博客地址: http://itblogs.ga/blog/20150329150706/    欢迎到这里阅读文章。编程

volatile关键字

volatile关键字用来修饰一个变量,提示编译器这个变量的值随时会改变。一般会在多线程、信号处理、中断处理、读取硬件寄存器等场合使用。数组

程序在执行时,一般将数据(变量的值)从内存的读到寄存器中,而后进行运算,此后对该变量的处理,都是直接访问寄存器就能够了,再也不访问内存,由于 访存的代价是很高的(这块是访问寄存器仍是从新访存加载到寄存器是编译器在编译阶段就决定了的)。但在上述说的几种状况下,内存会被另外一个线程或者信号处 理函数、中断处理函数、硬件改掉,这样,代码只访问寄存器的话,永远得不到真实的值。缓存

   

对这样的变量(会在多线程、线程与信号、线程与中断处理中共同访问的,或者硬件寄存器),在定义时都会加上volatile关键字修饰。这样编译器 在编译时,编译出的指令会从新访存,这样就能保证拿到正确的数据了。但这里须要注意的是,编译器只能作到让指令从新访问内存,而不是直接使用寄存器中的 值,这些和缓存没有关系,具体执行时指令是访问内存仍是访问的缓存,编译器也没法干预。多线程

   

另外,除了使用寄存器来避免屡次访存外,编译器有时可能直接将变量所有优化掉,使用常数代替。好比:架构

int main()
{
    int a = 1;
    int b = 2;

    printf("a = %d, b = %d \n", a, b);
}函数

   

编译器可能直接优化为:      性能

int main()
{
    printf("a = %d, b = %d \n", 1, 2);
}优化

   

  若是对ab的声明加了 volatile关键字,编译器将不在作这样的优化。线程

             

还有,对全部volatile变量,编译器在编译阶段保证不会将访问volatile变量的指令进行乱序重排。

    

   

  指令乱序

那么什么是指令乱序,指令乱序是为了提升性能,而致使的执行时的指令顺序和代码写的顺序不一致。指令乱序有编译期间指令乱序和执行时指令乱序。

执行时指令乱序是CPU的一个特性,这块比较复杂,再也不这里说起。咱们只须要知道在x86/x64的体系架构下,程序员通常不须要关注执行时指令乱序(不须要关注不表明没有)。

编译期间指令乱序是指在编译成二进制代码时,编译器为了所谓的优化进行了指令重排,致使二进制指令的顺序和咱们写的代码的顺序是不一致的。

好比如下代码:

int a;
int b;

int main()
{
    a = b + 1;
    b = 0;
}

会被优化成(实际上在汇编阶段进行的乱序优化,优化后的代码也只能以汇编的方式查看,这里只是拿C代码举例说明一下):

int a;
int b;

int main()
{
    b = 0;
    a = b + 1;
}

对加上volatile关键字的变量的访问,编译器不会进行指令乱序的优化,保证volatile变量的访问顺序和代码写的是同样的。好比以下代码不会优化:

volatile int a;
volatile int b;

int main()
{
    a = b + 1;
    b = 0;
}

   

可是如下代码,依然会乱序,由于编译器只是保证volatile变量访问的顺序,对于非volatile变量之间,以及volatile以及非volatile变量之间的顺序,编译器仍是会优化。

int a;
volatile int b;

int main()
{
    a = b + 1;
    b = 0;
}

   

       

asm volatile ("" : : : "memory");

通常编程时若是使用到volatile关键字,那么基本上都须要考虑编译器指令乱序的问题。解决编译器指令乱序所带来的问题,除了上面将必要的变量声明为volatile,还可使用下面一条嵌入式汇编语句:

1 asm volatile ("" : : : "memory");

这是一条空汇编语句,只是告诉编译器,内存发生了变化。编译器遇到这条语句后,会生成访存更新寄存器的指令,将全部的寄存器的值更新一遍。这里是编译器遇到这条语句额外生成了一些代码,而不是CPU遇到这条语句执行了一些处理,由于这条语句自己并无CPU指令与之对应。

因为编译器知道这条语句以后内存发生了变化,编译器在编译时就会保证这条语句上下的指令不会乱,即这条语句上面的指令,不会乱序到语句下面,语句下面的指令不会乱序到语句上面。

利用编译器这个功能,程序员能够:

一、利用这条语句,强制程序访存,而不是使用寄存器中的值,做为使用volatile关键字的一个替代手段;

二、在不容许乱序的两个语句之间插入这条语句从而保证不会被编译器乱序。

   

下面看一个应用的例子,两个线程访问共享的全局变量:

#define ARRAY_LEN 12

volatile int flag = 0;
int a[ARRAY_LEN];

pthread1()
{
    a[ARRAY_LEN - 1] = 10; <br>    asm volatile ("" : : : "memory");
    flag = 1;
}

pthread2()
{
    int sum = 0;

    if(flag == 0) {
        sum += a[ARRAY_LEN - 1];
    }    
}线程2假定flag==1时,线程1已经将数据放到数组中了。但实际上,若是没有  asm volatile ("" : : : "memory"),线程1并不能保证flag = 1在数组赋值以后。缘由就是咱们前面提到的编译器指令乱序。

     

指令乱序是一个比较复杂的话题,咱们这里只考虑了编译器指令乱序,在intel架构的CPU上,基本上考虑到这些就足够了。但在弱指令序的CPU上,好比mips,了解这些还远远不够。本文不打算展开CPU指令乱序的话题,感兴趣的能够参考如下文章了解如下:

   
   

volatile关键字的使用

volatile关键字使用和const一致,下面是一个总结:

char const * pContent;       // *pContent是const,   pContent可变
(char *) const pContent;     //  pContent是const,  *pContent可变
char* const pContent;        //  pContent是const,  *pContent可变
char const* const pContent;  //  pContent 和       *pContent都是const

   

沿着*号划一条线,若是const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;若是const位于*的右侧,const就是修饰指针自己,即指针自己是常量。

   

   

参考资料

Memory Ordering at Compile Time

如下是我搭建的博客地址:原文连接:http://itblogs.ga/blog/20150329150706/ 转载请注明出处
相关文章
相关标签/搜索