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

C++——高并发内存池设计(一)

目录

💕一、高并发内存池介绍

1.1 项目介绍

1.2 池化技术

1.3 内存池的思路

1.4 内存池主要解决的问题

1.5 malloc内存池

💕二、定长内存池的实现

2.1 如何实现 "定长"

2.2 定长内存池需要哪些成员变量?

2.3 内存池如何管理被释放的对象?

💕三、初步实现"申请内存"代码

逻辑上->:

物理实际上->:

💕四、初步实现"重复利用"内存块

💕五、先使用重复利用的内存块

5.1 如何判断有没有可重复利用内存块

5.2 如何使用可重复利用内存块

💕六、注意事项

💕七、内存对齐处理

💕八、初步的定长内存池实现代码如下->:

💕九、进一步优化

9.1 直接在堆上申请内存空间

💕十、测试功能代码

💕十一、初步的定长内存池代码最终实现

ObjectPool.h文件

Test.cpp

效果如图->: 请在Release下运行

💕十二、完结



💕一、高并发内存池介绍

1.1 项目介绍

      本项目的实现是一个高并发的内存池,它的原型是Google的一个开源项目->tcmalloc

      tcmalloc全称为Thread-Caching Malloc,即是线性缓存的malloc,它实现了高效的多线程内存管理,用于替换系统的内存分配函数malloc和free。

      tcmalloc的知名度是非常高的,很多公司都在使用它,比如Go语言就直接用它做了自己的内存分配器。

      该项目就是把tcmalloc中最核心的框架简化后拿出来,模拟实现出一个mini版的高并发内存池,目的就是学习tcmalloc的精华

      该项目主要涉及C/C++、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁等方面的技术。

1.2 池化技术

        所谓"池化技术",就是让程序向系统申请过量的资源,然后自己管理,以备不时之需 提高效率

      为什么申请过量的资源就可以提高效率 ? 

      "池"的作用是申请过量的资源,然后自己管理这些过量资源,如何自己管理?那就是当需要这个资源时直接从这个自己管理的这个池中拿。当你用完还回来时不直接还给操作系统,而是还给自己管理的这个池,进行重复利用。

      那有同学问了,这也具体看不出快在哪里啊 ! 它更快地地方在于可以少申请几次资源。

      举个例子,我们大学生的生活费是每一顿饭要一次钱,还是一次要一个月的?当然是一次要一个月的,因为一顿饭要一次钱那一个月就需要要90次饭钱!而一次要一个月的只需要要1次饭钱。

      这也正是效率提高的原因之一。


      在计算机中还有许多池的应用,如连接池,线程池,对象池等。

      以服务器上的线程池为例,它的主要思想就是->:先启动若干个线程,让它们处于睡眠状态,当接受到客户端请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求。当处理完这个请求后,让这个线程继续进入睡眠状态。


1.3 内存池的思路

      我们刚才讲了什么是池化技术和线程池的思路,内存池的思路也是如此。

      内存池是指,程序先向操作系统申请一块足够大的内存。此后,当程序中需要申请内存时,不直接向操作系统中申请,而是让内存池直接分配内存给程序。

      当程序释放内存时,也不直接还给操作系统了,而是还给内存池,进行重复利用这块空间

      当程序退出,或者达到某个特定的时间,内存池才会被真正释放,还给操作系统


1.4 内存池主要解决的问题

  根据上面提到的例子,我们不难发现

      内存池主要解决的问题是效率的问题,它能够让程序避免频繁的向操作系统申请空间和释放空间,这就节省了很多的时间

      其次,内存池还需要解决内存碎片的问题

  内存碎片分为外部碎片和内部碎片

  • 外部碎片是一些空闲的小块内存区域,由于这些内存空间不连续,以至于合计的内存足够,但是不能满足一些内存分配的申请需求。
  • 内部碎片是由于一些对齐的要求,导致分配出去的空间内部有一些无法被利用起来

  不过需要注意的是->:

  内存池尝试解决的是外部碎片的问题,同时尽可能的减少内部碎片的产生

