【项目日记(十)】瓶颈分析与使用基数树优化
前言
上一期我们对整个项目进行了细节部分的优化,并在最后测试了多线程环境下和malloc的性能对比测试,发现malloc有时候还是更胜一筹的,基于此我们进行对我们的内存池进行瓶颈分析与优化。
目录
前言
一、项目瓶颈分析
VS编译器下性能分析的操作步骤
分析性能瓶颈
二、针对性能瓶颈使用基数树进行优化
代码修改
再次对比malloc进行测试
打包成动静态库
项目源码:高并发内存池源码
一、项目瓶颈分析
经过前面的测试可以看到,我们的代码此时与malloc之间还是有差距的,此时我们就应该分析分析我们当前项目的瓶颈在哪里,但这不能简单的凭感觉,我们应该用性能分析的工具来进行分析。
VS编译器下性能分析的操作步骤
VS编译器中就带有性能分析的工具的,我们可以依次点击。“调试-》性能探测器”进行分析。注意:该过程一定是在Debug模式下的。
同时我们将代码中n的值由10000调成了1000,否则该分析过程可能会花费较多时间,并且将malloc的测试代码进行了屏蔽,因为我们要分析的是我们实现的高并发内存池。
int main()
{size_t n = 1000;cout << "==========================================================" <<endl;BenchmarkConcurrentMalloc(n, 4, 10);cout << endl << endl;//BenchmarkMalloc(n, 4, 10);cout << "==========================================================" <<endl;return 0;
}
勾选 检测-》开始
然后会弹出一个框,我们选择我们要分析的项目,确定即可
分析性能瓶颈
通过分析结果可以看到,光是Deallocate和MapObjectToSpan这两个函数就占用了一半多的时间。 而MapObjectToSpan是占用消耗时间最多的。
我们点击去看看MapObjectToSpan,发现是因为加锁的就消耗了47%的时间
在来点到Deallocate看看,发现是ListTooLong占用的多
继续点到ListTooLong看看,发现是RleaseListToSpan
继续点到RleaseListToSpan看看,发现是MapObjectToSpan
这里我们再点击去就肯定是锁的问题了:
因此当前项目的瓶颈点就在锁竞争上面,需要解决调用MapObjectToSpan函数访问映射关系时的加锁问题。tcmalloc当中针对这一点使用了基数树进行优化,使得在读取这个映射关系时可以做到不加锁。
二、针对性能瓶颈使用基数树进行优化
基数树实际上就是一个分层的哈希表(类似于多级页表),根据所分层数不同可分为单层基数树(32(32))、二层基数树(32(32))、三层基数树(64位)等。
单层基数树
单层基数树实际采用的就是直接定址法的哈希,每一个 页号对应span的地址 就存储数组中在以该页号为下标的位置。也就是说单层基数树本质上就是一个指针数组,页号就是下标,下标对应的元素就是页号对应 span 的地址。
//单层基数树
template <int BITS>
class TCMalloc_PageMap1
{
public:typedef uintptr_t Number;explicit TCMalloc_PageMap1(){size_t size = sizeof(void*) << BITS; //需要开辟数组的大小size_t alignSize = SizeClass::_RoundUp(size, 1 << PAGE_SHIFT); //按页对齐后的大小array_ = (void**)SystemAlloc(alignSize >> PAGE_SHIFT); //向堆申请空间memset(array_, 0, size); //对申请到的内存进行清理}void* get(Number k) const{if ((k >> BITS) > 0) //k的范围不在[0, 2^BITS-1]{return NULL;}return array_[k]; //返回该页号对应的span}void set(Number k, void* v){assert((k >> BITS) == 0); //k的范围必须在[0, 2^BITS-1]array_[k] = v; //建立映射}
private:void** array_; //存储映射关系的数组static const int LENGTH = 1 << BITS; //页的数目
};
代码解释:
BITS 表示存储页号对多需要的比特位的个数,32位下一页按8K算就是19页,这里就是19,64位下就是51页。即32-PAGE_SHIFT/64-PAGE_SHIFT
此时当我们需要建立映射时就调用set函数,需要读取映射关系时,就调用get函数就行了。
二层基数树
这里还是以32位平台下,一页的大小为8K为例来说明,此时存储页号最多需要19个比特位。而二层基数树实际上就是把这19个比特位分为两次进行映射。
比如用前5个比特位在基数树的第一层进行映射,映射后得到对应的第二层,然后用剩下的比特位在基数树的第二层进行映射,映射后最终得到该页号对应的span指针。
二层基数树相比一层基数树的好处就是,一层基数树必须一开始就把2 M 2M2M的数组开辟出来,而二层基数树一开始时只需将第一层的数组开辟出来,当需要进行某一页号映射时再开辟对应的第二层的数组就行了。
//二层基数树
template <int BITS>
class TCMalloc_PageMap2
{
private:static const int ROOT_BITS = 5; //第一层对应页号的前5个比特位static const int ROOT_LENGTH = 1 << ROOT_BITS; //第一层存储元素的个数static const int LEAF_BITS = BITS - ROOT_BITS; //第二层对应页号的其余比特位static const int LEAF_LENGTH = 1 << LEAF_BITS; //第二层存储元素的个数//第一层数组中存储的元素类型struct Leaf{void* values[LEAF_LENGTH];};Leaf* root_[ROOT_LENGTH]; //第一层数组
public:typedef uintptr_t Number;explicit TCMalloc_PageMap2(){memset(root_, 0, sizeof(root_)); //将第一层的空间进行清理PreallocateMoreMemory(); //直接将第二层全部开辟}void* get(Number k) const{const Number i1 = k >> LEAF_BITS; //第一层对应的下标const Number i2 = k & (LEAF_LENGTH - 1); //第二层对应的下标if ((k >> BITS) > 0 || root_[i1] == NULL) //页号值不在范围或没有建立过映射{return NULL;}return root_[i1]->values[i2]; //返回该页号对应span的指针}void set(Number k, void* v){const Number i1 = k >> LEAF_BITS; //第一层对应的下标const Number i2 = k & (LEAF_LENGTH - 1); //第二层对应的下标assert(i1 < ROOT_LENGTH);root_[i1]->values[i2] = v; //建立该页号与对应span的映射}//确保映射[start,start_n-1]页号的空间是开辟好了的bool Ensure(Number start, size_t n){for (Number key = start; key <= start + n - 1;){const Number i1 = key >> LEAF_BITS;if (i1 >= ROOT_LENGTH) //页号超出范围return false;if (root_[i1] == NULL) //第一层i1下标指向的空间未开辟{//开辟对应空间static ObjectPool<Leaf> leafPool;Leaf* leaf = (Leaf*)leafPool.New();memset(leaf, 0, sizeof(*leaf));root_[i1] = leaf;}key = ((key >> LEAF_BITS) + 1) << LEAF_BITS; //继续后续检查}return true;}void PreallocateMoreMemory(){Ensure(0, 1 << BITS); //将第二层的空间全部开辟好}
};
因此在二层基数树中有一个Ensure函数,当需要建立某一页号与其span之间的映射关系时,需要先调用该Ensure函数确保用于映射该页号的空间是开辟了的,如果没有开辟则会立即开辟。
而在32位平台下,就算将二层基数树第二层的数组全部开辟出来也就消耗了2 M 2M2M的空间,内存消耗也不算太多,因此我们可以在构造二层基数树时就把第二层的数组全部开辟出来。
三层基数树
上面一层基数树和二层基数树都适用于32位平台,而对于64位的平台就需要用三层基数树了。三层基数树与二层基数树类似,三层基数树实际上就是把存储页号的若干比特位分为三次进行映射。
此时只有当要建立某一页号的映射关系时,再开辟对应的数组空间,而没有建立映射的页号就可以不用开辟其对应的数组空间,此时就能在一定程度上节省内存空间。
//三层基数树
template <int BITS>
class TCMalloc_PageMap3
{
private:static const int INTERIOR_BITS = (BITS + 2) / 3; //第一、二层对应页号的比特位个数static const int INTERIOR_LENGTH = 1 << INTERIOR_BITS; //第一、二层存储元素的个数static const int LEAF_BITS = BITS - 2 * INTERIOR_BITS; //第三层对应页号的比特位个数static const int LEAF_LENGTH = 1 << LEAF_BITS; //第三层存储元素的个数struct Node{Node* ptrs[INTERIOR_LENGTH];};struct Leaf{void* values[LEAF_LENGTH];};Node* NewNode(){static ObjectPool<Node> nodePool;Node* result = nodePool.New();if (result != NULL){memset(result, 0, sizeof(*result));}return result;}Node* root_;
public:typedef uintptr_t Number;explicit TCMalloc_PageMap3(){root_ = NewNode();}void* get(Number k) const{const Number i1 = k >> (LEAF_BITS + INTERIOR_BITS); //第一层对应的下标const Number i2 = (k >> LEAF_BITS) & (INTERIOR_LENGTH - 1); //第二层对应的下标const Number i3 = k & (LEAF_LENGTH - 1); //第三层对应的下标//页号超出范围,或映射该页号的空间未开辟if ((k >> BITS) > 0 || root_->ptrs[i1] == NULL || root_->ptrs[i1]->ptrs[i2] == NULL){return NULL;}return reinterpret_cast<Leaf*>(root_->ptrs[i1]->ptrs[i2])->values[i3]; //返回该页号对应span的指针}void set(Number k, void* v){assert(k >> BITS == 0);const Number i1 = k >> (LEAF_BITS + INTERIOR_BITS); //第一层对应的下标const Number i2 = (k >> LEAF_BITS) & (INTERIOR_LENGTH - 1); //第二层对应的下标const Number i3 = k & (LEAF_LENGTH - 1); //第三层对应的下标Ensure(k, 1); //确保映射第k页页号的空间是开辟好了的reinterpret_cast<Leaf*>(root_->ptrs[i1]->ptrs[i2])->values[i3] = v; //建立该页号与对应span的映射}//确保映射[start,start+n-1]页号的空间是开辟好了的bool Ensure(Number start, size_t n){for (Number key = start; key <= start + n - 1;){const Number i1 = key >> (LEAF_BITS + INTERIOR_BITS); //第一层对应的下标const Number i2 = (key >> LEAF_BITS) & (INTERIOR_LENGTH - 1); //第二层对应的下标if (i1 >= INTERIOR_LENGTH || i2 >= INTERIOR_LENGTH) //下标值超出范围return false;if (root_->ptrs[i1] == NULL) //第一层i1下标指向的空间未开辟{//开辟对应空间Node* n = NewNode();if (n == NULL) return false;root_->ptrs[i1] = n;}if (root_->ptrs[i1]->ptrs[i2] == NULL) //第二层i2下标指向的空间未开辟{//开辟对应空间static ObjectPool<Leaf> leafPool;Leaf* leaf = leafPool.New();if (leaf == NULL) return false;memset(leaf, 0, sizeof(*leaf));root_->ptrs[i1]->ptrs[i2] = reinterpret_cast<Node*>(leaf);}key = ((key >> LEAF_BITS) + 1) << LEAF_BITS; //继续后续检查}return true;}void PreallocateMoreMemory(){}
};
因此当我们要建立某一页号的映射关系时,需要先确保存储该页映射的数组空间是开辟好了的,也就是调用代码中的Ensure函数,如果对应数组空间未开辟则会立马开辟对应的空间。
代码修改
我们将上面的tcmalloc实现的基数树专门放在一个PageMap.h中,在PageCahe.cpp中隐隐即可使用!
此时当我们需要建立页号与span的映射时,就调用基数树当中的set函数。
_idSpanMap.set(span->_pageId, span);
而当我们需要读取某一页号对应的span时,就调用基数树当中的get函数。
Span* ret = (Span*)_idSpanMap.get(id);
并且现在PageCache类向外提供的,用于读取映射关系的MapObjectToSpan函数内部就不需要加锁了。
//获取从对象到span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT; //页号Span* ret = (Span*)_idSpanMap.get(id);assert(ret != nullptr);return ret;
}
为什么读取基数树映射关系时不需要加锁?
当某个线程在读取映射关系时,可能另外一个线程正在建立其他页号的映射关系,而此时无论我们用的是C++当中的map还是unordered_map,在读取映射关系时都是需要加锁的。
因为C++中map的底层数据结构是红黑树,unordered_map的底层数据结构是哈希表,而无论是红黑树还是哈希表,当我们在插入数据时其底层的结构都有可能会发生变化。比如红黑树在插入数据时可能会引起树的旋转,而哈希表在插入数据时可能会引起哈希表扩容。此时要避免出现数据不一致的问题,就不能让插入操作和读取操作同时进行,因此我们在读取映射关系的时候是需要加锁的。
而对于基数树来说就不一样了,基数树的空间一旦开辟好了就不会发生变化,因此无论什么时候去读取某个页的映射,都是对应在一个固定的位置进行读取的。并且我们不会同时对同一个页进行读取映射和建立映射的操作,因为我们只有在释放对象时才需要读取映射,而建立映射的操作都是在page cache进行的。也就是说,读取映射时读取的都是对应span的_useCount不等于0的页,而建立映射时建立的都是对应span的_useCount等于0的页,所以说我们不会同时对同一个页进行读取映射和建立映射的操作。
再次对比malloc进行测试
均匀测试:
固定大小测试:
这次对比下来无论是定长的对象大小还是均匀的测试我们自己的内存池均占上风。
打包成动静态库
实际Google开源的tcmalloc是会直接用于替换malloc的,不同平台替换的方式不同。比如基于Unix的系统上的glibc,使用了weak alias的方式替换;而对于某些其他平台,需要使用hook的钩子技术来做。
对于我们当前实现的项目,可以考虑将其打包成静态库或动态库。我们先右击解决方案资源管理器当中的项目名称,然后选择属性。 此时会弹出一个框,点击常规-》配置类型,然后选择动态库/静态库
项目源码:高并发内存池源码
OK,到这里我们的这个项目就完成了,这个项目是第一个项目,我们的目的主要是学习人家tcmalloc的最核心思想,而不是说造一个更好的轮子。
相关文章:

【项目日记(十)】瓶颈分析与使用基数树优化
前言 上一期我们对整个项目进行了细节部分的优化,并在最后测试了多线程环境下和malloc的性能对比测试,发现malloc有时候还是更胜一筹的,基于此我们进行对我们的内存池进行瓶颈分析与优化。 目录 前言 一、项目瓶颈分析 VS编译器下性能分…...
后台管理系统比较全面的分析对比
以下是主流的 后台管理系统模板 分类与技术选型指南,涵盖开源、商业及全栈解决方案,可根据项目需求灵活选择: 一、开源免费模板 1. React 技术栈 Ant Design Pro 官网:pro.ant.design特点:阿里出品,内置 R…...

HCIA复习拓扑实验
一.拓扑图 二.需求 1.学校内部的HTTP客户端可以正常通过域名www.baidu.com访问到百度网络中HTTP服务器 2.学校网络内部网段基于192.168.1.0/24划分,PC1可以正常访问3.3.3.0/24网段,但是PC2不允许 3.学校内部路由使用静态路由,R1和R2之间两…...

TI毫米波雷达开发 —— 串口输出数据解析
TI毫米波雷达开发 —— 串口输出解析 TLV协议协议概述HeaderBodyPadding TI 毫米波雷达芯片计算的结果数据都会从UART发出供上位机进行解析并展示。解析和展示是两个不同的概念,解析指提取有效数据并转换成常见的度量值。展示指数据的可视化。 由于雷达这个领域的特…...

Docker Desktop 4.38 安装与配置全流程指南(Windows平台)
一、软件定位与特性 Docker Desktop 是容器化应用开发与部署的一体化工具,支持在本地环境创建、管理和运行Docker容器。4.38版本新增GPU加速支持、WSL 2性能优化和Kubernetes 1.28集群管理功能,适用于微服务开发、CI/CD流水线搭建等场景。 二、安装环境…...

【AD】5-16 泪滴的添加
1.工具—滴泪(快捷键TE)...

聊天服务器分布式改造
目前的聊天室是单节点的,无论是http接口还是socket接口都在同一个进程,无法承受太多人同时在线,容灾性也非常差。因此,一个成熟的IM产品一定是做成分布式的,根据功能分模块,每个模块也使用多个节点并行部署…...
el-table(elementui)表格合计行使用以及滚动条默认样式修改
一、el-table新增合计行以及el-table展示数据出现的问题 1. 使用合计行 el-table的属性show-summary设为true,即可在表格尾部展示合计行。默认情况下,第一列不展示数据,而显示合计二字,可以通过sum-text自己配置,其余…...

Web前端开发——HTML基础下
HTML语法 一表格1.基本格式2.美化表格合并居中属性 二表单1.input2.select3.textarea4.button5.date6.color7.checkbox8.radio9.range10.number 一表格 1.基本格式 HTML表格由<table>标签定义 其中行由<tr>标签定义,单元格由<td>定义。我们先来…...
Python使用入门(一)
初识数据类型 整型(int) print(666) print(2 10) print(2 * 12)字符串(str) 单行字符串 #单行字符串 print("我是小红aaa") print(我是小红aaa)print("中国上海") print(中国上海)# 输出带引号的字符串 print(我是"小红aaa) print("我是\&qu…...

基于multisim的花样彩灯循环控制电路设计与仿真
1 课程设计的任务与要求 (一)、设计内容: 设计一个8路移存型彩灯控制器,基本要求: 1. 8路彩灯能演示至少三种花型(花型自拟); 2. 彩灯用发光二极管LED模拟; 3. 选做…...

求最大公约数【C/C++】
大家好啊,欢迎来到本博客( •̀ ω •́ )✧,我将带领大家详细的了解最大公约数的思想与解法。 一、什么是公约数 公约数,也称为公因数,是指两个或多个整数共有的因数。具体来说,如果一个整数能被两个或多个整数整除&…...
leetcode day27 455+376
455 分发饼干 假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有…...
go的grpc
GRPC介绍 目录 单体架构微服务架构问题原始的grpc 服务端客户端原生rpc的问题 grpc的hello world 服务端客户端 proto文件proto语法 数据类型 基本数据类型其他数据类型 编写风格多服务 单体架构 只能对整体扩容一荣俱荣,一损俱损代码耦合,项目的开…...

算法每日一练 (9)
💢欢迎来到张胤尘的技术站 💥技术如江河,汇聚众志成。代码似星辰,照亮行征程。开源精神长,传承永不忘。携手共前行,未来更辉煌💥 文章目录 算法每日一练 (9)最小路径和题目描述解题思路解题代码…...
软考高级信息系统项目管理师笔记-第10章项目进度管理
第10章项目进度管理 10.1 管理基础 10.1.1 项目进度计划的定义和总要求 1、项目进度计划是 一种用于沟通和管理干系人期望的工具,为绩效报告提供依据。 2、项目管理团队编制进度计划的一般步骤为: 首先选择进度计划方法,例如关键路径法; 然后将项目特定数据,如活动、计…...
专门为高速连续扫描设计的TDI工业相机
TDI(Time Delay Integration,时间延迟积分)工业相机是一种基于特殊CCD(电荷耦合器件)技术的成像设备,主要用于高速、高灵敏度、高分辨率的图像采集场景。其核心原理是通过多级积分和同步电荷转移技术&#…...

【Vue3】实现一个超过高度后可控制显示隐藏的组件
组件效果图 未达到最大高度 达到设置的最大高度 进行展开 实现代码 组件代码 备注:通过tailwindcss设置的样式,通过element-plus/icons-vue设置的图标,可根据情况进行替换 <template><!-- 限制高度组件 --><div ref"…...
Spring提供的SPEL表达式
SPEL 1. 概述 SpEL是Spring框架中用于表达式语言的一种方式。它类似于其他编程语言中的表达式语言,用于在运行时计算值或执行特定任务。 SpEL提供了一种简单且强大的方式来访问和操作对象的属性、调用对象的方法,以及实现运算、条件判断等操作。它可以…...

JAVA编程【jvm垃圾回收的差异】
jvm垃圾回收的差异 JVM(Java Virtual Machine)的垃圾回收(GC)机制是自动管理内存的一种方式,能够帮助开发者释放不再使用的内存,避免内存泄漏和溢出等问题。不同的垃圾回收器(GC)有…...
三维GIS开发cesium智慧地铁教程(5)Cesium相机控制
一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点: 路径验证:确保相对路径.…...
Linux简单的操作
ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...

ardupilot 开发环境eclipse 中import 缺少C++
目录 文章目录 目录摘要1.修复过程摘要 本节主要解决ardupilot 开发环境eclipse 中import 缺少C++,无法导入ardupilot代码,会引起查看不方便的问题。如下图所示 1.修复过程 0.安装ubuntu 软件中自带的eclipse 1.打开eclipse—Help—install new software 2.在 Work with中…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...

【VLNs篇】07:NavRL—在动态环境中学习安全飞行
项目内容论文标题NavRL: 在动态环境中学习安全飞行 (NavRL: Learning Safe Flight in Dynamic Environments)核心问题解决无人机在包含静态和动态障碍物的复杂环境中进行安全、高效自主导航的挑战,克服传统方法和现有强化学习方法的局限性。核心算法基于近端策略优化…...

【Linux】Linux 系统默认的目录及作用说明
博主介绍:✌全网粉丝23W,CSDN博客专家、Java领域优质创作者,掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围:SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…...
解决:Android studio 编译后报错\app\src\main\cpp\CMakeLists.txt‘ to exist
现象: android studio报错: [CXX1409] D:\GitLab\xxxxx\app.cxx\Debug\3f3w4y1i\arm64-v8a\android_gradle_build.json : expected buildFiles file ‘D:\GitLab\xxxxx\app\src\main\cpp\CMakeLists.txt’ to exist 解决: 不要动CMakeLists.…...

抽象类和接口(全)
一、抽象类 1.概念:如果⼀个类中没有包含⾜够的信息来描绘⼀个具体的对象,这样的类就是抽象类。 像是没有实际⼯作的⽅法,我们可以把它设计成⼀个抽象⽅法,包含抽象⽅法的类我们称为抽象类。 2.语法 在Java中,⼀个类如果被 abs…...

tauri项目,如何在rust端读取电脑环境变量
如果想在前端通过调用来获取环境变量的值,可以通过标准的依赖: std::env::var(name).ok() 想在前端通过调用来获取,可以写一个command函数: #[tauri::command] pub fn get_env_var(name: String) -> Result<String, Stri…...

高考志愿填报管理系统---开发介绍
高考志愿填报管理系统是一款专为教育机构、学校和教师设计的学生信息管理和志愿填报辅助平台。系统基于Django框架开发,采用现代化的Web技术,为教育工作者提供高效、安全、便捷的学生管理解决方案。 ## 📋 系统概述 ### 🎯 系统定…...