单入单出队列性能优化(Lock-Free)

news/2025/2/24 11:16:05

摘要:文中首先介绍了有锁线程安全循环队列的基本实现,然后探讨了使用原子变量实现 Lock-Free 队列的优势,能够减少线程之间的数据竞争。接着,介绍了数据对齐的策略,以降低伪共享的概率,随后引入了索引缓存来减少索引访问冲突的影响。最后,文中提出了使用位运算替代模运算来优化循环队列的性能。
关键字:单入单出队列,Lock-Free,伪共享,索引缓存,性能优化

  CppCon中有一个单入单出Lock-Free队列的话题,描述了如何优化单入单出队列来达到性能最大化。该话题比较适合学习,因此基于该话题自己测试了下具体优化策略的性能。

1 线程安全循环队列

  ,我们先简单描述下有锁线程安全的队列的实现。首先是循环队列的实现。

在这里插入图片描述

  循环队列的实现比较简单。首先,申请一个固定大小的内存,也就是图示的capacity长度。然后维护两个游标inoutin是插入的位置,out是元素出队的位置。当in==out时,表示队列为空。为了方便表示队列满,队列的最后一块内存空闲出来,也就是当(in + 1) % capacity == out时队列满。

  入队时将插入值写入data[in],然后in=(in+1)%capacity即可(为了线程安全,我们操作前进行加锁)。

bool push(const value_type& v){
    std::unique_lock<std::mutex> lock(_mutex);
    if(full(0)){
        return false;
    }

    alloc_traits::construct(_alloc, _data + _in, v);
    _in = (_in + 1)%_cap;
    return true;
}

  出队时将data[out]处的元素出队并且销毁队列内存上的对象,out=(out+1)%capacity即可。

bool pop(value_type & v){
    std::unique_lock<std::mutex> lock(_mutex);
    if(empty(0)){
        return false;
    }

    v = std::move(_data[_out]);
    _data[_out].~value_type(); 
    _out = (_out + 1)%_cap;
    return true;
}

2 Lock-Free

  多线程访问队列存在线程安全问题,为了保证线程安全,通过加锁来保证不同线程的安全性。但是锁的粒度太大,在访问冲突比较大的场景下,在临界区多线程依然是串行的,容易造成性能问题。因此一个优化思路是使用原子变量来实现Lock-Free的线程安全队列。Lock-Free的线程安全队列能够最小化不同线程之间的数据竞争,增加并发度。

bool push(const value_type& v){
    const auto inCur = _in.load(std::memory_order_relaxed);
    const auto outCur = _out.load(std::memory_order_acquire);
    if(full(inCur, outCur)){
        return false;
    }

    alloc_traits::construct(_alloc, _data + inCur, v);
    _in.store((inCur + 1) % capacity(), std::memory_order_release);
    return true;
}

bool pop(value_type &val){
    const auto inCur = _in.load(std::memory_order_acquire);
    const auto outCur = _out.load(std::memory_order_relaxed);
    if (empty(inCur, outCur)) // (3)
        return false;

    val = std::move(_data[outCur]);
    alloc_traits::destroy(_alloc, _data + outCur);
    _out.store((outCur + 1) % capacity(), std::memory_order_release); // (4)
    return true;
}

  能够看到单入单出队列使用Lock-Free之后性能优化非常明显。
在这里插入图片描述

3 数据对齐

  数据对齐到CPU的Cacheline能够减低伪共享的概率。CPU 通常使用缓存来加速内存访问。数据在内存中是以缓存行(通常为 64 字节)为单位进行加载的。当一个线程修改一个变量时,整个缓存行会被标记为无效,这会影响到同一缓存行中的其他变量,即使这些变量并未被修改。尽管访问的是不同的变量,但由于它们共享同一个缓存行,不同线程的并发执行可能会导致频繁的缓存失效,进而影响性能。

  数据对齐的实现比较简单,下面代码中的pad是为了保证不同索引在不同的缓存行。

static constexpr auto hardware_destructive_interference_size = size_type{ 64 };
value_type* _data{};
char _pad0[hardware_destructive_interference_size]{};
alloc _alloc{};
char _pad1[hardware_destructive_interference_size]{};
alignas(hardware_destructive_interference_size) size_type _cap{};
char _pad2[hardware_destructive_interference_size]{};
alignas(hardware_destructive_interference_size) AtomicType _in{};
char _pad3[hardware_destructive_interference_size]{};
alignas(hardware_destructive_interference_size) AtomicType _out{};

在这里插入图片描述

4 索引缓存

  对于单入单出队列,如果push的时候如果上一次未满,这一次push也未满,pop同理,此时并不需要强行访问索引。因此可以增加索引缓存保存上一次索引的值,避免索引访问冲突而导致的CPU缓存刷新。但是需要注意的是这种方式只有索引竞争比较激烈的情况下才会有性能优化,否则可能有副作用。

bool push(const value_type& v) {
    const auto inCur = _in.load(std::memory_order_relaxed);
    if (full(inCur, _outCache)) {
        _outCache = _out.load(std::memory_order_acquire);
        if (full(inCur, _outCache)) {
            return false;
        }
    }

    alloc_traits::construct(_alloc, _data + inCur, v);
    _in.store((inCur + 1) % capacity(), std::memory_order_release);
    return true;
}