1.5 malloc内存池

      我们在C/C++中动态申请内存并不是直接去堆上申请的,而是通过malloc函数去申请的,包括new函数也是如此,因为new的底层调用的是malloc。

      我们在申请内存时,先是调用malloc函数,然后malloc函数再去向操作系统申请内存。malloc其实就是一个内存池,因为malloc就是向操作系统申请一大份的空间,然后供给程序用。当free掉时,并不是把这份内存直接还给操作系统,而是还给malloc进行管理,之后再重复利用。

      malloc的实现方式有很多种,一般不同编译器平台用的都是不同的。比如Windows的VS系统中的malloc就是微软自行实现的,而Linux下的gcc用的是glibc中的ptmalloc


💕二、定长内存池的实现

      我们刚才讲了malloc内存池,但是malloc是一个通用的内存池,在什么场景下都可以使用,这也意味着malloc在什么场景下都不会有非常突出的性能,因为 "术业有专攻" , 也就是说malloc并不是特意针对某种情景下的

      定长内存池就是针对固定大小内存块的申请和释放的内存池。因为定长内存池只需要支持固定大小内存块的申请和释放,因此我们可以将其性能做到极致,并且在实现定长内存池时不需要考虑内部碎片等问题,因为我们申请/释放的都是固定大小的内存块,外部碎片可以很好的解决,如何解决的请看下面的实现。

2.1 如何实现 "定长"

      刚才所说,定长内存池申请/释放的都是固定大小的内存块,因此我们可以使用模板参数来规范这一行为

      1.非类型模板参数

      第一种方法是使用非类型模板参数,让所有对象来申请的内存空间大小都是N字节

template<size_t N>
class ObjectPool
{};

      2.类模板参数

      定长内存池也叫做对象池,对象池就是可以根据对象的类型大小来实现"定长"的内存开辟,因此我们可以使用类模板参数来实现这个"定长”, 比如创建定长内存池时模板T是int,那么该内存池就只支持4字节的大小内存的申请和释放。比如模板T是一个类的类型,那该内存池就只支持这个类的字节大小内存的申请和释放

template<class T>
class ObjectPool
{};

2.2 定长内存池需要哪些成员变量?

   我们首先回顾一下内存池的原理->:

     1. 向操作系统申请一块大的空间,然后自己管理,进行内存分配。

     2. 这份空间需要能重复利用

     3. 重点思考->: 如果这份空间全被使用了,我们需要再申请一份大的空间,所以我们需要一个变量来显示内存池中的内存空间还剩下多少,借此来判断是否需要再申请(何时申请下面讲)


      我们用一个变量 _memory 来定义我们申请的这块大的内存的起始地点

      在未来我们需要将这份大的内存空间进行分配,我们需要根据对象的类型进行分配,因此这块大的内存空间起始地点我们定义为 char* 是最好的,这样无论是int还是char还是double都很方便分配内存,因为char*类型的指针加几就是几

      我们在2.2目录的开头写到了一个重点思考,那就是如果在将内存池中的内存分配出去时,此时内存池剩3字节,某个对象来申请了4字节,这就导致内存池的内存不够分配出去。因此需要重新向操作系统申请内存放到内存池中。我们需要一个变量来表示剩余内存池内存的大小,这个变量就是_remainBytes


      我们如何让这份空间重复利用?

      我们在内存池中,通过链表的形式来管理使用完的内存,什么意思?

因为我们希望内存池申请出来的空间在使用完后能重复使用,那我们就必须管理起来,因为使用完后的内存空间在还回来时不一定是连续的。而且因为来申请内存空间对象的不同,导致大小不一定是相同的,所以在内存池中通过链表的方式管理这份内存

      我们将释放回来的定长内存块链接成一个链表。这里,我们将管理释放回来的内存块的链表叫做自由链表,因为它的形式是链表形式的,但是本质并不是创建了一个链表(我们后面代码会体现出来)。为了能够找到这个自由链表,我们还需要一个自由指针

具体实现我们后面讲,这里我们先了解是用 _freeList 这个指针来间接管理被归还回来的重复空间即可


因此,定长内存池当中包含三个成员变量:

  • _memory:指向大块内存的指针。
  • _remainBytes:大块内存切分过程中剩余字节数。
  • _freeList:还回来过程中链接的自由链表的头指针。

