热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【项目设计】高并发内存池(七)[性能测试和提升]

🎇C++学习历程:入门 博客主页:一起去看日落吗持续分享博主的C++学习历程博主的能力有限,出现错误希望大

🎇C++学习历程:入门


  • 博客主页:一起去看日落吗
  • 持续分享博主的C++学习历程
  • 博主的能力有限,出现错误希望大家不吝赐教
  • 分享给大家一句我很喜欢的话: 也许你现在做的事情,暂时看不到成果,但不要忘记,树🌿成长之前也要扎根,也要在漫长的时光🌞中沉淀养分。静下来想一想,哪有这么多的天赋异禀,那些让你羡慕的优秀的人也都曾默默地翻山越岭🐾。

在这里插入图片描述

🍁 🍃 🍂 🌿



目录

  • 🌿1. 性能瓶颈分析
  • 🌿2. 针对性能瓶颈使用基数树进行优化
  • 🌿3. 使用基数树进行优化代码实现


🌿1. 性能瓶颈分析

经过前面的测试可以看到,我们的代码此时与malloc之间还是有差距的,此时我们就应该分析分析我们当前项目的瓶颈在哪里,但这不能简单的凭感觉,我们应该用性能分析的工具来进行分析。

  • VS编译器下性能分析的操作步骤

VS编译器中就带有性能分析的工具的,我们可以依次点击“调试→性能和诊断”进行性能分析,注意该操作要在Debug模式下进行。

同时我们将代码中n的值由10000调成了1000,否则该分析过程可能会花费较多时间,并且将malloc的测试代码进行了屏蔽,因为我们要分析的是我们实现的高并发内存池。

在点击了“调试→性能和诊断”后会弹出一个提示框,我们直接点击“开始”进行了。

在这里插入图片描述
然后会弹出一个选项框,这里我们选择的是第二个,因为我们要分析的是各个函数的用时时间,然后点击下一步。

出现以下选项框继续点击下一步。

最后点击完成,就可以等待分析结果了。

  • 分析性能瓶颈

通过分析结果可以看到,光是Deallocate和MapObjectToSpan这两个函数就占用了一半多的时间。

在这里插入图片描述
而在Deallocate函数中,调用ListTooLong函数时消耗的时间是最多的。

在这里插入图片描述
在ListTooLong函数中,调用ReleaseListToSpans函数时消耗的时间是最多的。

在这里插入图片描述

在ReleaseListToSpans函数中,调用MapObjectToSpan函数时消耗的时间是最多的。

在这里插入图片描述

也就是说,最终消耗时间最多的实际就是MapObjectToSpan函数,我们这时再来看看为什么调用MapObjectToSpan函数会消耗这么多时间。通过观察我们最终发现,调用该函数时会消耗这么多时间就是因为锁的原因。

在这里插入图片描述
因此当前项目的瓶颈点就在锁竞争上面,需要解决调用MapObjectToSpan函数访问映射关系时的加锁问题。tcmalloc当中针对这一点使用了基数树进行优化,使得在读取这个映射关系时可以做到不加锁。



🌿2. 针对性能瓶颈使用基数树进行优化

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

  • 单层基数树

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

在这里插入图片描述

最坏的情况下我们需要建立所有页号与其span之间的映射关系,因此这个数组中元素个数应该与页号的数目相同,数组中每个位置存储的就是对应span的指针。

//单层基数树
template <int BITS>
class TCMalloc_PageMap1
{
public:typedef uintptr_t Number;explicit TCMalloc_PageMap1(){size_t size &#61; sizeof(void*) << BITS; //需要开辟数组的大小size_t alignSize &#61; SizeClass::_RoundUp(size, 1 << PAGE_SHIFT); //按页对齐后的大小array_ &#61; (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) &#61;&#61; 0); //k的范围必须在[0, 2^BITS-1]array_[k] &#61; v; //建立映射}
private:void** array_; //存储映射关系的数组static const int LENGTH &#61; 1 << BITS; //页的数目
};

此时当我们需要建立映射时就调用set函数&#xff0c;需要读取映射关系时&#xff0c;就调用get函数就行了。

代码中的非类型模板参数BITS表示存储页号最多需要比特位的个数。在32位下我们传入的是32-PAGE_SHIFT&#xff0c;在64位下传入的是64-PAGE_SHIFT。而其中的LENGTH成员代表的就是页号的数目&#xff0c;即 22BITS

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

