当前位置: 首页 > web >正文

高并发内存池 性能瓶颈分析与基数树优化(9)

文章目录

  • 前言
  • 一、性能瓶颈分析
    • 操作步骤及其环境配置
    • 分析性能瓶颈
  • 二、基数树优化
    • 单层基数树
    • 二层基数树
    • 三层基数树
  • 三、使用基数树来优化代码
  • 总结


前言

  到了最后一篇喽,嘻嘻!
  终于是要告一段落了,接下来我们将学什么呢,再说吧,可能得把Linux的一些内容给补完


一、性能瓶颈分析

操作步骤及其环境配置

  我们的代码此时与 malloc 之间还是有差距的,所以现在我们来分析一下我们的代码

  这里就要借助 VS 的一些神奇妙妙功能了,首先先确保在 Debug 环境下运行,依次点击“调试→性能探查器”进行性能分析

在这里插入图片描述
  同时我们屏蔽一下 malloc / free 的 BenchMark 函数,而是就对我们自己的 内存池 进行分析

int main()
{size_t n = 10000;cout << "==========================================================" << endl;BenchmarkConcurrentMalloc(n, 4, 10);cout << endl << endl;//BenchmarkMalloc(n, 4, 10);cout << "==========================================================" << endl;return 0;
}

  接下来我们点击勾选“检测”,再点击“开始”进行
在这里插入图片描述

  接着会弹出这个页面,说明此时此刻你没配置,当然了,我一开始也没有

在这里插入图片描述

  那具体要怎么配置呢?直接就来个讲解吧!先右键项目名称,然后再点击属性

在这里插入图片描述

  然后再依次 点击配置属性 -> 点击链接器 -> 点击高级 -> 将配置文件设置为是(/PROFILE)

在这里插入图片描述
    好了这下你就配置好了,那么我们接下来继续启动性能分析器,看看到底怎么个说法

在这里插入图片描述
    接下来就静静等待结果就好了

分析性能瓶颈

    通过分析结果可以看到,ConcurrentAlloc 和 ConcurrentFree 这两个函数占用了接近60%的时间
在这里插入图片描述

    接着将视图修改为调用方/被调用方,这样看起来更加直观

在这里插入图片描述
    点击占用时间最多的 Allocate ,再点击占用时间最多的 FetchFromCentralCache ,再点击占用时间最多的 FetchRangeObj ,再点击占用时间最多的 GetOneSpan ,再点击进去就是 lock函数 占用最多时间了,事实上 ConcurrentAlloc 也同样如此,就是因为锁竞争导致了浪费那么多的时间

    针对此瓶颈,我们想到了使用基数树进行性能优化

二、基数树优化

  基数树实际上就是一个分层的哈希表,根据所分层数不同可分为单层基数树、二层基数树、三层基数树等。

  或者我们不如先来想想为什么要加锁,无非就是因为STL底层不保证线程安全,因为 unordered_map的底层数据结构是红黑树,在访问映射关系的时候如果另一个线程进行添加映射的时候,可能由于插入或者旋转导致读取线程发生错误,而基数树就可以完全避免这个问题,至于N层只是说根据不同环境下的不同选择

单层基数树

    因为我是在 32位 环境下写的,所以我将选择这个基数树,当然原理都是一样的,如果你是64位环境下,那我还是建议你用三层基数树吧

    单层基数树实际采用的就是直接定址法,每一个页号对应 span 的地址就存储数组中在以该页号为下标的位置。

在这里插入图片描述
    最坏的情况下我们需要建立所有页号与其 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; //页的数目
};

    当我们需要建立映射时就调用set函数,需要读取映射关系时,就调用get函数

    比如32位平台下,以一页大小为8K为例,此时页的数目就是2 ^ 32 ÷ 2 ^ 13 = 2 ^ 19 ,因此存储页号最多需要19个比特位,此时传入非类型模板参数的值就是32 − 13 = 19。由于32位平台下指针的大小是4字节,因此该数组的大小就是2 ^ 19 × 4 = 2 ^ 21 = 2 M ,内存消耗不大,是可行的。但如果是在64位平台下,此时该数组的大小是2 ^ 51 × 8 = 2 ^ 54 = 2 ^ 24 G ,这显然是不可行的,实际上对于64位的平台,我们需要使用三层基数树

    其实又是时间换空间的思想,这真是一个经久不衰的结论

下面是DS大人给出的结合比喻的解析
在这里插入图片描述