2.3 内存池如何管理被释放的对象?

      对于归还回来的定长内存块,我们可以用自由链表将其链接起来,但我们并不需要为其专门定义链式结构,我们可以让内存块的前4个字节(32位系统下)或者前8个字节(64位系统下)作为一个指针,让这个指针存储后面内存块的起始地址,这样就是一个链式结构了。

      这个绿色的区域就是存放后面内存块起始地址的地方。


      最开始时->: 我们在还没有返回定长内存块时,_freeList指针指向的是NULL,那当返回第一个定长内存块时,我们先让这个定长内存块指向NULL,然后让_freeList指向第一个定长的内存块即可。

      第二个内存块返回时->: 我们需要进行的是头插,为什么是头插?

因为头插的时间复杂度是O(1),而随着内存块的逐渐返回增多,时间复杂度就变成了O(n),所以需要进行的是头插。

      当第二个内存块返回时,先让第二个内存块指向原先的_freeList指向的内存块,然后让_freeList指向新的内存块即可。

      往后以此类推,我们会发现,第一块内存块返回时与第二块内存块返回时,其实步骤是相同的,因此在未来实现时,我们只需要都实现 "头插" 这个操作即可

      总结->: 因此在向自由链表插入被释放的内存块时,先让该内存块的前4个字节或8个字节存储自由链表中第一个内存块的地址,然后再让_freeList指向该内存块即可,也就是一个简单的链表头插操作。


我们刚才说了很多次让新来的内存块指向别的内存块,但是有一个有趣的问题?

      我们如何让内存块中前几个内存空间的一个指针,在32位系统下指向的是4个字节,在64位地址下指向的是8个字节呢?

      首先我们得知道,在32位系统下指针大小是4字节,在64位系统下指针大小是8字节。

我们在指针时学习过,指针指向数据的类型决定了指针解引用后能访问字节的大小,比如(int*)类型的指针解引用后访问的是4个字节,那么如果我们有一个指向指针的指针,把它解引用后看看大小就知道是4个字节还是8个字节了,所以我们需要用到二级指针

      为什么二级指针可以解决?

      因为我们在指针时学习过,指针的大小不是4字节就是8字节,这是因为系统的不同导致的。

      那么二级指针解引用后得到的是一级指针,这个一级指针如果是4字节那么就是32位系统下,如果是8字节,那么就是64位系统下。

      所以根据二级指针解引用,我们可以区分出来是32位系统下还是64位系统,借此完成对内存块中前几个内存空间的一个指针是指向4字节还是8字节的一个控制。

      而且这个指针是void**还是int**还是double**都无所谓,因为一级指针的地址不是4就是8


💕三、初步实现"申请内存"代码

      我们代码实现的第一步毫无疑问就是向操作系统申请内存,那么申请的内存大小我们可以自己决定,申请内存空间大小很容易实现,但是我们该如何分配出去呢?
      我们需要重新定义一个指针 obj ,我们把程序要申请的空间的起始地址定义为 obj ,然后把 obj 这个指针传给来申请内存的程序或者对象,这个程序或者对象拿到 obj 了就可以使用内存池分配给它的内存了。

        但是这个obj的类型是什么?这个obj的类型就是模板T,因为我们是按对象类型来分配内存的定长内存池。你需要int*的大小,我就给你int*的大小。你需要类的字节大小空间,我就给你类的字节大小空间。所以我们obj的类型是T*


具体实现思路如图->: 

逻辑上->:

物理实际上->:

      所谓在内存池中"申请内存",并不是把内存中的内存抠下来拿去用,而是分配这个内存池。

      _memory要不断变化,向前推进以至于 "指针obj指向的内存空间" 能够不与其他指针指向的内存重叠。  _memory不需要记录最开始的内存池的地址,因为内存池的内存空间是重复利用的,而这个内存的重复利用跟_memory没有一点关系,跟_freeList有关,因为返回回来的内存块直接交给自由链表管理起来了。  所以_memory只需要向前进即可,不需要记录起始位置也不需要回头了

      思考->:

      在设计中有一个小问题,如果内存池的剩余空间大小不够一个对象申请怎么办?比如内存池剩余3字节,而对象要申请4字节。这种情况下我们就需要重新向操作系统申请内存到内存池中


初始代码实现(后续会小幅度更改)->:

