002 高并发内存池_定长内存池设计
前言
我们知道申请内存使用的是malloc,malloc几乎在什么场景下都可以用,意味着什么场景下都不会有很高的性能,下面我们就先来设计一个定长内存池做个开胃菜,学习它目的有两层,先熟悉一下简单内存池是如何控制的,第二它会作为我们后面内存池的一个基础组件。
一、设计整体框架
既然是内存池,那么我们首先得向系统申请一块内存空间,然后对其进行管理。要想直接向堆申请内存空间,在Windows下,可以调用VirtualAlloc函数;在Linux下,可以调用brk或mmap函数。_memory指针管理的就是我们向系统申请的一大块内存
1、 对于向堆申请到的大块内存,我们可以用一个指针来对其进行管理,但仅用一个指针肯 定是不够的,我们还需要用一个变量来记录这块内存的长度
size_t _SurplusBytes(剩余)
2、我们需要将这块内存进行切分,为了方便切分操作,指向这块内存的指针最好是字符指针,因为指针的类型决定了指针向前或向后走一步有多大距离,对于字符指针来说,当我们需要分配出去n个字节的空间时,直接对字符指针进行加n操作即可。
char* _memory
3、释放回来的定长内存块也需要被管理,我们可以将这些释放回来的定长内存块链接成一个链表,这里我们将管理释放回来的内存块的链表叫做自由链表,为了能找到这个自由链表,我们还需要一个指向自由链表的指针。
void* _freeList
这样我们所需的三个成员变量就定义好了
另外地,我们还需要引进一个概念:自由链表(这个结构我们在new与delete操作都会用到)
自由链表(free_List)是一种数据结构,通常用于管理可用内存块的列表。在动态内存分配中,当需要分配内存时,可以从自由链表中获取一个可用的内存块,当内存不再需要时,将其释放并加入到自由链表中。头部存储下一个节点的地址
整体代码框架如下:
#ifdef _WIN32
#include <Windows.h>
#else
#endif
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* 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:
//申请空间
T* New()
{}
//将空间释放还回到自由链表中
void Delete(T* obj)
{}
private:
char* _memory = nullptr; // 指向内存块的指针
size_t _SurplusBytes = 0; // 内存块中剩余字节数
void* _freeList = nullptr; // 管理还回来的内存对象的自由链表的头指针
};
对代码进行补充说明
既然是内存池,那么我们首先得向系统申请一块内存空间,然后对其进行管理。要想直接向堆申请内存空间,在Windows下,可以调用VirtualAlloc函数;在Linux下,可以调用brk或mmap函数。
当前环境是Windows(_WIN32定义为真),我们则使用WindowsAPI中的VirtualAlloc函数来分配内存。VirtualAlloc函数可以用来将一段虚拟地址空间分配给进程,并将其映射到物理内存上。在具体的使用上我们直接用封装过的SystemAlloc分配内存即可。
二、New操作(申请空间)
当我们申请对象的时候,内存池应该优先将释放回来的空间重复利用(自由链表中的一个个内存块),这个操作是一个头删的操作
if (_freeList)
{
obj = (T*)_freeList;
//指向下一个
_freeList = *(void**)_freeList;
}
如果自由链表的头指针_free_List为空,则应该由堆上申请的大块内存块中分配,具体操作:让_memory加等上OBJSIZE,让surplusBytes减等上OBJSIZE即可
这样我们的New操作就搞定了
T* New()
{
T* obj = nullptr; //定义指向所申请空间的指针
//从自由链表中申请
if (_freeList)
{
obj = (T*)_freeList;
//指向下一个
_freeList = *(void**)_freeList;
}
else
{
//扩容:如果剩余的内存块大小少于所申请的空间大小
if (_SurplusBytes < sizeof(T))
{
_SurplusBytes = 128 * 1024;
_memory = (char*)SystemAlloc(_SurplusBytes>>13);
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
obj = (T*)_memory;
size_t OBJSIZE = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
//分配空间
_memory += OBJSIZE;
_SurplusBytes -= OBJSIZE;
}
//对开辟好空间的obj进行初始化
//(使用定位new操作符对已经分配好的内存空间进行对象的构造(初始化)操作。当我们使用new关键字创建一个对象时,会在内存中分配一块空间来存储对象的成员变量,并调用对象的构造函数来对这块内存进行初始化。但是,在某些情况下,我们可能已经手动分配了内存空间,这时候就需要使用定位new操作符来调用对象的构造函数,以便正确地初始化这块内存空间。)
new(obj)T;
return obj;
}
重点重点重点!!!
_freeList = *(void**)_freeList;
*(void**)是什么意思呢,为什么要这么写?
那就要从定长池的设计讲起了。在实现定长内存池时要做到“定长”有很多种方法,比如我们可以使用非类型模板参数,使得在该内存池中申请到的对象的大小都是N。定长内存池也叫做对象池,在创建对象池时,对象池可以根据传入的对象类型的大小来实现“定长”,因此我们可以通过使用模板参数来实现“定长”,比如创建定长内存池时传入的对象类型是int(32位下),那么该内存池就只支持4字节大小内存的申请和释放。
这就有一个问题了,假如在64位机器下,一个指针的大小是8字节,但是用户申请了1字节的空间,这时我们需要将空间扩为八字节再返回给用户,因为一旦这份空间被返回,1字节是无法当成指针使用存储下一个节点地址的!!!
当自由链表为空时,此时我们会从大块内存中分配空间,
当空间回收时,我们会用自由链表管理这份空间
_freeList = *(void**)_freeList;
解析:在32位下 *(void**)看的是_freeList头部四个字节,在64位下
看的是_freeList头部八个字节,这样我们就可以在每个节点头部存储下一个节点的地址,然后就可以把节点链接起来
以下这个代码是自由链表为空时,向大块内存中申请
疑问:为什么我们在大块内存中申请也是需要划定好固定大小呢?
size_t OBJSIZE = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
因为New操作的本质就是申请一块固定大小的空间,但是当这一块固定大小的空间被释放后,我们是需要重新拿来继续使用的(也是为了让自由链表的节点能够链接起来)
毕竟我们是不能确定机器是32位还是64位下的,当申请的空间小于32/64位下的指针,那么就开这个指针的大小,如果大于32/64位下指针的大小,那么就申请开辟空间的大小赋值给OBJSIZE
补充:当然你这里可以不用void,选择int一样可以
三、Delete操作(用自由链表管理释放的空间)
我们使用头插操作对已返还的空间做管理(当然尾插也行,只是还要找尾(ㄒoㄒ)~)
void Delete(T* obj)
{
obj->~T();
// 头插到freeList ,指向下一个
*((void**)obj) = _freeList;
_freeList = obj;
}
四、测试性能
比较在申请和释放大量TreeNode对象时,直接使用new和delete操作与使用对象池(ObjectPool)的效率差异
struct TreeNode
{
int _val;
TreeNode* _left;
TreeNode* _right;
TreeNode()
:_val(0)
, _left(nullptr)
, _right(nullptr)
{}
};
void TestObjectPool()
{
// 申请释放的轮次
const size_t Rounds = 3;
// 每轮申请释放多少次
const size_t N = 100000;
size_t begin1 = clock();
std::vector<TreeNode*> v1;
v1.reserve(N);
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();
//使用定长内存池
ObjectPool<TreeNode> TNPool;
size_t begin2 = clock();
std::vector<TreeNode*> v2;
v2.reserve(N);
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;
}
小结
今日的项目分享就到这里啦,如果本文存在疏漏或错误的地方还请您能够指出!