unordered_map性能被吊打!我用基数树让内存池性能暴涨几十倍的秘密

哈喽,大家好,我是小康!

今天要和大家聊一个特别有意思的话题——基数树

说实话,我第一次听到这个名词的时候,内心是懵逼的。基数?树?这玩意儿到底是啥?

直到有一天,我在研究TCMalloc内存池源码的时候,发现了一个神奇的现象:为什么Google的工程师不用std::unordered_map来做页号映射,而要自己实现一个看起来很复杂的数据结构?

带着这个疑问,我深入研究了一下,结果发现了一个宝藏——基数树

一个让我震惊的性能对比

先来看个数据,直接震撼你的小心脏:

测试场景:100万次查找操作
std::unordered_map:    40878微秒
基数树:               714微秒
性能提升:             57.2521倍!

这还只是中等规模的测试,在大规模场景下,差距会更加夸张。

基数树到底是个啥玩意儿?

好,现在我用最通俗的话给大家解释一下基数树。

简单粗暴地说,基数树就是一个超级大数组!

没错,就这么简单。

比如你要存储键值对,传统的做法是:

  • std::map:用红黑树,需要比较、旋转,O(log n)时间
  • std::unordered_map:用哈希表,需要计算哈希、处理冲突,平均O(1)但有常数开销

而基数树呢?直接把键当作数组的下标!

// 传统方式
unordered_map[123] = ptr;  // 需要计算hash(123),可能还要处理冲突// 基数树方式
array[123] = ptr;          // 直接访问!O(1)且常数极小

是不是超级简单?

动手实现一个基数树(一级)

talk is cheap,show me the code!

const uint32_t MAX_KEY = (1 << 20) - 1;// ============ 基数树实现 ============
template<int BITS = 20>  // 20位,支持100万条目 800 0000
class SimpleRadixTree {
private:static const size_t SIZE = 1 << BITS;void** array_;public:SimpleRadixTree() {array_ = new void*[SIZE];memset(array_, 0, SIZE * sizeof(void*));cout << "创建了支持 " << SIZE << " 条目的基数树" << endl;cout << "理论内存: " << (SIZE * 8) / (1024) << " KB" << endl;}~SimpleRadixTree() {delete[] array_;}// 设置键值对 - O(1)时间!void set(uint32_t key, void* value) {if (key >= SIZE) {cerr << "错误: 键 " << key << " 超出范围 [0, " << (SIZE-1) << "]" << endl;return;}array_[key] = value;}// 获取值 - O(1)时间!void* get(uint32_t key) {if (key >= SIZE) return nullptr;return array_[key];}// 删除键void remove(uint32_t key) {if (key < SIZE) {array_[key] = nullptr;}}// 统计已使用的条目数size_t count_used() const {size_t count = 0;for (size_t i = 0; i < SIZE; ++i) {if (array_[i] != nullptr) {count++;}}return count;}// 获取支持的最大键值uint32_t max_key() const {return SIZE - 1;}
};

就这么几行代码,我们就实现了一个高性能的一级基数树!

基数树的核心:用空间换时间

性能大PK:基数树 VS unordered_map

我专门写了个测试程序,在我的Ubuntu机器上跑了一下:

void performance_battle() {cout << "\n========== 性能大PK开始 ==========" << endl;const int TEST_COUNT = 1000000;// 准备测试数据vector<uint32_t> keys;cout << "准备 " << TEST_COUNT << " 个测试键..." << endl;for(int i = 0; i < TEST_COUNT; i++) {//keys.push_back(i * 7);  // 一些分散的键,避免连续访问的缓存优势keys.push_back(i);  }cout << "数据准备完成,开始测试..." << endl;// ========== 测试 unordered_map ==========cout << "\n测试 unordered_map..." << endl;unordered_map<uint32_t, void*> hashmap;auto start = chrono::high_resolution_clock::now();// 插入测试for(int i = 0; i < TEST_COUNT; i++) {hashmap[keys[i]] = reinterpret_cast<void*>(i + 1);}// 查找测试for(int i = 0; i < TEST_COUNT; i++) {volatile void* result = hashmap[keys[i]];(void)result; // 防止编译器优化掉}auto end = chrono::high_resolution_clock::now();auto hashmap_time = chrono::duration_cast<chrono::microseconds>(end - start);// ========== 测试基数树 ==========cout << "测试基数树..." << endl;SimpleRadixTree<20> radix_tree;start = chrono::high_resolution_clock::now();// 插入测试for(int i = 0; i < TEST_COUNT; i++) {radix_tree.set(keys[i], reinterpret_cast<void*>(i + 1));}// 查找测试for(int i = 0; i < TEST_COUNT; i++) {volatile void* result = radix_tree.get(keys[i]);(void)result; // 防止编译器优化掉}end = chrono::high_resolution_clock::now();auto radix_time = chrono::duration_cast<chrono::microseconds>(end - start);// ========== 输出结果 ==========cout << "\n=== 性能大PK结果 ===" << endl;cout << "测试规模:       " << TEST_COUNT << " 次操作" << endl;cout << "unordered_map:  " << hashmap_time.count() << " 微秒" << endl;cout << "基数树:         " << radix_time.count() << " 微秒" << endl;if (radix_time.count() > 0) {double speedup = (double)hashmap_time.count() / radix_time.count();cout << "性能提升:       " << speedup << " 倍" << endl;if (speedup > 5) {cout << "基数树性能碾压!" << endl;} else if (speedup > 2) {cout << " 基数树明显更快!" << endl;} else {cout << "性能相当" << endl;}}// 验证结果正确性cout << "\n验证结果正确性..." << endl;bool correct = true;for(int i = 0; i < 1000; i++) {  // 验证前1000个结果void* hash_result = hashmap[keys[i]];void* radix_result = radix_tree.get(keys[i]);if (hash_result != radix_result) {cout << "结果不一致!键: " << keys[i] << endl;correct = false;break;}}if (correct) {cout << "结果验证通过,两种方法结果一致!" << endl;}
}

在我的机器上跑出来的结果:

=== 性能大PK结果 ===
测试规模:       1000000 次操作
unordered_map:  40878 微秒
基数树:         714 微秒
性能提升:       57.2521 倍

50+倍的性能提升! 这还只是中等规模的测试。

为什么会有这么大的差距?

  1. 基数树:直接数组访问,没有任何额外计算
  2. unordered_map:需要计算哈希值、处理冲突、内存分散访问

基数树在内存池中的神奇应用

现在来看看基数树的实际应用场景——内存池!

在内存池系统中,我们需要根据内存地址快速找到对应的内存块信息(Span)。

传统方式可能是这样:

// 传统内存池的页号映射
unordered_map<PageID, Span*> page_to_span;// 哈希查找,较慢
Span* find_span(void* ptr) {PageID page_id = (uintptr_t)ptr >> 12;  // 假设4KB页面std::lock_guard<std::mutex> lock(map_mutex_);// 还要加锁auto it = page_to_span.find(id);return (it != page_to_span.end()) ? it->second : nullptr;   
}

用基数树优化后:

// 基数树版本的页号映射
SimpleRadixTree<28> page_map;  // 支持1TB地址空间// 不需要加锁
Span* find_span(void* ptr) {PageID page_id = (uintptr_t)ptr >> 12;return (Span*)page_map.get(page_id);    // 直接数组访问,超快!
}

这就是为什么TCMalloc、jemalloc这些高性能内存池都在用基数树的原因!

一个完整的内存池应用示例