template<class T>
class ObjectPool
{public://类模板参数申请内存空间//向我申请int*类型的内存空间我就返回int*类型的T* New(){	//if (_memory == 0)		1.0版本 -> 内存池起始空间地址为0,申请地址//if(_remainBytes == 0) 2.0版本 -> 内存池中剩余字节数为0//最终版本 -> 内存块不够一个对象的空间大小时,需要重新分配资源if(_remainBytes < sizeof(T)) {_remainBytes = 128 * 1024; //开辟131072个字节大小的空间_memory = (char*)malloc(_remainBytes); if (_memory == nullptr)	//开辟空间失败{throw  std::bad_alloc();}}//如图,开辟空间后要分配空间T* obj = (T*)_memory;	//分配空间给obj_memory += sizeof(T);	//_memory向前推进_remainBytes -= sizeof(T);	//剩余内存减少return obj;}private:char* _memory = nullptr;	//内存池起始空间地址void* _freeList = nullptr;	//自由链表中的自由指针size_t _remainBytes = 0;	//内存池中剩余字节数
};

      可能有的同学不理解为什么(T*)_memory就可以实现内存块区域的分配。

      这是因为我们给它的类型是T*,那么当这个指针解引用时就只能访问一个sizeof(T)的内存大小

      这就类比于int* 类型的指针,解引用后访问的内存大小是sizeof(int)


💕四、初步实现"重复利用"内存块

      我们在目录2.3中讲到了内存池如何管理被释放的对象,实现对被释放的内存管理,主要就是将这份内存通过"头插"的形式管理起来

      我们在使用 free 函数时,是需要知道你要释放的这个内存的起始地址的,我们内存池管理释放的内存也是如此,我们需要知道被释放的内存的起始地址,才能进行管理。

      如何知道这个起始地址?我们知道,我们从内存池分配出去的内存都是有起始地址 obj 的,所以只要我们拿到这个 obj ,我们就可以进行释放。

      但是 obj 的类型我们并不确定,所以直接用 模板T 即可,你给我传的是什么类型,T就实例化出什么类型

      具体实现->:先让该内存块的前4个字节或8个字节存储自由链表中第一个内存块的地址,然后再让 _freeList 指向该内存块即可,也就是一个简单的链表头插操作。

void Delete(T* obj)
{	//头插*(void**)obj = _freeList;_freeList = obj;
}

💕五、先使用重复利用的内存块

5.1 如何判断有没有可重复利用内存块

      我们内存池的使用,不应该先一直使用内存池中的内存,如果我们有可重复利用的内存块,我们需要先使用能重复利用的内存块。

      我们如何知道自己有没有能重复利用的内存?很简单,我们对重复内存的管理是基于一个 _freeList 指针的东西的,最开始没有重复内存的使用,这个指针也是指向 nullptr 的。因此只需要判断这个指针是否为空,就知道有没有能重复利用的内存了。

5.2 如何使用可重复利用内存块

      我们知道 _freeList 这个指针是间接管理被释放的内存块的,所以我们从这个指针下手,所有被释放的内存块都在这个由 _freeList 指向的 "链表" 结构中,所以我们可以进行头删或者尾删,把内存块取出来用,我们选择头删,因为尾删的时间复杂度是O(n)

      本质上就是把头删出来的这个内存块给到 obj ,然后把 obj 返回回去

      如何进行头删取出这个内存块?  先记录第一个内存块 _freeList 指向的下一个内存块,我们把 _freeList 指向的下一个内存块的起始地址定义为 next 指针,然后把 _freeList 当前指向的内存块作为 obj ,然后把 _freeList 更改为 next 即可。

初步修改后整体代码->:

template<class T>
class ObjectPool
{public://类模板参数申请内存空间//向我申请int*类型的内存空间我就返回int*类型的T* New(){T * obj;if (_freeList){void* next = *(void**)_freeList;obj = (T*)_freeList;_freeList = next;}//if (_memory == 0)		1.0版本 -> 内存池起始空间地址为0,申请地址//if(_remainBytes == 0) 2.0版本 -> 内存池中剩余字节数为0//最终版本 -> 内存块不够一个对象的空间大小时,需要重新分配资源else {if (_remainBytes < sizeof(T)){_remainBytes = 128 * 1024; //开辟131072个字节大小的空间_memory = (char*)malloc(_remainBytes);if (_memory == nullptr)	//开辟空间失败{throw  std::bad_alloc();}}//如图,开辟空间后要分配空间obj = (T*)_memory;	//分配空间给obj_memory += sizeof(T);	//_memory向前推进_remainBytes -= sizeof(T);	//剩余内存减少return obj;}}void Delete(T* obj){	//头插*(void**)obj = _freeList;_freeList = obj;}private:char* _memory = nullptr;	//内存池起始空间地址void* _freeList = nullptr;	//自由链表中的自由指针size_t _remainBytes = 0;	//内存池中剩余字节数
};