  • 二层基数树

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

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

在这里插入图片描述
在二层基数树中&#xff0c;第一层的数组占用25 x &#61; 27 Byte 空间&#xff0c;第二层的数组最多占用25 x 214 x 4 &#61; 221 &#61; 2M。二层基数树相比一层基数树的好处就是&#xff0c;一层基数树必须一开始就把 2M 的数组开辟出来&#xff0c;而二层基数树一开始时只需将第一层的数组开辟出来&#xff0c;当需要进行某一页号映射时再开辟对应的第二层的数组就行了。

//二层基数树
template <int BITS>
class TCMalloc_PageMap2
{
private:static const int ROOT_BITS &#61; 5; //第一层对应页号的前5个比特位static const int ROOT_LENGTH &#61; 1 << ROOT_BITS; //第一层存储元素的个数static const int LEAF_BITS &#61; BITS - ROOT_BITS; //第二层对应页号的其余比特位static const int LEAF_LENGTH &#61; 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 &#61; k >> LEAF_BITS; //第一层对应的下标const Number i2 &#61; k & (LEAF_LENGTH - 1); //第二层对应的下标if ((k >> BITS) > 0 || root_[i1] &#61;&#61; NULL) //页号值不在范围或没有建立过映射{return NULL;}return root_[i1]->values[i2]; //返回该页号对应span的指针}void set(Number k, void* v){const Number i1 &#61; k >> LEAF_BITS; //第一层对应的下标const Number i2 &#61; k & (LEAF_LENGTH - 1); //第二层对应的下标assert(i1 < ROOT_LENGTH);root_[i1]->values[i2] &#61; v; //建立该页号与对应span的映射}//确保映射[start,start_n-1]页号的空间是开辟好了的bool Ensure(Number start, size_t n){for (Number key &#61; start; key <&#61; start &#43; n - 1;){const Number i1 &#61; key >> LEAF_BITS;if (i1 >&#61; ROOT_LENGTH) //页号超出范围return false;if (root_[i1] &#61;&#61; NULL) //第一层i1下标指向的空间未开辟{//开辟对应空间static ObjectPool<Leaf> leafPool;Leaf* leaf &#61; (Leaf*)leafPool.New();memset(leaf, 0, sizeof(*leaf));root_[i1] &#61; leaf;}key &#61; ((key >> LEAF_BITS) &#43; 1) << LEAF_BITS; //继续后续检查}return true;}void PreallocateMoreMemory(){Ensure(0, 1 << BITS); //将第二层的空间全部开辟好}
};

因此在二层基数树中有一个Ensure函数&#xff0c;当需要建立某一页号与其span之间的映射关系时&#xff0c;需要先调用该Ensure函数确保用于映射该页号的空间是开辟了的&#xff0c;如果没有开辟则会立即开辟。

而在32位平台下&#xff0c;就算将二层基数树第二层的数组全部开辟出来也就消耗了 2 M 2M 2M的空间&#xff0c;内存消耗也不算太多&#xff0c;因此我们可以在构造二层基数树时就把第二层的数组全部开辟出来。

