多线程false sharing带来的影响和一些优化.

最近在线项目中测试一个无锁队列的性能的时候发现,在一个线程push另一个线程pop整型数据的时候,吞吐量竟然和std::queue+spinlock类似甚至更差,这样完全体现不出lockfree的优势, 决定找找原因.

这个无锁队列是通过一个头指针来push数据,一个尾指针来pop数据来实现的.

template<typename T>
class LockFreeQueue {
    struct Node {
        T value;
        Node *next;
    }
    ...
    bool Push(const T & data) {
        //只操作tail成员
        ...
    }
    bool Pop(T &data) {
        //只操作head成员
        ...
    }
    ...
private:
    Node * head;
    Node * tail;
}

使用上一个线程只调用队列的push方法, 另一个线程调用队列的pop方法.
push方法只访问tail成员实现入队,pop方法只方法head成员实现出队.

注意到head和tail指针有极大的可能在同一个L1缓存行上的, 这就会造成false sharing,这里体现在push线程操作tail指针的时候将缓存行刷新掉了,cpu会通知读线程所在的cpu将该相同的缓存行也刷新掉,以达到多处理器指针的cache coherence, 读线程修改head指针的时候也会做相同的事情, 在系统高压的情况下这种访存方式会对性能造成很大的伤害,具体通过下面一个例子来看.

例子程序P1

#include <pthread.h>
#include <stdio.h>

#define ITERATIONS 1e9
int A;
int B;

static void * thread_func(void *){
        for (int i = 0; i < ITERATIONS; i++){
                if (A == 1) {
                        A = 0;
                } else {
                        A = 1;
                }
        }
}

int main() {
        pthread_t tid;
        pthread_create(&tid, NULL, thread_func, NULL);

        for (int i = 0; i < ITERATIONS; i++){
                if (B == 1) {
                        B = 0;
                } else {
                        B = 1;
                }
        }

        return 0;
}

这个简单程序展示了false sharing带来的性能损失,两个线程访问不同的地址,但是却在同一个缓存行上.这里通过linux下的perf工具来统计程序的运行指标。

>> perf stat -e instructions -e cache-references -e cache-misses -e L1-dcache-loads -e L1-dcache-load-misses -e L1-dcache-stores -e L1-dcache-store-misses -e LLC-loads -e LLC-load-misses -e LLC-prefetches -e cycles -e cs ./cacheline_unaligned

 Performance counter stats for './cacheline_unaligned':

    19,006,842,755 instructions              # 注意这行   0.97  insns per cycle         [36.43%]
        78,160,551 cache-references                                             [45.54%]
            24,661 cache-misses              #    0.032 % of all cache refs     [45.54%]
     7,990,941,413 L1-dcache-loads                                              [45.54%]
        78,220,759 L1-dcache-load-misses     #  注意这行  0.98% of all L1-dcache hits   [45.54%]
     4,009,177,234 L1-dcache-stores                                             [36.31%]
        69,339,246 L1-dcache-store-misses #注意这行                                   [36.54%] 
         8,499,780 LLC-loads                                                    [36.49%]
             5,684 LLC-load-misses           #    0.07% of all LL-cache hits    [36.45%]
        76,310,160 LLC-prefetches                                               [18.20%]
    19,623,169,444 cycles                    [27.27%]
               612 cs                                                          

       3.438158407 seconds time elapsed

对代码做一个简单的修改得到例子程序P2,对变量之间加以padding, 使得变量A和B存在于独立的缓存行上,这里机器的cacheline大小为64个字节, linux下可以通过getconf LEVEL1_DCACHE_LINESIZE来得到这个大小.

#include <pthread.h>
#include <stdio.h>

#define ITERATIONS 1e9
int A;
int32_t __padding__[16]; //padding, 使A,B独立存在一个缓存行上
int B;
static void * thread_func(void *){
        for (int i = 0; i < ITERATIONS; i++){
                if (A == 1) {
                        A = 0;
                } else {
                        A = 1;
                }
        }
}

int main() {
        pthread_t tid;
        pthread_create(&tid, NULL, thread_func, NULL);

        for (int i = 0; i < ITERATIONS; i++){
                if (B == 1) {
                        B = 0;
                } else {
                        B = 1;
                }
        }

        return 0;
}

接着看修改后的结果.

>> perf stat -e instructions -e cache-references -e cache-misses -e L1-dcache-loads -e L1-dcache-load-misses -e L1-dcache-stores -e L1-dcache-store-misses -e LLC-loads -e LLC-load-misses -e LLC-prefetches -e cycles -e cs ./cacheline_aligned 

 Performance counter stats for './cacheline_aligned':

    18,128,002,993 instructions              # 注意这行   1.58  insns per cycle         [36.49%]
           180,503 cache-references                                             [45.61%]
            17,691 cache-misses              #    9.801 % of all cache refs     [45.61%]
     7,640,076,865 L1-dcache-loads                                              [45.61%]
           208,122 L1-dcache-load-misses     # 注意这行   0.00% of all L1-dcache hits   [45.61%]
     3,841,727,742 L1-dcache-stores                                             [36.27%]
            16,237 L1-dcache-store-misses   # 注意这行                                 [36.65%]
            53,850 LLC-loads                                                    [36.58%]
             3,879 LLC-load-misses           #    7.20% of all LL-cache hits    [36.51%]
             4,254 LLC-prefetches                                               [18.22%]
    11,479,745,606 cycles                    [27.27%]
               379 cs                                                          

       2.030437774 seconds time elapsed

看到修改后L1-dcache-store-misses数量约为未修改版本的0.26%, ipc指标从0.97提升到了1.58. 这带来的好处是相当的明显的,因为L1缓存的miss使得cpu流水线的暂停去访问下一级缓存获取数据,减少了ipc(instructions per cycle)这个重要的指标.

上述的无锁队列的情况恰好和这种访存方式相同, 所以很自然可以做这样的一个优化.

template<typename T>
class LockFreeQueue {
    struct Node {
        T value;
        Node *next;
    }
    ...
    bool Push(const T & data) {
        //只操作tail成员
        ...
    }
    bool Pop(T &data) {
        //只操作head成员
        ...
    }
    ...
private:
    Node * head;
    char __padding__[CACHELINE_SIZE - sizeof(Node *)];
    Node * tail;
}

经过测试之后, 相比之前的队列实现,效率约有5~60%的提升, 还是比较满意的!

通过这个简单的例子可以得出在多线程环境下要尽量避免Flase sharing的发生,我自己总结了方法.即将一个线程独享的变量通过padding放置在同一个缓存行上.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值