💕六、注意事项

      需要注意的是,在释放对象时,我们应该显示调用该对象的析构函数清理该对象,因为该对象可能还管理着其他某些资源,如果不对其进行清理那么这些资源将无法被释放,就会导致内存泄漏。

      原因正如我们之前所讲,这里再讲一次

你来申请的内存要int*的指针,我就给你int*的指针。你来申请的内存要类的类型的指针,我就给你类类型的指针。

      那么你来申请的内存要的是类的类型时,是需要显示调用它的构造函数和析构函数的。

显示调用构造函数原因->:  给它的类的成员变量进行初始化。       

显示调用析构函数原因->:  因为这块空间可能被别的自定义类型使用过了,需要调用它的类的析构函数来释放掉对象内部管理的其他内存。

      总之都是为了安全性和避免内存泄漏

//管理释放的内存块
void Delete(T* obj)
{	//显示调用T的析构函数清理对象obj->~T();//调用obj指向对象的析构函数//头插*(void**)obj = _freeList;_freeList = obj;
}

      因为我们内存池的作用是分配内存块出去,并不能直接影响到obj指向对象的内容,所以需要调用obj指向对象的析构函数和构造函数。

      在return obj前面显示调用构造函数

				obj = (T*)_memory;	//分配空间给obj_memory += sizeof(T);	//_memory向前推进_remainBytes -= sizeof(T);	//剩余内存减少// 定位new,显示调用T的构造函数初始化new(obj)T;//调用obj指向对象的析构函数return obj;}}

💕七、内存对齐处理

      我们的每一个内存块前4个字节或者前8个字节要存放下一个内存块的地址,但如果我们申请的内存块不到4个字节或者8个字节呢?

      这该怎么办?

      所以,我们需要处理,确保每个内存块至少能存储一个指针,防止小对象无法加入自由链表

      否则就会导致无法回收内存,造成内存浪费

      因此,我们在给对象内存块之前,要先判断它申请内存块的大小够不够4个字节或者8个字节。

如果够,就正常该申请多少就该多少。

如果不够,就强制申请4个字节或8个字节的空间。

      我们可以定义一个新的变量 size_t  objSize,这个变量用于决定申请多少内存

//判断下是否够4个字节或8个字节
//如果不够,那objSize就是4个字节或8个字节。否则正常就是T
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);

      我们可以进一步把代码优化,我们看到如果不够4字节或8字节就多申请内存,如果够就正常申请T字节的大小

      但是我们看这三行代码(原文代码在 return obj 前面)

//如图,开辟空间后要分配空间
obj = (T*)_memory;	//分配空间给obj
_memory += sizeof(T);	//_memory向前推进
_remainBytes -= sizeof(T);	//剩余内存减少

      我们知道申请内存在物理上的本质,如下图->:

      我们的 _memory 是用来控制 obj 申请内存的起始地址的,所以,当开辟的地址需要强制多申请内存空间时,我们不需要移动obj的位置,我们直接向后移动 _memory ,让下一个对象的 obj 指向的内存不存在重复,这就是强制多申请内存!

      我们刚才说如果需要强制多申请就强制多申请,否则就该申请多少是多少,这两个事情的赋值都给了 ObjSize ,所以我们的 _memory ,其实加上 ObjSize 就够了, _remainBytes 同理。因为这两个不是 sizeof(T) 的大小,就是强制的多申请内存大小


加入 ObjSize 的 New() 代码