  • 三层基数树

上面一层基数树和二层基数树都适用于32位平台&#xff0c;而对于64位的平台就需要用三层基数树了。三层基数树与二层基数树类似&#xff0c;三层基数树实际上就是把存储页号的若干比特位分为三次进行映射。

在这里插入图片描述
此时只有当要建立某一页号的映射关系时&#xff0c;再开辟对应的数组空间&#xff0c;而没有建立映射的页号就可以不用开辟其对应的数组空间&#xff0c;此时就能在一定程度上节省内存空间。

//三层基数树
template <int BITS>
class TCMalloc_PageMap3
{
private:static const int INTERIOR_BITS &#61; (BITS &#43; 2) / 3; //第一、二层对应页号的比特位个数static const int INTERIOR_LENGTH &#61; 1 << INTERIOR_BITS; //第一、二层存储元素的个数static const int LEAF_BITS &#61; BITS - 2 * INTERIOR_BITS; //第三层对应页号的比特位个数static const int LEAF_LENGTH &#61; 1 << LEAF_BITS; //第三层存储元素的个数struct Node{Node* ptrs[INTERIOR_LENGTH];};struct Leaf{void* values[LEAF_LENGTH];};Node* NewNode(){static ObjectPool<Node> nodePool;Node* result &#61; nodePool.New();if (result !&#61; NULL){memset(result, 0, sizeof(*result));}return result;}Node* root_;
public:typedef uintptr_t Number;explicit TCMalloc_PageMap3(){root_ &#61; NewNode();}void* get(Number k) const{const Number i1 &#61; k >> (LEAF_BITS &#43; INTERIOR_BITS); //第一层对应的下标const Number i2 &#61; (k >> LEAF_BITS) & (INTERIOR_LENGTH - 1); //第二层对应的下标const Number i3 &#61; k & (LEAF_LENGTH - 1); //第三层对应的下标//页号超出范围&#xff0c;或映射该页号的空间未开辟if ((k >> BITS) > 0 || root_->ptrs[i1] &#61;&#61; NULL || root_->ptrs[i1]->ptrs[i2] &#61;&#61; NULL){return NULL;}return reinterpret_cast<Leaf*>(root_->ptrs[i1]->ptrs[i2])->values[i3]; //返回该页号对应span的指针}void set(Number k, void* v){assert(k >> BITS &#61;&#61; 0);const Number i1 &#61; k >> (LEAF_BITS &#43; INTERIOR_BITS); //第一层对应的下标const Number i2 &#61; (k >> LEAF_BITS) & (INTERIOR_LENGTH - 1); //第二层对应的下标const Number i3 &#61; k & (LEAF_LENGTH - 1); //第三层对应的下标Ensure(k, 1); //确保映射第k页页号的空间是开辟好了的reinterpret_cast<Leaf*>(root_->ptrs[i1]->ptrs[i2])->values[i3] &#61; v; //建立该页号与对应span的映射}//确保映射[start,start&#43;n-1]页号的空间是开辟好了的bool Ensure(Number start, size_t n){for (Number key &#61; start; key <&#61; start &#43; n - 1;){const Number i1 &#61; key >> (LEAF_BITS &#43; INTERIOR_BITS); //第一层对应的下标const Number i2 &#61; (key >> LEAF_BITS) & (INTERIOR_LENGTH - 1); //第二层对应的下标if (i1 >&#61; INTERIOR_LENGTH || i2 >&#61; INTERIOR_LENGTH) //下标值超出范围return false;if (root_->ptrs[i1] &#61;&#61; NULL) //第一层i1下标指向的空间未开辟{//开辟对应空间Node* n &#61; NewNode();if (n &#61;&#61; NULL) return false;root_->ptrs[i1] &#61; n;}if (root_->ptrs[i1]->ptrs[i2] &#61;&#61; NULL) //第二层i2下标指向的空间未开辟{//开辟对应空间static ObjectPool<Leaf> leafPool;Leaf* leaf &#61; leafPool.New();if (leaf &#61;&#61; NULL) return false;memset(leaf, 0, sizeof(*leaf));root_->ptrs[i1]->ptrs[i2] &#61; reinterpret_cast<Node*>(leaf);}key &#61; ((key >> LEAF_BITS) &#43; 1) << LEAF_BITS; //继续后续检查}return true;}void PreallocateMoreMemory(){}
};

因此当我们要建立某一页号的映射关系时&#xff0c;需要先确保存储该页映射的数组空间是开辟好了的&#xff0c;也就是调用代码中的Ensure函数&#xff0c;如果对应数组空间未开辟则会立马开辟对应的空间。



&#x1f33f;3. 使用基数树进行优化代码实现
  • 代码更改

现在我们用基数树对代码进行优化&#xff0c;此时将PageCache类当中的unorder_map用基数树进行替换即可&#xff0c;由于当前是32位平台&#xff0c;因此这里随便用几层基数树都可以。

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

此时当我们需要建立页号与span的映射时&#xff0c;就调用基数树当中的set函数。

_idSpanMap.set(span->_pageId, span);

而当我们需要读取某一页号对应的span时&#xff0c;就调用基数树当中的get函数。

Span* ret &#61; (Span*)_idSpanMap.get(id);

并且现在PageCache类向外提供的&#xff0c;用于读取映射关系的MapObjectToSpan函数内部就不需要加锁了。

//获取从对象到span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{PAGE_ID id &#61; (PAGE_ID)obj >> PAGE_SHIFT; //页号Span* ret &#61; (Span*)_idSpanMap.get(id);assert(ret !&#61; nullptr);return ret;
}