// ============ 内存池应用演示 ============
struct Span {uint32_t page_id;uint32_t num_pages;bool in_use;void* free_list;Span(uint32_t pid, uint32_t pages, bool used = true) : page_id(pid), num_pages(pages), in_use(used), free_list(nullptr) {}
};class MemoryPool {
private:SimpleRadixTree<24> page_map_;  // 24位,支持16M页,相当于64GB地址空间vector<Span*> spans_;  // 保存所有Span,用于清理public:~MemoryPool() {for(auto* span : spans_) {delete span;}}// 注册内存块void register_span(Span* span) {spans_.push_back(span);// 为这个Span的每一页都建立映射for(uint32_t i = 0; i < span->num_pages; i++) {uint32_t page_id = span->page_id + i;page_map_.set(page_id, span);}cout << "注册Span: 起始页号=" << span->page_id << ", 页数=" << span->num_pages << endl;}// 根据地址快速找到所属的内存块Span* addr_to_span(void* ptr) {// 假设页面大小为4KB,即PAGE_SHIFT=12uint32_t page_id = (reinterpret_cast<uintptr_t>(ptr) >> 12) & 0xFFFFFF;  // 取低24位return reinterpret_cast<Span*>(page_map_.get(page_id));}// 根据页号查找SpanSpan* page_to_span(uint32_t page_id) {return reinterpret_cast<Span*>(page_map_.get(page_id));}// 释放内存时,快速定位到对应的Spanbool free_memory(void* ptr) {Span* span = addr_to_span(ptr);  // O(1)时间找到Span!if(span && span->in_use) {cout << "找到地址 " << ptr << " 对应的Span: 页号" << span->page_id << endl;return true;}return false;}};

看到没?有了基数树,内存池的地址查找变成了O(1)操作,这对高并发场景下的内存分配性能提升是巨大的!

当然,这里只是简单展示一下基数树的应用思路。实际的内存池项目要复杂得多,想学习完整实现的同学不妨看看: 三周肝出4000行代码,我的内存池竟然让malloc"破防"了!性能暴涨7.37倍背后的技术真相

什么时候该用基数树?

基数树虽然牛逼,但也不是万能的。什么时候用呢?

适合用基数树的场景:

  • 键的范围相对固定且已知
  • 键分布比较密集
  • 对查找性能要求极高
  • 内存充足的环境

不适合用基数树的场景:

  • 键的范围非常大且分布稀疏
  • 内存严重受限的环境
  • 键是字符串等复杂类型

进阶:多层基数树

当键的范围太大时(比如64位地址空间),1层基数树就不够用了。这时候我们可以用多层基数树。

简单来说,就是把一个超级大数组拆分成多个小数组的组合:

// 2层基数树示意
template<int BITS = 36>  // 支持Linux 64位地址空间  
class PageMap2 {
private:// 位数分配:尽量均匀分配static const int LEAF_BITS = BITS / 2;        // 18位static const int ROOT_BITS = BITS - LEAF_BITS; // 18位static const size_t ROOT_SIZE = 1ULL << ROOT_BITS; static const size_t LEAF_SIZE = 1ULL << LEAF_BITS;struct Leaf {void* values[LEAF_SIZE];Leaf() {memset(values, 0, sizeof(values));}};Leaf* root_[ROOT_SIZE];public:PageMap2() {memset(root_, 0, sizeof(root_));cout << "二级基数树初始化完成!" << endl;cout << "支持地址空间: " << (1ULL << BITS) << " 页 ("<< ((1ULL << BITS) * 4096) / (1024ULL*1024*1024*1024) << " TB)" << endl;cout << "固定内存开销: " << sizeof(root_) / (1024*1024) << " MB" << endl;}~PageMap2() {// 清理所有分配的叶子节点for(size_t i = 0; i < ROOT_SIZE; i++) {delete root_[i];}}// 获取值 - 两次数组访问,仍然是O(1)!void* get(uint64_t key) {if(key >= (1ULL << BITS)) return nullptr;uint64_t i1 = key >> LEAF_BITS;      // 高位作为一级索引uint64_t i2 = key & (LEAF_SIZE - 1); // 低位作为二级索引if(root_[i1] == nullptr) return nullptr;return root_[i1]->values[i2];}// 设置值 - 按需分配叶子节点void set(uint64_t key, void* value) {if(key >= (1ULL << BITS)) return;uint64_t i1 = key >> LEAF_BITS;uint64_t i2 = key & (LEAF_SIZE - 1);// 按需分配叶子节点if(root_[i1] == nullptr) {root_[i1] = new Leaf();cout << "分配新的叶子节点: " << i1 << endl;}root_[i1]->values[i2] = value;}
};

这样可以支持36位键空间(256TB地址),但内存是按需分配的,不会浪费。

在我最近带领学员做的高性能内存池项目中,我们就采用了这种二级基数树的设计思路,不过做了更多的工程优化。实际效果非常不错,相比原来的unordered_map方案,性能提升了10倍以上。

感兴趣的朋友可以看这篇文章: 三周肝出4000行代码,我的内存池竟然让malloc"破防"了!性能暴涨7.37倍背后的技术真相

我的使用心得

用了基数树这么久,我总结几个心得:

  1. 不要被"树"这个名字迷惑了,基数树本质上就是数组
  2. 在高性能场景下,基数树是神器,特别是内存池、路由表这些场景
  3. 多层基数树是进阶版本,1层够用就别搞复杂

编译运行试试看

想要自己试试的同学,把文章中的代码整合一下,用这个命令编译:

g++ -std=c++11 -O2 radix_tree_test.cpp -o test
./test

在我的Ubuntu 20.04上跑得飞快!

写在最后

基数树真的是一个被低估的数据结构。

在合适的场景下,它能带来数倍甚至数十倍的性能提升。Google、Facebook这些大厂的底层库都在大量使用基数树,绝对不是没有道理的。

下次如果你遇到需要快速键值查找的场景,特别是键范围相对固定的情况,不妨试试基数树。说不定会有意想不到的惊喜!

好了,今天就聊到这里。如果你觉得这篇文章对你有帮助,记得点赞、收藏、关注三连哦!

有什么问题欢迎在评论区讨论,我看到会第一时间回复的。

C++ 项目实战课程

不过话说回来,这些底层性能优化技巧,光看理论是远远不够的,必须在真实项目中摸爬滚打才能融会贯通。

这段时间我一直在带学员做一些硬核的C++项目,从底层数据结构到高并发组件,每个项目都是企业级的真实场景。看到不少同学通过这些实战项目,技术水平有了质的飞跃,面试时也更有底气了。

C++硬核项目实战
手撸线程池才是C++程序员的硬实力!7天手把手带你从0到1完整实现
三周肝出4000行代码,我的内存池竟然让malloc"破防"了!性能暴涨7.37倍背后的技术真相
手撸4200行MySQL连接池,8天带你搞定后端核心组件!
终于有人把C++多线程下载工具讲透了!7天手把手带你写出专业级工具

这些项目都会深入涉及多线程编程、并发优化、 高并发处理、系统级性能调优等企业级开发的核心技术,而且是在真实的项目环境中应用。不是纸上谈兵,而是真刀真枪地写代码、调优化、解决实际问题。

感兴趣的同学,赶紧加我vx:jkfwdkf,备注「项目实战」!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网站备案后可以更换域名吗网红营销优势

Title 题目 Automated anomaly-aware 3D segmentation of bones and cartilages in kneeMR images from the Osteoarthritis Initiative 来自骨关节炎计划的膝关节MR图像的自动异常感知3D骨骼和软骨分割 Background 背景 近年来&#xff0c;多个机器学习算法被提出用于图像…

asp网站配置伪静态重庆注册公司核名在哪个网站

上次提到的开机自启动的配置&#xff0c;获得了LD的称赞&#xff0c;然而LD的要求&#xff0c;都是“既得陇复望蜀”的&#xff0c;他又期望我们能实现openGauss安装的“自动化”&#xff0c;于是尝试了下用shell脚本部署&#xff0c;附件中的脚本实测有效&#xff0c;openEule…

详细介绍:《 Linux 点滴漫谈: 一 》开源之路:Linux 的历史、演进与未来趋势

详细介绍:《 Linux 点滴漫谈: 一 》开源之路:Linux 的历史、演进与未来趋势pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-fa…

深入解析:TENGJUN“二合一(2.5MM+3.5MM)”耳机插座:JA10-BPD051-A;参数与材质说明

深入解析:TENGJUN“二合一(2.5MM+3.5MM)”耳机插座:JA10-BPD051-A;参数与材质说明pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important…

龙凤网站建设云聚达长春网站建设哪里好

基于飞桨paddle波士顿房价预测练习模型测试代码 导入基础库 #paddle&#xff1a;飞桨的主库&#xff0c;paddle 根目录下保留了常用API的别名&#xff0c;当前包括&#xff1a;paddle.tensor、paddle.framework、paddle.device目录下的所有API&#xff1b; import paddle #Lin…

CentOS 9服务器版 部署Zabbix7.0 server端 - 详解

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

深入解析:Apache 生产环境操作与 LAMP 搭建指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

JAVA第一天

Markdown 学习 标题 +空格=一级标题 +空格=二级标题 ......... 字体 粗体 斜体 粗斜体 删除 引用第一天学习分割线图片超链接 我的世界 列表表格ctrl+t 代码

什么网站做简历模板关键词排名怎么快速上去

http://answers.unity3d.com/questions/34328/terrain-with-multiple-splat-textures-how-can-i-det.html转载于:https://www.cnblogs.com/klobohyz/archive/2012/10/09/2716627.html

c 网站开发需要什么软件东莞出行政策有变了

文章目录 前言一、哈希结构体&#xff1f;二、增删差3、遍历&#xff0c;清空&#xff0c;计数 前言 哈希表在头文件“utash.h”中已经有了&#xff0c;只需简单学习用法即可 例如&#xff1a;随着人工智能的不断发展&#xff0c;机器学习这门技术也越来越重要&#xff0c;很…

自己做免费网站的视频参考消息电子版手机版

处理 HttpApplication 的事件HttpApplication 提供了基于事件的扩展机制&#xff0c;允许程序员借助于处理管道中的事件进行处理过程扩展。由于 HttpApplication 对象是由 ASP.NET 基础架构来创建和维护的&#xff0c;那么&#xff0c;如何才能获取这个对象引用&#xff0c;以便…

东莞营销型网站建设流程网站速成

1.类型转换 1.1 int(x):转化为一个整数&#xff0c;只能转换由纯数字组成的字符串 float->int 浮点型强转整形会去掉小数点后面的数&#xff0c;只保留整数部分 a 1.2 print(type(a)) #<class float> b int(a) print(type(b)) #<class int>print(int…

现货做网站wordpress登入可见插件

需做工作 在每个微服务下面新建一个Dockerfile文件根据Dockerfile文件使用docker build指令&#xff0c;打包为具体的镜像&#xff08;根据自己需求选择&#xff09;将docker镜像上传到私人docker仓库或者是公共仓库&#xff0c;如果没有上传&#xff0c;则自动保存在本地编写…

C# Avalonia 15- Animation- CustomEasingFunction

C# Avalonia 15- Animation- CustomEasingFunctionCustomEasingFunction.axaml代码<Window xmlns="https://github.com/avaloniaui"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xm…

白银市建设局网站云捷配快速开发平台

目录 环境准备 生成SSH 密钥对 数据库备份并推送到gitlab脚本 设置定时任务 环境准备 服务器要有安装达梦数据库&#xff08;达梦安装这里就不示例了&#xff09;&#xff0c;git 安装Git 1、首先&#xff0c;确保包列表是最新的&#xff0c;运行以下命令&#xff1a; …

网站开发综合实训总结变化型网页网站有哪些

编辑 | 宋慧 出品 | CSDN云计算 vSphere、vSAN&#xff0c;从云计算兴起&#xff0c;就是 VMware 在虚拟化、分布式存储里大名鼎鼎的核心技术产品。不过随着云的发展到云原生、以及国内混合云快速发展的今天&#xff0c;虚拟化的领导者 VMware 有哪些最新的方案&#xff0c;值…

网站开发语言那个好新建网站如何调试

SQL 视图&#xff1a;概念、应用与最佳实践 SQL&#xff08;Structured Query Language&#xff09;视图是数据库管理中的一个重要概念&#xff0c;它允许用户以虚拟表的形式查看数据。视图在数据库中并不实际存储数据&#xff0c;而是提供了一个查询结果的快照&#xff0c;这…

哪个网站可以做鸟瞰图济南网站建设索q479185700

记录一下最近的生活&#xff0c;做一下简单的梳理&#xff0c;具体详细的梳理等我目前的工作步入正轨 以后再开始好好地总结一下2023年的过往经历&#xff0c;总结过去&#xff0c;展望未来。计划一下未来的2024该怎么度过。 最近一阵子都忙着考试&#xff0c;然后从10号以后一…

US$189 VVDI2 BMW FEM amp; BDC Functions Authorization Service With Ikeycutter Condor

VVDI2 BMW FEM & BDC Functions Authorization Service With Ikeycutter CondorNote: VVDI2 now add BMW FEM & BDC functions, VVDI2 Must have BMW OBD Function(SV86-3), then can open this function.Ther…

wordpress删除中文温州网站建设选择乐云seo

使用命令查看磁盘的空间 docker system df &#xff0c;类似于Linux的df命令&#xff0c;用于查看Docker使用的磁盘空间Docker镜像占据了4.789GBDocker容器占据了348BDocker数据卷占据了0B 执行删除命令 docker system prune命令可以用于清理磁盘&#xff0c;删除关闭的容器、…