template<class T>
class ObjectPool
{public://类模板参数申请内存空间//向我申请int*类型的内存空间我就返回int*类型的T* New(){	T * obj;//存在可重复利用的内存块if (_freeList){void* next = *(void**)_freeList;obj = (T*)_freeList;_freeList = next;}//if (_memory == 0)		1.0版本 -> 内存池起始空间地址为0,申请地址//if(_remainBytes == 0) 2.0版本 -> 内存池中剩余字节数为0//最终版本 -> 内存块不够一个对象的空间大小时,需要重新分配资源else {if (_remainBytes < sizeof(T)){_remainBytes = 128 * 1024; //开辟131072个字节大小的空间_memory = (char*)malloc(_remainBytes);if (_memory == nullptr)	//开辟空间失败{throw  std::bad_alloc();}}//判断下是否够4个字节或8个字节//如果不够,那objSize就是4个字节或8个字节。否则正常就是Tsize_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);//如图,开辟空间后要分配空间obj = (T*)_memory;	//分配空间给obj_memory += objSize;	//_memory向前推进_remainBytes -= objSize;	//剩余内存减少// 定位new,显示调用T的构造函数初始化new(obj)T;//调用obj指向对象的析构函数return obj;}}

💕八、初步的定长内存池实现代码如下->:

#pragma once
#include<iostream>
using std::cout;
using std::endl;template<class T>
class ObjectPool
{public://类模板参数申请内存空间//向我申请int*类型的内存空间我就返回int*类型的T* New(){	T * obj;//存在可重复利用的内存块if (_freeList){void* next = *(void**)_freeList;obj = (T*)_freeList;_freeList = next;}//if (_memory == 0)		1.0版本 -> 内存池起始空间地址为0,申请地址//if(_remainBytes == 0) 2.0版本 -> 内存池中剩余字节数为0//最终版本 -> 内存块不够一个对象的空间大小时,需要重新分配资源else {if (_remainBytes < sizeof(T)){_remainBytes = 128 * 1024; //开辟131072个字节大小的空间 = 128KB_memory = (char*)malloc(_remainBytes);if (_memory == nullptr)	//开辟空间失败{throw  std::bad_alloc();}}//判断下是否够4个字节或8个字节//如果不够,那objSize就是4个字节或8个字节。否则正常就是Tsize_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);//如图,开辟空间后要分配空间obj = (T*)_memory;	//分配空间给obj_memory += objSize;	//_memory向前推进_remainBytes -= objSize;	//剩余内存减少// 定位new,显示调用T的构造函数初始化new(obj)T;//调用obj指向对象的析构函数return obj;}}//管理释放的内存块void Delete(T* obj){	//显示调用T的析构函数清理对象obj->~T();//调用obj指向对象的析构函数//头插*(void**)obj = _freeList;_freeList = obj;}private:char* _memory = nullptr;	//内存池起始空间地址void* _freeList = nullptr;	//自由链表中的自由指针size_t _remainBytes = 0;	//内存池中剩余字节数
};

      至此,我们就是实现了一个定长的内存池。


💕九、进一步优化

      接下来是优化部分,到目录八为止我们已经初步实现了定长内存池的实现,并可以正常运用。

9.1 直接在堆上申请内存空间

      我们在上面所讲,我们申请空间不是直接上堆区申请的,是通过函数malloc来申请的,malloc去操作系统申请,操作系统把堆区内容分配给你。

      有一种方法可以让我们直接向操作系统申请内存空间,避免了中间人malloc

      我们需要这样一个头文件

#ifdef _WIN32
#include<windows.h>
#else
// 
#endif

      ​#ifdef _WIN32​:这是一个预处理指令,意思是"如果定义了_WIN32这个宏"

  • 编译器在Windows平台上通常会​​自动定义​​_WIN32这个宏

      ​#else​:如果不是在Windows平台下编译(即在Linux、macOS等其他系统下)

  #endif:结束条件编译块

      我们想要直接向操作系统申请内存空间我们需要这样写

// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else// linux下brk mmap等
#endif//检测是否申请成功if (ptr == nullptr)throw std::bad_alloc();return ptr;
}

      内联函数是因为可以提高效率,代码很短,可以内联函数展开。省去了参数压栈和call调用的时间。

   MEM_COMMIT | MEM_RESERVE->:  完成地址空间保留和物理内存分配

   PAGE_READWRITE ->:   允许对该内存区域进行读取和写入操作

   第一个参数->:  传入0 是让操作系统​​自动选择​​合适的起始地址

   第二个参数->:  在Windows内存系统中 "1页" 代表4KB。<< 13代表2字节的13次方,也就是8192字节也就是8KB = 2页。

      最终开辟的内存空间时kpage * 2页,这个kpage由我们自由决定,传参过去


      所以我们开辟空间时也可以这样写,这样省去了调用malloc,直接去堆区申请内存,效率进一步提高了