二层基数树

    这里还是以 32位 平台下,一页的大小为 8K 为例来说明,此时存储页号最多需要 19个 比特位。而二层基数树实际上就是把这 19个 比特位分为两次进行映射。

    比如用前5个比特位在基数树的第一层进行映射,映射后得到对应的第二层,然后用剩下的比特位在基数树的第二层进行映射,映射后最终得到该页号对应的span指针。

    在二层基数树中,第一层的数组占用 2 ^ 5 × 4 = 2 ^ 7 Byte 空间,第二层的数组最多占用 2 ^ 5 × 2 ^ 14 × 4 = 2 ^ 21 = 2 M 。二层基数树相比一层基数树的好处就是,一层基数树必须一开始就把 2 M 的数组开辟出来,而二层基数树一开始时只需将第一层的数组开辟出来,当需要进行某一页号映射时再开辟对应的第二层的数组就行了

//二层基数树
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); //将第二层的空间全部开辟好}
};

    下面又是DS大人给出的解释

在这里插入图片描述

三层基数树

    原理图如下,其实还是很好看的
在这里插入图片描述

//三层基数树
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(){}
};

    当然要注意的是此时只有当要建立某一页号的映射关系时,再开辟对应的数组空间,而没有建立映射的页号就可以不用开辟其对应的数组空间,此时就能在一定程度上节省内存空间

三、使用基数树来优化代码

    此时将 PageCache类 当中的 unorder_map 用 基数树 进行替换即可,由于当前是 32位 平台,因此这里随便用几层基数树都可以

// 单例模式
class PageCache
{
public://...
private://std::unordered_map<PAGE_ID, Span*> _idSpanMap;TCMalloc_PageMap1<32 - PAGE_SHIFT> _idSpanMap;
};

    当我们需要建立页号与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;
}

    最后看看效果吧,下面是添加了基数树优化后的对比,依次是固定内存大小、不固定内存大小

在这里插入图片描述

在这里插入图片描述

    可以看到在不固定内存大小的前提下,由于 central cache 的桶锁机制,对比差距更大了


总结

  代码的世界里,没有“完美”,只有“不断迭代”,这个内存池是我的第一个项目,也是第一个孩子,过去学到的“锁优化”“内存对齐”“数据结构”不再是抽象概念,而是解决实际问题的工具,C++本身就与性能优化及其相关,通过这个项目也是领悟到了锁的魅力,真切感受到了工程能力的成长

  下一步可以怎么继续加深学习呢,或许可以尝试将内存池嵌入实际应用(如HTTP服务器)?不过那是之后的事了呢,但是我现在需要休息一下~

http://www.xdnf.cn/news/17934.html

相关文章:

  • vLLM(Vectorized Large Language Model Serving) 的深度解析
  • C++ 应用场景全景解析:从系统级到AI的跨越式演进
  • GaussDB数据库架构师修炼(十六) 如何选择磁盘
  • jetson orin nx(8G)烧录super系统实录
  • 亚马逊手工制品分类体系革新:从流量重构到运营升级的深度解析
  • Wireshark中常见协议
  • WriteBooster
  • 解决安装 scikit-learn==1.3.1 时出现的版本匹配错误
  • MixOne在macOS上安装碰到的问题
  • SpringMVC请求与响应
  • FPGA读取AHT20温湿度模块思路及实现,包含遇到的问题(IIC协议)
  • Opencv 边界填充 图像运算 阈值处理 和图像平滑处理
  • 【Linux基础知识系列】第九十四篇 - 如何使用traceroute命令追踪路由
  • 【从零开始学习Redis】项目实战-黑马点评D1
  • 【代码随想录day 20】 力扣 538.把二叉搜索树转换为累加树
  • 计算机网络---传输控制协议Transmission Control Protocol(TCP)
  • 数据结构之顺序表相关算法题
  • Qt---Qt函数库
  • 西门子PLC通过稳联技术EtherCAT转Profinet网关连接baumuller伺服器的配置案例
  • Java基础 8.14
  • linux中的dump命令
  • Python训练营打卡Day32-神经网络的训练
  • 问题总结三
  • 财务自动化软件敏感数据泄露风险评估与防护措施
  • AI幻觉终结之后:GPT-5开启的“可靠性”新赛道与开发者生存指南
  • 边缘光效果加流光效果
  • 在启智平台使用A100对文心开源大模型Ernie4.5 0.3B微调(失败)
  • iOS混淆工具有哪些?游戏 App 防护下的混淆与加固全攻略
  • 应用银行卡识别技术,构建更安全、便捷的数字身份认证与支付生态
  • Linux下使用Samba 客户端访问 Samba 服务器的配置(Ubuntu Debian)