• C/C++ 编程中的内存屏障(Memory Barriers) (1)


    明天就要transfor去做检索引擎了,今天闲下来了,更新一下博客哈。之前 @高V 同学对本人之前《代码技巧及优化(c/c++)》的文章第六条,有关cache命中和cpu流水优化比较感兴趣,也提出了一些他的看法,今天,我就细化的说一下某些编程的点 -- 内存屏障,以及内存屏障对代码的影响。

           OK,首先来说一下什么是"内存屏障",可以先看一下官方式的说法 http://www.kernel.org/doc/Documentation/memory-barriers.txt内存屏障其实就是因为编译器优化和CPU对寄存器和cache的使用,导致对内存的操作不能够及时的反映出来,比如cpu写入后,读出来的值可能是旧的内容。举个例子,对一个变量赋值然后读出它的值这一看似“原子”的操作,因为内存是没有ALU计算单元的,所以内存没有计算的能力。而CPU一般情况下是不直接读写内存的(emmintrin.h应用例外),所以这一个过程可以看作(编译器优化后):

           读取内存数据到cache -->  CPU读取cache/寄存器  -->  CPU的计算  -->  将结果写入cache/寄存器  -->  写回数据到内存

    有人可能会问,为什么要这么麻烦,因为是编译器优化和CPU优化的结果,内存的时延比CPU高的多,是10ns级别,所以会通过读写寄存器或拆cache优化。这有可能导致一个问题,cache中的数据和内存实际的数据不一致,当多线程情况下,有可能会读"脏数据",或者带来线程执行结果不一致。这就是所谓的“内存屏障”(但不仅限于此case)。

            网上有几篇文章提到的《独辟蹊径品内核》中的代码,本人写了一个近似版本如下:

    1. include <stdlib.h>  
    2. #include <stdio.h>   
    3. #include <pthread.h>   
    4. #include <unistd.h>   
    5.   
    6. // global variable   
    7. int flag=1;  
    8.   
    9. void* wait(void *context){  
    10.     while (flag) {  
    11.         printf("continue\n");  
    12.         sleep(1);  
    13.     }     
    14. }  
    15.   
    16. void wakeup(){  
    17.     flag=0;  
    18. }  
    19.   
    20. int main(int argc, char *argv[])  
    21. {  
    22.     pthread_t pid = 0;  
    23.     pthread_create(&pid,NULL,wait,NULL);  
    24.     sleep(3);  
    25.     wakeup();  
    26.   
    27.     printf("Done\n");  
    28.     getchar();  
    29. }  

           按照书中的说法,flag是被其他线程意外修改的话。while循环会被编译器优化,编译器在发现wait函数里并没有修改flag,所以就会对flag进行cache,放在eax寄存器中,内存中的flag实体如果被修改,eax寄存器不会感知。自己试了一下,在没有优化和Gcc O2的情况下程序正常跳出了循环,可能是因为cpu或者linux新版内核或者gcc的新编译特性所致(如果哪位同学了解,可以留言哈)。之后会跟进这个问题,如果有发现,我会贴出来哈。

           其实,通过gcc -S看到汇编过程的中间汇编代码也可以看出写东西:

    1. wait:  
    2. .LFB46:  
    3.     .cfi_startproc  
    4.     subq    $8, %rsp  
    5.     .cfi_def_cfa_offset 16  
    6.     movl    flag(%rip), %edx  
    7.     testl   %edx, %edx  
    8.     je  .L4   
    9.     .p2align 4,,10  
    10.     .p2align 3  
    11. .L5:  
    12.     movl    $.LC0, %edi  
    13.     call    puts  
    14.     movl    $1, %edi  
    15.     call    sleep  
    16. <SPAN style="COLOR: #ff6600">    movl    flag(%rip), %eax  
    17.     testl   %eax, %eax  
    18.     jne .L5 </SPAN>  
    19. .L4:  
    20.     addq    $8, %rsp  
    21.     .cfi_def_cfa_offset 8  
    22.     ret   
    23.     .cfi_endproc  
    24. .LFE46:  
    25.     .size   wait, .-wait  
    26.     .p2align 4,,15  
    27.     .globl  wakeup  
    28.     .type   wakeup, @function  

       

          (我不是汇编大牛,错了别炮轰哈),其中标红的语句16~18行可以看出,循环只是检测eax寄存器是不是0(不太熟悉汇编的朋友可能会为什么test %eax,%eax,这只是一个优化因为与操作比cmp要快),可以看出,确实是在不断的读寄存器而不是内存。

           到此,大家对“内存屏障”估计也有了一个初步的认识,内存屏障主要分三类:编译器优化(如上case) / 缓存优化 / CPU乱序执行(后面文章会提到)

           大家可能会问,那岂不这样会造成很多问题。其实不一定,据本人的知识范围,大部分内存屏障导致的问题都出现在内核态,用户态需要注意的方面不多。而且用户也有相应的解决方案,最简单的就是锁机制,还有volatile关键字,可以把可能出现的cache读脏的数据volatile int tmp = 0;这样每次操作都会从内存获取。

           之后的文章会深入一下“内存屏障”和CPU乱序的问题。缓存优化导致的内存屏障,基本在新的硬件上得到了比较好的解决,而且在用户态下基本感知不到。只要大家合适的用好多线程的锁机制和volatile的正确运用即可。

  • 相关阅读:
    生命中的另一口井
    sqlldr使用小记
    字节单位介绍
    《Java虚拟机》随笔记01
    Python生成器实现杨辉三角打印
    什么是递归?用十进制转二进制的Python函数示例说明
    Python的filter与map内置函数
    Python内置函数property()使用实例
    Python装饰器的理解
    Python迭代与递归方法实现斐波拉契数列
  • 原文地址:https://www.cnblogs.com/wangfengju/p/6173134.html
Copyright © 2020-2023  润新知