if (_remainBytes < sizeof(T))
{_remainBytes = 128 * 1024; //开辟131072个字节大小的空间 = 128KB//_memory = (char*)malloc(_remainBytes);_memory = (char*)SystemAlloc(_remainBytes >> 13); //128KB >> 13 = 131072 / 8192 = 16//所以传过去的 kpage 是16 ,最终开辟16*2页 = 16 * 8KB = 128KB 的内存if (_memory == nullptr)	//开辟空间失败{throw  std::bad_alloc();}
}

💕十、测试功能代码

      测试代码是一个利用二叉树结构进行测试效率

      在我们的代码基础上直接可用,同样写到.h文件中

//测试功能
//二叉树结构进行检测
struct TreeNode
{int _val;TreeNode* _left;TreeNode* _right;TreeNode():_val(0), _left(nullptr), _right(nullptr){}
};void TestObjectPool()
{// 申请释放的轮次const size_t Rounds = 5;// 每轮申请释放多少次const size_t N = 100000;//测试原始malloc和free效率std::vector<TreeNode*> v1;v1.reserve(N);size_t begin1 = clock();for (size_t j = 0; j < Rounds; ++j){for (int i = 0; i < N; ++i){v1.push_back(new TreeNode);}for (int i = 0; i < N; ++i){delete v1[i];}v1.clear();}size_t end1 = clock();//测试高并发内存池效率std::vector<TreeNode*> v2;v2.reserve(N);ObjectPool<TreeNode> TNPool;size_t begin2 = clock();for (size_t j = 0; j < Rounds; ++j){for (int i = 0; i < N; ++i){v2.push_back(TNPool.New());}for (int i = 0; i < N; ++i){TNPool.Delete(v2[i]);}v2.clear();}size_t end2 = clock();//输出结果cout << "new cost time:" << end1 - begin1 << endl;cout << "object pool cost time:" << end2 - begin2 << endl;
}

💕十一、初步的定长内存池代码最终实现

ObjectPool.h文件

#pragma once
#include<iostream>
using std::cout;
using std::endl;
#include<vector>
#include <time.h>#ifdef _WIN32
#include<windows.h>
#else
// 
#endif// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else// linux下brk mmap等
#endif//检测是否申请成功if (ptr == nullptr)throw std::bad_alloc();return ptr;
}template<class T>
class ObjectPool
{public://类模板参数申请内存空间//向我申请int*类型的内存空间我就返回int*类型的T* New(){	T * obj;//存在可重复利用的内存块if (_freeList){void* next = *(void**)_freeList;obj = (T*)_freeList;_freeList = next;}//if (_memory == 0)		1.0版本 -> 内存池起始空间地址为0,申请地址//if(_remainBytes == 0) 2.0版本 -> 内存池中剩余字节数为0//最终版本 -> 内存块不够一个对象的空间大小时,需要重新分配资源else {if (_remainBytes < sizeof(T)){_remainBytes = 128 * 1024; //开辟131072个字节大小的空间 = 128KB//_memory = (char*)malloc(_remainBytes);_memory = (char*)SystemAlloc(_remainBytes >> 13); //128KB >> 13 = 131072 / 8192 = 16//所以传过去的 kpage 是16 ,最终开辟16*2页 = 16 * 8KB = 128KB 的内存if (_memory == nullptr)	//开辟空间失败{throw  std::bad_alloc();}}//判断下是否够4个字节或8个字节//如果不够,那objSize就是4个字节或8个字节。否则正常就是Tsize_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);//如图,开辟空间后要分配空间obj = (T*)_memory;	//分配空间给obj_memory += objSize;	//_memory向前推进_remainBytes -= objSize;	//剩余内存减少// 定位new,显示调用T的构造函数初始化new(obj)T;//调用obj指向对象的析构函数return obj;}}//管理释放的内存块void Delete(T* obj){	//显示调用T的析构函数清理对象obj->~T();//调用obj指向对象的析构函数//头插*(void**)obj = _freeList;_freeList = obj;}private:char* _memory = nullptr;	//内存池起始空间地址void* _freeList = nullptr;	//自由链表中的自由指针size_t _remainBytes = 0;	//内存池中剩余字节数
};//测试功能
//二叉树结构进行检测
struct TreeNode
{int _val;TreeNode* _left;TreeNode* _right;TreeNode():_val(0), _left(nullptr), _right(nullptr){}
};void TestObjectPool()
{// 申请释放的轮次const size_t Rounds = 5;// 每轮申请释放多少次const size_t N = 100000;//测试原始malloc和free效率std::vector<TreeNode*> v1;v1.reserve(N);size_t begin1 = clock();for (size_t j = 0; j < Rounds; ++j){for (int i = 0; i < N; ++i){v1.push_back(new TreeNode);}for (int i = 0; i < N; ++i){delete v1[i];}v1.clear();}size_t end1 = clock();//测试高并发内存池效率std::vector<TreeNode*> v2;v2.reserve(N);ObjectPool<TreeNode> TNPool;size_t begin2 = clock();for (size_t j = 0; j < Rounds; ++j){for (int i = 0; i < N; ++i){v2.push_back(TNPool.New());}for (int i = 0; i < N; ++i){TNPool.Delete(v2[i]);}v2.clear();}size_t end2 = clock();//输出结果cout << "new cost time:" << end1 - begin1 << endl;cout << "object pool cost time:" << end2 - begin2 << endl;
}

