zoukankan      html  css  js  c++  java
  • falsesharing原理浅析和测试

    绪论

    SMP(对称多处理)架构简单的说就是多个CPU核,共享同一个内存和总线。L1 cache也叫芯片缓存,一般是CPU Core私有的,即每个CPU核一个,L2 cache可能是私有的也可能是部分共享的,L3 cache则多数是共享的。false-sharing是在SMP的架构下常见的问题。 

    false-sharing产生背景及原因

    CPU利用cache和内存之间交换数据的最小粒度不是字节,而是称为cache line的一块固定大小的区域,缓存行是内存交换的实际单位。缓存行是2的整数幂个连续字节,一般为32-256个字节,最常见的缓存行大小是64个字节。

    在写多线程代码时,为了避免使用锁,通常会采用这样的数据结构:根据线程的数目,安排一个数组, 每个线程一个项,互相不冲突。从逻辑上看这样的设计无懈可击,但是实践的过程可能会发现有些场景下非但没提高执行速度,反而会性能很差,而且年轻司机通常很难定位问题所在。

    问题在于cpu的cache line,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享,即false-sharing

    实际案例

    在多处理器,多线程情况下,如果两个线程分别运行在不同的CPU上,而其中某个线程修改了cache line中的元素,由于cache一致性的原因,另一个线程的cache line被宣告无效,在下一次访问时会出现一次cache line miss,大量的cache line miss会导致性能的显著下降。究其原因,cache line miss是由于两个线程的Cache line有重合(非共享的变量实际上却共享的使用了同一个cacheline,导致竞争)引起的。

    如在Intel Core 2 Duo处理器平台上,L2 cache是由两个core共享的,而L1 data cache是分开的,由两个core分别存取。cache line的大小是64 Bytes。假设有个全局共享结构体变量f由2个线程A和B共享读写,该结构体一共8个字节同时位于同一条cache line上。

    struct foo {
      int x;
      int y;
    };

    若此时两个线程一个读取f.x另一个读取f.y,即便两个线程的执行是在独立的cpu core上的,实际上结构体对象f被分別读入到两个CPUs的cache line中且该cache line 处于shared状态。此时在核心1上运行的线程A想更新变量X,同时核心2上的线程B想要更新变量Y。

    如果核心1上线程A优先获得了所有权,线程A修改f.x会使该CPU core 1 上的这条cache line将变为modified状态,另一个CPU core 2上对应的cache line将变成invalid状态;此时若线程B马上读取f.y,为了确保cache一致性,B所在CPU核上的相应cache line的数据必须被更新;当核心2上线程B优先获得了所有权然后执行更新操作,核心1就要使自己对应的缓存行失效。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,若读写的次数频繁,将增大cache miss的次数,严重影响系统性能。

    虽然在memory的角度这两种的访问时隔离的,但是由于错误的紧凑地放在了一起,是的两个变量处于同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。可见,false sharing会导致多核处理器上对于缓存行cache line的写竞争,造成严重的系统性能下降,有人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。

    false-sharing避免方法

    把每个项凑齐cache line的长度,即可实现隔离,虽然这不可避免的会浪费一些内存。

    1. 对于共享数组而言,增大数组元素的间隔使得由不同线程存取的数组元素位于不同的cache line上,使一个核上的Cache line修改不会影响其它核;或者在每个线程中创建全局数组的本地拷贝,然后执行结束后再写回全局数组,此方法比较粗暴不优雅。
    2. 对于共享结构体而言,使每个结构体成员变量按照Cache Line大小(一般64B)对齐。可能需要使用#pragma宏。

    注意事项

    单线程或单核多线程都不存在这个问题,因为只有一个CPU核也即只有一个L1 Cache,不存在缓存一致性的问题。

    示例程序

    注意程序中的LEVEL1_DCACHE_LINESIZE宏来自g++编译命令传入的,使用Shell命令getconf LEVEL1_DCACHE_LINESIZE能获取cpu cache line的大小。(有关getconf命令的使用可以自行google)

    #include <stdio.h>
    #include <sys/time.h>
    #include <time.h>
    #include <pthread.h>
    #define  PACK  __attribute__  ((packed))
    typedef int cache_line_int __attribute__((aligned(LEVEL1_DCACHE_LINESIZE)));
    
    #ifdef FS
    struct data
    {
        cache_line_int a;
        cache_line_int b;
    };
    #endif
    #ifdef NONFS
    struct data
    {
        int a;
        int b;
    };
    #endif
    
    #define MAX_NUM 500000000
    
    void* thread_func_1(void* param)
    {
        timeval start, end;
        gettimeofday(&start, NULL);
        data* d = (data*)param;
        for (int i=0; i<MAX_NUM; ++i)
        {
            ++d->a;
        }
        gettimeofday(&end, NULL);
        printf("thread 1, time=%d\n", (int)(end.tv_sec-start.tv_sec)*1000000+(int)(end.tv_usec-start.tv_usec));
        return NULL;
    }
    
    void* thread_func_2(void* param)
    {
        timeval start, end;
        gettimeofday(&start, NULL);
        data* d = (data*)param;
        for (int i=0; i<MAX_NUM; ++i)
        {
            ++d->b;
        }
        gettimeofday(&end, NULL);
        printf("thread 2, time=%d\n", (int)(end.tv_sec-start.tv_sec)*1000000+(int)(end.tv_usec-start.tv_usec));
        return NULL;
    }
    
    int main()
    {
        data d = {a:0, b:0};
        printf("sizeof(data) : %d\n", sizeof(data));
        pthread_t t1, t2;
        pthread_create(&t1, NULL, thread_func_1, &d);
        pthread_create(&t2, NULL, thread_func_2, &d);
        pthread_join(t1, NULL);
        pthread_join(t2, NULL);
        printf("end, a=%d,b=%d\n", d.a, d.b);
        return 0;
    }

    编译、运行可以看到结果对比:

    /*编译指令*/
    g++ -o 1 1.cpp -g -Wall -lpthread -DLEVEL1_DCACHE_LINESIZE=`getconf LEVEL1_DCACHE_LINESIZE` -DFS
    g++ -o 1 1.cpp -g -Wall -lpthread -DLEVEL1_DCACHE_LINESIZE=`getconf LEVEL1_DCACHE_LINESIZE` -DNONFS
     
    /*输出结果:*/
    thread 1, time=1607430
    thread 2, time=1629508

    我的腾讯云主机只有一个CPU核,所以运行的结果并没有差异,但是在多核CPU上执行大约相差2~3倍。

    注:本文整理自多篇文章,参考文章列表后续补充。

  • 相关阅读:
    Windows Phone 8 Wallet 手机钱包 / 电子钱包
    Windows Phone 8 In app purchase 应用内购买 / 应用内支付
    Windows Phone 8 适应多屏分辨率
    Windows phone 8 基于定位的后台应用
    Windows Phone 8 Nokia地图控件
    Windows Phone 8 MDIL编译与代码混淆工具
    Windows Phone 8 近场通信 NFC / Bluetooth Proximity
    Windows Phone 8 镜头应用 Lenses for Windows Phone 8
    Windows Phone 8 与 windows 8 开发技术概览
    嵌入式成长轨迹54 【Zigbee项目】【CC2430基础实验】【系统睡眠工作状态】
  • 原文地址:https://www.cnblogs.com/blastbao/p/8290332.html
Copyright © 2011-2022 走看看