走进C++11(四十三)memory order 番外篇一 为什么实现同样逻辑,别人的程序比我快 - 从SPSC queue谈起

关注公众号获取更多信息:

 

最近也写了很多关于内存模型的文章,会有人有灵魂一问 -- 内存模型到底有啥用?什么时候能用到内存模型?这个问题我也思考了很久,接下来我会举个在现实中的应用 -- SPSC queue。

 

有些人会说,SPSC queue?我分分钟就能给你写出一个。很简单,就是普通的queue加上锁就行了。但是既然我们都说过了内存模型,我们就要忘记锁这个东西。的确,锁是解决很多线程问题的”万金油“,可是有没有想过,锁的应用,给你的系统带来了多大的额外开销。之前曾经维护过公司的一个软件,这个软件就在worker thread和逻辑thread之间共享了一个SPSC queue -- 就是那种你想象中的,push pop都要加锁的那种。也不可避免的,对这个queue的操作成了系统的瓶颈。

 

闲话少说,书归正传。要想实现一个SPSC queue,最关键的两个API就是enqueue和dequeue。面临的最大问题就是确保两个API是thread safe的。还有一个重要的问题就是 --False sharing。这个聊起来就会没完没了。为了讲明白来龙去脉,这个文章可能不止一篇。

 

在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件底层相关的影响因素,由难到易, 我们会首先从硬件系统层面上说起。

 

这里首先要科普几个概念:

1. cache line

2. False sharing

3. padding

 

一. cache line

CPU不是按单个bytes来读取内存数据的,而是以“块数据”的形式,每块的大小通常为64bytes,这些“块”被称为“Cache Line”。举个例子,一个 long 是 8 个 byte,那么 8 个元素的 long[8] 数组会在同一个 cache line 中被一次读入到 CPU 的 cache 中。多线程处理时,同一个 long[8] 会被分别读到每个 CPU 自己的 cache 里面(为了简化概念,不考虑存在共享 cache 的情况)。那么某个 CPU 改变了其中的某一个元素的值时,整个 cache line 的数据都被污染了。由于多个 CPU 在 cache line 层面共享这个数组,因此需要将这个 cache line 的数据都写回内存;然后其他 CPU 要对这个数组的其他元素进行操作,又要重新将这个数组的全部内容加载进自己的 cache 中。如此不断在 累加一个元素 -> 写回内存 -> 其他 cpu cache 失效 -> 读取内存 循环。相当于所有 CPU 都在竞争这一小块内存的使用,由于大量的数据要在内存和 CPU 缓存间不断传输,比单线程串行处理还糟糕。

Image

 

二. False sharing

有多个线程操作不同的成员变量,但是相同的缓存行,这个时候会发生什么?。没错,伪共享(False Sharing)问题就发生了!有张 Disruptor 项目的经典示例图,如下:

 

Image

 

上图中,一个运行在处理器 core1上的线程想要更新变量 X 的值,同时另外一个运行在处理器 core2 上的线程想要更新变量 Y 的值。但是,这两个频繁改动的变量都处于同一条缓存行。两个线程就会轮番发送 RFO 消息,占得此缓存行的拥有权。当 core1 取得了拥有权开始更新 X,则 core2 对应的缓存行需要设为 I 状态。当 core2 取得了拥有权开始更新 Y,则 core1 对应的缓存行需要设为 I 状态(失效态)。轮番夺取拥有权不但带来大量的 RFO 消息,而且如果某个线程需要读此行数据时,L1 和 L2 缓存上都是失效数据,只有 L3 缓存上是同步好的数据。从前一篇我们知道,读 L3 的数据非常影响性能。更坏的情况是跨槽读取,L3 都要 miss,只能从内存上加载。

表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。

 

三. padding

padding就是为了解决false sharing的技术之一。思路就是-- 让不同线程操作的对象处于不同的缓存行即可。

这里我们首先把整个代码贴出来,作为例子:

 

#pragma once

#include <atomic>

#include <assert.h>

#include <cstddef>

 

inline size_t next_pow_2(size_t num)

{

    size_t next = 2;

    size_t i = 0;

    while (next < num)

    {

        next = static_cast<size_t>(1) << i++;

    }

    return next;

}

 

template <typename T>

class SPSCQueue

{

  public:

    typedef T EntryType;

 

    SPSCQueue(size_t size)

        : size_(next_pow_2(size)), mask_(size_ - 1), buffer_(new T[size_]), tail_(0), head_(0) {}

 

    ~SPSCQueue() { delete[] buffer_; }

 

    bool enqueue(const T &input)

    {

        const size_t pos = tail_.load(std::memory_order_relaxed);

        const size_t next_pos = (pos + 1) & mask_;

        if (next_pos == head_.load(std::memory_order_acquire))

        {

            return false;

        }

        buffer_[pos] = input;

        tail_.store(next_pos, std::memory_order_release);

        return true;

    }

 

    bool dequeue(T &output)

    {

        const size_t pos = head_.load(std::memory_order_relaxed);

        if (pos == tail_.load(std::memory_order_acquire))

        {

            return false;

        }

        output = buffer_[pos];

        head_.store((pos + 1) & mask_, std::memory_order_release);

        return true;

    }

 

    bool is_empty()

    {

        return head_.load(std::memory_order_acquire) ==

               tail_.load(std::memory_order_acquire);

    }

 

  private:

    typedef char cache_line_pad_t[64];

    cache_line_pad_t pad0_;

    const size_t size_;

    const size_t mask_;

    T *const buffer_;

    cache_line_pad_t pad1_;

    std::atomic<size_t> tail_;

    cache_line_pad_t pad2_;

    std::atomic<size_t> head_;

};

 

如果你作为一个code reviewer,看到cache_line_pad_t这个变量,你会不会让程序员删掉这些用不到的变量?

 

没错,这个变量就是一个padding变量。

 

如何避免false sharing?如何知道系统有没有false sharing问题?

 

通过上面大篇幅的介绍,我们已经知道伪共享的对程序的影响。那么,在实际的生产开发过程中,我们一定要通过缓存行填充去解决掉潜在的伪共享问题吗?

 

其实并不一定。

 

首先就是多次强调的,伪共享是很隐蔽的,不同类型的计算机具有不同的微架构,如果设计到跨平台的设计,那就更难以把握了,一个确切的填充方案只适用于一个特定的操作系统。还有,缓存的资源是有限的,如果填充会浪费珍贵的 cache 资源,并不适合大范围应用。最后,目前主流的 Intel 微架构 CPU 的 L1 缓存,已能够达到 80% 以上的命中率。

综上所述,并不是每个系统都适合花大量精力去解决潜在的伪共享问题。

 

如果真的想知道false sharing在现有的系统中的情况,可以通过 Intel® VTune™ Performance Analyzer 或者 Intel® Performance Tuning Utility、Visual Studio Profiler 以及 性能计数器 的表现来发现潜在的 false sharing。

 

https://github.com/maxcong001/spsc_queue

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值