bool pop(value_type& val) {
    const auto outCur = _out.load(std::memory_order_relaxed);
    if (empty(_inCache, outCur)) {
        _inCache = _in.load(std::memory_order_acquire);
        if (empty(_inCache, outCur)) {
            return false;
        }
    }
        
    val = std::move(_data[outCur]);
    alloc_traits::destroy(_alloc, _data + outCur);
    _out.store((outCur + 1) % capacity(), std::memory_order_release); // (4)
    return true;
}

  下面的数据带锁性能非常差,是因为大幅度降低队列的长度,增加缓存的竞争,导致带锁近乎于串行访问。
在这里插入图片描述

5 Mask

  引入Mask实际上是为了优化循环队列%的耗时。而mask是利用位运算替代除余。具体实现比较简单,只需要限制构造输入的cap一定是2^n,而实际的cap也就是下面的成员mask2^n-1,对应的二进制位0x111...111````。这样当队列满的时候只需要cur&mask```就能替代除余操作。

ThreadSafeQueueLockFreeAlignCacheMask(const size_type cap)
    : _mask(cap - 1) {
    _data = alloc_traits::allocate(_alloc, cap);
}

auto full(const size_type inCur, const size_type outCur) const {
    return ((inCur + 1) & _mask) == outCur;
}

auto empty(const size_type inCur, const size_type outCur) const {
    return inCur == outCur;
}

bool push(const value_type& v) {
    const auto inCur = _in.load(std::memory_order_relaxed);
    if (full(inCur, _outCache)) {
        _outCache = _out.load(std::memory_order_acquire);
        if (full(inCur, _outCache)) {
            return false;
        }
    }

    alloc_traits::construct(_alloc, _data + inCur, v);
    _in.store((inCur + 1) & _mask, std::memory_order_release);
    return true;
}

bool pop(value_type& val) {
    const auto outCur = _out.load(std::memory_order_relaxed);
    if (empty(_inCache, outCur)) {
        _inCache = _in.load(std::memory_order_acquire);
        if (empty(_inCache, outCur)) {
            return false;
        }
    }
        
    val = std::move(_data[outCur]);
    alloc_traits::destroy(_alloc, _data + outCur);
    _out.store((outCur + 1) & _mask, std::memory_order_release); // (4)
    return true;
}

6 参考文献

  • Single Producer Single Consumer Lock-free FIFO From the Ground Up - Charles Frasch - CppCon 2023
  • Github: Single Producer Single Consumer Lock-free FIFO From the Ground Up - Charles Frasch - CppCon 2023

http://www.niftyadmin.cn/n/5864253.html

相关文章

C++初阶——简单实现list

目录 1、前言 2、List.h 3、Test.cpp 1、前言 1. 简单实现std::list&#xff0c;重点&#xff1a;迭代器&#xff0c;模板类&#xff0c;运算符重载。 2. 并不是&#xff0c;所有的类&#xff0c;都需要深拷贝&#xff0c;像迭代器类模板&#xff0c;只是用别的类的资源&am…

leetcode day20 滑动窗口209+904

209 长度最小的子数组 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl1, ..., numsr-1, numsr] &#xff0c;并返回其长度。如果不存在符合条件的子数组&#xff0c;返回 0 。 示例 1&#…

【Python量化金融实战】-第1章:Python量化金融概述:1.4 开发环境搭建:Jupyter Notebook、VS Code、PyCharm

在量化金融开发中&#xff0c;选择合适的开发环境至关重要。本章介绍三种主流工具&#xff1a;Jupyter Notebook&#xff08;交互式分析&#xff09;、VS Code&#xff08;轻量级编辑器&#xff09;、PyCharm&#xff08;专业IDE&#xff09;&#xff0c;并通过实战案例展示其应…

UniApp SelectorQuery 讲解

一、SelectorQuery简介 在UniApp中&#xff0c;SelectorQuery是一个非常强大的工具&#xff0c;它允许开发者查询节点信息。通过这个API&#xff0c;我们可以获取到页面元素的尺寸、位置、滚动条位置等信息。这在处理动态布局、动画效果或是用户交互时尤为重要。 二、基本使用…

【Git 学习笔记_27】DIY 实战篇:利用 DeepSeek 实现 GitHub 的 GPG 密钥创建与配置

文章目录 1 前言2 准备工作3 具体配置过程3.1. 本地生成 GPG 密钥3.2. 导出 GPG 密钥3.3. 将密钥配置到 Git 中3.4. 测试提交 4 问题排查记录5 小结与复盘 1 前言 昨天在更新我的第二个 Vim 专栏《Mastering Vim (2nd Ed.)》时遇到一个经典的 Git 操作问题&#xff1a;如何在 …

亚马逊云科技MySQL托管服务:Amazon RDS for MySQL的技术优势与成本优化实践

引言&#xff1a; 在数字化转型的浪潮中&#xff0c;数据库作为企业核心业务的“中枢神经”&#xff0c;其稳定性、性能及成本直接影响企业的运营效率和竞争力。然而&#xff0c;自建MySQL数据库的复杂性、运维成本高企、扩展性不足等问题&#xff0c;始终是开发者与…

【人工智能】蓝耘智算平台盛大发布DeepSeek满血版:开创AI推理体验新纪元

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀ 蓝耘智算平台 蓝耘智算平台核心技术与突破元生代推理引擎快速入门&#xff1a;三步调用大模型接口&#xff0c;OpenAI SDK无缝兼容实战用例文…

编程技巧/算法

1、在strings中查找key的位置 来源&#xff1a; BusyBox-1.37.0 index_in_strings()数据结构&#xff1a; static const char keywords[] __attribute__((aligned(1))) "bs\0""count\0""seek\0""skip\0""if\0""…