false-sharing原理浅析和测试

绪论

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倍。

 

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

 

 

posted @ 2018-01-15 22:36  小僧尤里  阅读(2605)  评论(1编辑  收藏  举报