STL 提供的容器可以有多快?(下)「榨干最后一滴」

以下内容为本人的烂笔头,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/QWgA97TDMGBnwR4hKA7BwA

查表的消耗

某些场景下需要用到大量的 (string, X) 键值对来存储数据,标准库提供了关联容器 std::map 来解决键值对的存储需求。

由于 std::map 负责管理插入元素(键值对)的存储顺序,访问元素时需要比较键,比如 (string, X) 中的 string。键的比较依赖操作符 <,操作符的执行过程直接影响查表的效率,也就影响访问效率。

std::map 内部数据结构采用红黑树,假设 std::map<std::string, int> 中包含 N 个元素,查找一个元素的最多比较次数等于树的高度。对于一个高度均衡的树,平均高度可以认为是 log2(N),其中 N 是树中的元素数量。

可见元素数量比较少,而且比较操作符 < 运算代价较小,同时无法构造出良好的哈希函数的情况下,std::map 是非常合适的容器。相反,元素数量比较多,同时如果能提供良好的哈希函数的情况下,std::unordered_map 更合适一些。

如果想要提升键的比较效率,可以尝试用 (const char *, X) 替换 (string, X)。针对 C 风格的字符串指针,调用比较操作符 < 运算不会执行字典序比较,所以执行速度更快,容器占用空间也相对小。麻烦的是,访问元素时,需要自行确保其以正确的字典序方式进行比较。当然,使用指针可能带来其他副作用,如访问内存地址越界、泄漏、未初始化等等。

除了键对访问效率的限制之外,键值对的值 X 如果比较占空间,在复制时同样会拖慢效率。处理方法可以参考之前提到的策略:指针、智能指针以及写时复制等等策略。

如果你对「写时复制」的概念不是很清楚,可以查阅一下笔者之前的文章《C++ 代码性能空间之极限拉扯:「COW」 真乃神助攻》,本文末尾有跳转阅读链接。

侵入式列表

在对系统效率极为苛刻的条件下,有的开发者需要实现更高效的自定义侵入式列表,榨干最后一滴性能。标准库提供的容器链表虽然在空间和效率上已大为改善,但是对性能的极致追求决定了特定场景的自定义链表还有发挥的空间。

比如:

  1. 标准库提供的链表容器,需要额外的内存开销来存储指向前后节点的信息。如果容器插入大量小对象,会产生内存碎片,对内存的利用率不足。

  2. 即使是最保守的内存分配和释放,比如基本的插入和删除节点的动作,仍然会占用 CPU 资源,何况是频繁为节点分配和释放内存,会大为拖慢链表容器的执行效率。

Intrusive lists(侵入式列表)是一种特殊的数据结构实现方式,不同于传统的链表。它不要求列表负责分配和释放节点的存储空间,而是要求用户自己管理被插入的节点存储空间,

另外,节点自身必须存储其在列表中的位置信息。比如,双向侵入式列表,节点结构中包含了指向前后节点的指针,而单向侵入式列表,节点结构中包含有一个指向下一个节点的指针。

这种列表被称为“侵入式”,是因为存储在列表中节点对象的内部结构包含了列表的相关信息(链接信息),而且节点内存必须由用户管理,不能像标准库提供的链表那样为每个节点单独分配内存。

下面来看看如何定义侵入式链表容器的节点

// 链表容器的链接信息结构
template <typename T>
struct IntrusiveListNodeBase
{
    T* prev;
    T* next;
};

// 要插入链表的用户对象
struct UserObject
{
    IntrusiveListNodeBase<UserObject> list_node;
    // 其它成员,比如数据...
};

由于插入容器列表的用户对象类型是可变的,在设计链表容器的链接信息结构时需要传入可变的类型参数,所以使用模板类。在 C++ 里,结构体其实是特殊的类。

然后,实现一个简单的侵入式链表容器,同样使用模板类的形式。容器应该提供基本的接口供用户使用,比如插入、查询、删除等

template <typename T>
class IntrusiveList
{
public:
    // 在链表前插入节点
    void push_front(T* item)
    {
        auto& node = item->list_node;
        node.prev = nullptr;
        node.next = head_;
        if (head_)
            head_->list_node.prev = item;
        head_ = item;
    }

    // 擦除节点
    void erase(T* item)
    {
        auto& node = item->list_node;
        if (node.prev)
            node.prev->list_node.next = node.next;
        else
            head_ = node.next;

        if (node.next)
            node.next->list_node.prev = node.prev;

        // 清理节点指针,防止悬挂指针
        node.prev = nullptr;
        node.next = nullptr;
    }