  • 为什么读取基数树映射关系时不需要加锁&#xff1f;

当某个线程在读取映射关系时&#xff0c;可能另外一个线程正在建立其他页号的映射关系&#xff0c;而此时无论我们用的是C&#43;&#43;当中的map还是unordered_map&#xff0c;在读取映射关系时都是需要加锁的。

因为C&#43;&#43;中map的底层数据结构是红黑树&#xff0c;unordered_map的底层数据结构是哈希表&#xff0c;而无论是红黑树还是哈希表&#xff0c;当我们在插入数据时其底层的结构都有可能会发生变化。比如红黑树在插入数据时可能会引起树的旋转&#xff0c;而哈希表在插入数据时可能会引起哈希表扩容。此时要避免出现数据不一致的问题&#xff0c;就不能让插入操作和读取操作同时进行&#xff0c;因此我们在读取映射关系的时候是需要加锁的。

而对于基数树来说就不一样了&#xff0c;基数树的空间一旦开辟好了就不会发生变化&#xff0c;因此无论什么时候去读取某个页的映射&#xff0c;都是对应在一个固定的位置进行读取的。并且我们不会同时对同一个页进行读取映射和建立映射的操作&#xff0c;因为我们只有在释放对象时才需要读取映射&#xff0c;而建立映射的操作都是在page cache进行的。也就是说&#xff0c;读取映射时读取的都是对应span的_useCount不等于0的页&#xff0c;而建立映射时建立的都是对应span的_useCount等于0的页&#xff0c;所以说我们不会同时对同一个页进行读取映射和建立映射的操作。

  • 再次对比malloc进行测试

还是同样的代码&#xff0c;只不过我们用基数树对代码进行了优化&#xff0c;这时测试固定大小内存的申请和释放的结果如下&#xff1a;

在这里插入图片描述

可以看到&#xff0c;这时就算申请释放的是固定大小的对象&#xff0c;其效率都是malloc的两倍。下面在申请释放不同大小的对象时&#xff0c;由于central cache的桶锁起作用了&#xff0c;其效率更是变成了malloc的好几倍。

在这里插入图片描述



推荐阅读
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • MPLS VP恩 后门链路shamlink实验及配置步骤
    本文介绍了MPLS VP恩 后门链路shamlink的实验步骤及配置过程,包括拓扑、CE1、PE1、P1、P2、PE2和CE2的配置。详细讲解了shamlink实验的目的和操作步骤,帮助读者理解和实践该技术。 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了Windows操作系统的版本及其特点,包括Windows 7系统的6个版本:Starter、Home Basic、Home Premium、Professional、Enterprise、Ultimate。Windows操作系统是微软公司研发的一套操作系统,具有人机操作性优异、支持的应用软件较多、对硬件支持良好等优点。Windows 7 Starter是功能最少的版本,缺乏Aero特效功能,没有64位支持,最初设计不能同时运行三个以上应用程序。 ... [详细]
  • 本文介绍了在CentOS上安装Python2.7.2的详细步骤,包括下载、解压、编译和安装等操作。同时提供了一些注意事项,以及测试安装是否成功的方法。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
author-avatar
用户q4oaa53j5h
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有