一、什么是内存池?
从堆上一次性开辟一大片空间,由我们自己管理,这里的一大片的空间我们就将它称为池,当我们需要拿空间的时候去这个池中拿取
二、为什么要有内存池?有什么好处?
我们知道如果要开辟一块内存需要去调用malloc向内核操作系统要内存,每次切换去要找操作系统要是有消耗的,一次两次还可以,次数多了这个消耗则越大。于是就有人相出了内存池,向内核一次性要一大片空间,这片空间由我们自己管理,每次要空间时先向这个池子里要,如果池子里不够了再次向内核申请一大片的空间,这样访问内核的次数就减少了,大大提升了效率,节约了时间
三、如何管理内存池?
拿对象所需要的空间
这里我们使用char* _memory来控制,当一个需要一个obj的对象时,_memory的地址就会指向_memory+sizeof(obj),将obj对象要的空间腾出来,obj就将这段空间拿走。
释放对象时归还空间管理
当对象释放时并不是free掉归还给操作系统,而是由我们控制将其维护起来,当进程结束时,我们才将所有的空间归还给操作系统,用一个指针void* _freelist来作为头指针控制归还的内存,当一个内存归还时用单链表的形式将这些内存链接起来
如何将这些内存链接起来?
我们将对象的头4个字节(64位就是8个字节)作为存储地址的容器,这样就可以链接起来了。
如果创建的对象的容量小于4个字节(64位8个字节),怎么办?
特殊处理一下,我们将对象所需的内存给对象,同时留出对象内存加扩充到4个字节(64位8个字节),用完后释放的时候这4个字节(64位8个字节)就可以充当链条链接。
拿空间时优先看freelist链表内是否有资源,有就先从freelist中取资源
四、代码实现
第一部分创建定长内存池
char* _memory = nullptr;
void* _freelist = nullptr;
int remain_memory = 0;
char* _memory 为什么要用char呢?因为好取,char是最小的字节类型,不管什么类型都可以取到对应的大小
void* _freelist 用于链接释放后的内存空间
int remain_memory 记录内存池剩余容量
创建一个定长内存池代码
T* New()
{
T* obj = nullptr;
//判断freelist是否为空的不为空的那么就先从这个列表中取
if (_freelist)
{
//头删
void* next = *(void**)_freelist;
obj = (T*)_freelist;
_freelist = next;
}
else
{//freelist为空
if (remain_memory<sizeof(T))
{
//如果剩余的容量小于一个对象的大小就要开辟新的池子了
remain_memory = 128 * 1024;
_memory = (char*)malloc(remain_memory*sizeof(char));
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
obj = (T*)_memory;
_memory += sizeof(T);
remain_memory -= sizeof(T);
}
new (obj)T;
return obj;
}
解释:
1.当freelist内有释放出来的内存,那么先去使用freelist管理的内存去创建对象,头删的方式从freelist链表中取出一个对象
2.如果freelist内没有了或者剩余的内存不够创建一个对象,那么就从剩余的内存池中取,创建对象返回
3.如果内存池中也没有了,那么就重新向系统要
注意:new (obj)T; 定位new ,去调用对象的初始化函数
第二部分释放
void Delete(T*obj)
{
//头插
*(void**)obj = _freelist;
_freelist = obj;
}
释放一个对象时,相当于将这段空间交给freelist管理,头插给连起来
注:*(void**)obj,为在32或者64位下都有准确的空间存储链接地址,void**obj是一个二级指针,解引用后就是一个一级指针,并且32位系统或者64位系统都可以使用
第三部分,其它考虑
如果我们要创建的对象小于一个指针的大小,那么这个时候我们释放了对象后,对象里的内存并不够存一个地址这个时候怎么办呢?
因为我们要的内存池是连续的一片空间,将对象分配后留出指针大小-对象大小的空间,留给freelist后面使用
obj = (T*)_memory;
size_t objectsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += objectsize;
_remain_memory -= objectsize;
整体代码和测试如下
#include<iostream>
#include<vector>
#include<time.h>
using std::cout;
using std::endl;
#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()
{
T* obj=nullptr;
//如果回收列表非空,就先从这个列表中取空间给对象
if (_freelist)
{
//头删
void* next = *(void**)_freelist;//将freelist强转成二级指针解一次引用
obj = (T*)_freelist;
_freelist = next;
}
else
{
//如果剩余的内存小于我要构造的对象了,就要去问OS要
//初始化的时候也需要去申请内存
if (_remain_memory < sizeof(T))
{
_remain_memory =128 * 1024;
//_memory = (char*)malloc(_remain_memory);//用malloc来开空间
_memory = (char*)SystemAlloc(_remain_memory >> 13);//跳过malloc直接调用win下的系统调用
if (_memory == nullptr)
{
throw std::bad_alloc();//抛异常不会记得去学
}
}
obj = (T*)_memory;
size_t objectsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += objectsize;
_remain_memory -= objectsize;
}
new (obj)T;
return obj;
}
//还回来一个对象,我们销毁去销毁,但不是真的销毁,还回来的用freelist去链接起来
void Delete(T* obj)
{
//这是第一次回收
obj->~T();
//if(_freelist == nullptr)
//{
// _freelist = obj;
// //32位是对的 但是64位就跑不动了
// //*(int*)obj = nullptr;
// *(void**)obj = nullptr;//不管是32和64都可以跑了,void**二级指针解引用就是一个地址,取一个地址的大小
//}
//头插
*(void**)obj = _freelist;
_freelist = obj;
}
private:
char* _memory = nullptr;
int _remain_memory = 0;
void* _freelist = nullptr;
};
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 = 1000000;
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;
}
解释:
1.我们要使用原生的操作系统的开辟空间的API就需要去查找对应的接口,这里windows的接口是VirtualAlloc,并且windows开辟空间是按页开辟,一页为8kb,所以有上面Systemalloc函数封装了一下
2.后面的代码为测试代码,不断地创建对象又释放对象看看谁更快
结果为我们做的内存池的速度比系统的快,数值越大则差距也越大
因为我们做的是只针对一个类型的内存池,系统则需要照顾全局,所以定长内存池是有缺陷的只能针对一种类型