Test.cpp

#define _CRT_SECURE_NO_WARNINGS #include"ObjectPool.h"
int main()
{TestObjectPool();
}

效果如图->: 请在Release下运行

      可以看见运行速率非常快


💕十二、完结


文章转载自:

http://WQpD0pv5.qznkn.cn
http://bCvTArUo.qznkn.cn
http://CF3ZU6LE.qznkn.cn
http://NbKODmzp.qznkn.cn
http://57zg42oe.qznkn.cn
http://cociPJJZ.qznkn.cn
http://kqUkoOKV.qznkn.cn
http://4BKQmWPw.qznkn.cn
http://jF3yufHN.qznkn.cn
http://ElZHY26O.qznkn.cn
http://UFx2aVGY.qznkn.cn
http://8Wlx4N3q.qznkn.cn
http://LSOpAjdt.qznkn.cn
http://hxPl3IiG.qznkn.cn
http://swUScV9Z.qznkn.cn
http://Cmp4nnGD.qznkn.cn
http://UNtTGUSM.qznkn.cn
http://fBsVOvWE.qznkn.cn
http://FsjvsQls.qznkn.cn
http://AtYzAaWi.qznkn.cn
http://Sk5GFVAP.qznkn.cn
http://DbUHqtIp.qznkn.cn
http://IWeTIHAo.qznkn.cn
http://dCvVnNDi.qznkn.cn
http://DplTE0c5.qznkn.cn
http://HlkkAks5.qznkn.cn
http://hw7t9jOn.qznkn.cn
http://A98zWCmd.qznkn.cn
http://cl9iKguh.qznkn.cn
http://sh1Y7Xae.qznkn.cn
http://www.dtcms.com/a/385043.html

相关文章:

  • 快速入门HarmonyOS应用开发(一)
  • 深入解析 `pthread_detach`:告别线程的优雅之道
  • Arduino 通讯接口全景解析:从硬件到软件的跨板对话艺术
  • Python3练习题
  • AI GEO 实战:借百度文小言优化,让企业名称成搜索热词
  • 数字图像处理(1)OpenCV C++ Opencv Python显示图像和视频
  • 《拆解Unity开发顽疾:从UI渲染异常到物理交互失效的实战排障手册》
  • 安装和初始化配置Git
  • 蓝牙BLE调制端GFSK高斯滤波查表设计原理
  • PPO算法-强化学习
  • Spring Boot 实战:优雅地将配置文件映射为Java配置类并自动注入
  • ADC转换原理与应用详解
  • 第五章 搭建ZYNQ视频图像处理系统——软件设计
  • Chapter6—原型模式
  • Java字符串操作:从入门到精通
  • 如何科学评估CMS系统性能优化效果?
  • 批量更新和批量插入,内含jdbc工具类
  • 3D地球可视化教程 - 第2篇:夜晚纹理与着色器入门
  • Ajax笔记2
  • DDoS高防IP是什么? DDoS攻击会暴露IP吗?
  • Java 设计模式——原则:从理论约束到项目落地指南
  • 从零开始打造个性化浏览器导航扩展:极简高级风格设计
  • 软件包安装
  • QARM:Quantitative Alignment Multi-Modal Recommendation at Kuaishou
  • 通达信抓波段指标(主图)
  • Django基础环境入门
  • Java学习笔记2——简单语法
  • LLM-LLM大语言模型快速认识
  • Winogender:衡量NLP模型性别偏见的基准数据集
  • Oracle UNDO表空间使用率过高解决方案