    // 查询列表当前节点数量
    int num_of_list()
    {
        T* head = head_;
        int num = 0;
        if (head == nullptr) {
            return num;
        }

        num = 1;
        while (head->list_node.next != nullptr) {
            ++ num;
            head = head->list_node.next;
        }
        return num;
    }

    // ...

private:
    T* head_ = nullptr;
};

最后看看链表的使用情况,对列表插入多个节点和移除部分节点后,查询剩余的节点数量

int main()
{
    UserObject obj1, obj2;
    IntrusiveList<UserObject> list;

    // 将对象插入列表
    list.push_front(&obj1);
    list.push_front(&obj2);
    // 将对象移出列表
    list.erase(&obj1);
    // 查询列表当前节点数量
    std::cout << "there is "
        << list.num_of_list()
        << " elements"
        << std::endl;
    // ...
    return 0;
}

用户负责分配和释放插入侵入式列表的对象资源,上面的演示代码里,插入列表的对象资源分配在栈内,所以用户无须特意手动释放。而且,插入节点的过程无需任何的复制拷贝,删除节点的过程也无需释放任何资源,所以再频繁的插入删除操作都是极快的!

但是,还是要提醒一下,这种侵入式的链表容器不应该是首选的工具,标准库提供的容器已经可以应付绝大部分的需求,理应作为开发首选。现在绝大部分的生产环境并不依赖如此苛刻的精打细算,过分开发反而是高昂的成本。


全文写到这里就结束了,如果各位同学朋友有什么疑问欢迎联系我交流。另外,八戒有自己的技术圈交流群,如果读者朋友有兴趣入群交流技术问题,欢迎联系我。下拉到文章底部有我的联系方式!

最后,非常感激各位朋友的点 「赞」 和点击 「在看」,谢谢!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/797367.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【MySQL 进阶】MySQL 程序 -- 详解

一、MySQL 程序简介 MySQL 安装完成通常会包含如下程序&#xff1a; 1、Linux 系统 程序⼀般在 /usr/bin 目录下&#xff0c;可以通过命令查看&#xff1a; 2、Windows系统 目录&#xff1a;你的安装路径\MySQL Server 8.0\bin&#xff0c;可以通过命令查看&#xff1a; 可…

图像处理:使用 OpenCV-Python 卡通化你的图像(2)

一、说明 在图像处理领域&#xff0c;将图像卡通化是一种新趋势。人们使用不同的应用程序将他们的图像转换为卡通图像。如今&#xff0c;玩弄图像是许多人的爱好。人们通常会点击图片并添加滤镜或使用不同的东西自定义图像并将其发布到社交媒体上。但我们是程序员&#xff0c;…

QML界面控件加载与显示顺序

一、QML界面控件加载顺序 QML在界面加载时的顺序和我们认知的有很大的不同&#xff0c;有时候会对我们获取参数以及界面实现造成很大的困扰 1、加载顺序 import QtQuick 2.12 import QtQml 2.12 import QtQuick.Window 2.12 import QtQuick.VirtualKeyboard 2.4Window {id: …

java.sql.SQLException: Before start of result set

情况描述&#xff0c;在通过JDBC连接数据库时&#xff0c;想直接判断获取的值是否存在&#xff0c;运行时报错。 翻译&#xff1a; 在开始结果集之前 报错截图 解决问题的方法&#xff1a;对结果集ResultSet进行操作之前&#xff0c;一定要先用ResultSet.next()将指针移动至…

CSS学习碎碎念之卡片展示

效果展示&#xff1a; 代码展示 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>图片展示</title…

UART编程

Q:为什么使用串口前要先在电脑上安装CH340驱动&#xff1f; 中断的作用&#xff1f; 环形buffer的作用&#xff1f; static和valitate的作用 三种编程方式简介 也可以通过DMA方式减小CPU资源的消耗 直接把数据在SRAM内存和UART模块进行传输 &#xff0c;流程&#xff1a; …

【算法】平衡二叉树

难度&#xff1a;简单 题目 给定一个二叉树&#xff0c;判断它是否是 平衡二叉树 示例&#xff1a; 示例1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;true 示例2&#xff1a; 输入&#xff1a;root [1,2,2,3,3,null,null,4,4] 输出&…

调整网络安全策略以适应不断升级的威胁形势

关键网络安全统计数据和趋势 当今数字时代网络安全的重要性

项目收获总结--本地缓存方案选型及使用缓存的坑

本地缓存方案选型及使用缓存的坑 一、摘要二、本地缓存三、本地缓存实现方案3.1 自己编程实现一个缓存3.2 基于 Guava Cache 实现本地缓存3.3 基于 Caffeine 实现本地缓存3.4 基于 Encache 实现本地缓存3.5 小结 四、使用缓存的坑4.1 缓存穿透4.2 缓存击穿4.3 缓存雪崩4.4 数据…

