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

高并发内存池 性能瓶颈分析与基数树优化(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.lryc.cn/news/621051.html

相关文章:

  • anaconda创建pytorch1.10.0和pytorch2.0.0的GPU环境
  • lesson38:MySQL数据库核心操作详解:从基础查询到高级应用
  • app-4 日志上传
  • 第一章 java基础
  • 在IAR Embedded Workbench for Arm中实现NXP S32K3安全调试
  • Wireshark中捕获的大量UDP数据
  • 一次 Unity ↔ Android 基于 RSA‑OAEP 的互通踩坑记
  • 【题解】P1000 超级玛丽游戏 题解
  • 2025中国快递物流智能装备产业发展论坛将于9月3日上海举办
  • 如何选择图表库|2025 年实现强大数据可视化的 6 个 JavaScript 图表库对比
  • 二进制与进制转换
  • SpringBoot+Vue线上部署MySQL问题解决
  • WinForm之自定义布局(了解)
  • Centos9傻瓜式linux部署CRMEB 开源商城系统(PHP)
  • C++ 仿RabbitMQ实现消息队列项目
  • ClickHouse 日常运维命令总结
  • JMeter性能测试详细版(适合0基础小白学习--非常详细)
  • 前端css学习笔记5:列表表格背景样式设置
  • 回归算法:驱动酒店智能化定价与自动化运营的引擎—仙盟创梦IDE
  • 手写MyBatis第17弹:ResultSetMetaData揭秘:数据库字段到Java属性的桥梁
  • uniapp对接极光消息推送
  • Webpack Plugin 深度解析:从原理到实战开发指南
  • 读取Kaggle下载的数据集(数据的读取 f’{path}\\CMaps\\train_FD001.txt’)
  • mlir operand
  • Day54 Java面向对象08 继承
  • Java中Record的应用
  • 机器翻译:回译与低资源优化详解
  • Java 8 新特性介绍
  • 51单片机-驱动LED模块教程
  • 广义矩估计随机近似中公式(2d)的推导