游戏的无边框模式是什么?有啥用?

现在很多游戏的显示设置中&#xff0c;都有个比较特殊的选项“无边框”。小伙伴们如果尝试过&#xff0c;就会发现这个效果和全屏几乎一毛一样&#xff0c;于是就很欢快地用了起来&#xff0c;不过大家也许会发现&#xff0c;怎么和全屏比起来&#xff0c;似乎有点不够爽快&…

【2024_CUMCM】时间序列1

目录 概念 时间序列数据 时期和时点时间序列 数值变换规律 长期趋势T 季节趋势S 循环变动C 不规则变动I 叠加和乘积模型 叠加模型 相互独立 乘积模型 相互影响 注 spss缺失值填补 简单填补 五种填补方法 填补原则 1.随机缺失 2.完全随机缺失 3.非随机缺失…

HarmonyOS NEXT:一次开发,多端部署

寄语 这几年特别火的uni-app实现了“一次开发&#xff0c;多端使用”&#xff0c;它这个端指的是ios、安卓、各种小程序这些&#xff0c;而HarmonyOS NEXT也提出了“一次开发&#xff0c;多端部署”&#xff0c;而它这个端指的是终端设备&#xff0c;也就是我们的手机、平板、电…

Java面试题:MVCC

MVCC 保证事务的隔离性 排它锁: 一个事务获取了数据行的排他锁,其他事务就不能再获取该行的其他锁 MVCC: 多版本并发控制 维护一个数据的多个版本,使读写不存在冲突 具体实现依靠 隐藏字段 mysql中隐藏了三个隐藏字段 db_trx_id:最近修改事务 db_roll_ptr:指向上一个…

【Leetcode】最小数字游戏

你有一个下标从 0 开始、长度为 偶数 的整数数组 nums &#xff0c;同时还有一个空数组 arr 。Alice 和 Bob 决定玩一个游戏&#xff0c;游戏中每一轮 Alice 和 Bob 都会各自执行一次操作。游戏规则如下&#xff1a; 每一轮&#xff0c;Alice 先从 nums 中移除一个 最小 元素&…

[linux]IO多路复用机制:select、poll、epoll

为什么需要IO多路复用 首先我要向大家输出一个IO的概念&#xff1a;IO在我看来就是 等 拷贝&#xff08;简化IO模型&#xff09;&#xff0c;等就是等待系统资源&#xff08;设备。数据等&#xff09;就绪&#xff08;比如等待文件描述符就绪&#xff0c;等待数据就绪&#x…

Linux开发:Fuse介绍

Fuse(filesystem in userspace),是一个用户空间的文件系统。通过fuse内核模块的支持&#xff0c;开发者只需要根据fuse提供的接口实现具体的文件操作时所对应的回调函数&#xff0c;就可以实现一个文件系统。由于其主要实现代码位于用户空间中&#xff0c;因此不需要重新编译内…

springboot+vue 开发记录(九)后端打包部署运行

本篇文章主要内容是后端项目写好了&#xff0c;怎么打包部署到服务器上运行。 文章目录 1. 在服务器上安装Docker2. 在Docker中装MySQL3. 在Docker中设置网桥&#xff0c;实现容器间的网络通信4. 修改后端配置文件5. 修改pom.xml文件6. 打包7. 编写DockerFile文件8. 上传文件到…

【调试笔记-20240713-Windows-Tauri 多个HTML页面支持】

调试笔记-系列文章目录 调试笔记-20240713-Windows-Tauri 多个HTML页面支持 文章目录 调试笔记-系列文章目录调试笔记-20240713-Windows-Tauri 多个HTML页面支持 前言一、调试环境操作系统&#xff1a;Windows 10 专业版调试环境调试目标 二、调试步骤搜索相似问题 三、应用场…

Python中的数据容器及其在大数据开发中的应用

在Python编程中&#xff0c;数据容器是存储和组织数据的基本工具。作为大数据开发者&#xff0c;了解并灵活运用各种容器类型对于高效处理大规模数据至关重要。今天&#xff0c;我们将从Set出发&#xff0c;探讨Python中的各种数据容器&#xff0c;以及它们在大数据处理中的应用…

Leetcode3200. 三角形的最大高度

Every day a Leetcode 题目来源&#xff1a;3200. 三角形的最大高度 解法1&#xff1a;模拟 枚举第一行是红色还是蓝色&#xff0c;再按题意模拟即可。 代码&#xff1a; /** lc appleetcode.cn id3200 langcpp** [3200] 三角形的最大高度*/// lc codestart class Solutio…