1.什么是内存池
1.1 池化技术
将程序中需要经常使用的核心资源先申请出来,放在一个池内,由程序自己管理,这样可以提高资源的使用效率,也可以保证本程序占有的资源数量。
比如之前博文实现的线程池,就是预先的申请出来一些线程,当有任务被推送到任务队列时,线程池内的线程立即开始处理任务,不需要在程序内一次次的创建线程、关闭线程等。
1.2 内存池
普通场景下,当程序长时间运行,或多或少有相关内容会去申请内存资源,由于这些申请的内存块大小不一,会造成大量的内存碎片从而降低程序和操作系统的效率。内存池就是在使用内存空间之前,先整体申请分配一大块内存(内存池),当需要申请内存时,从内存池中取出一块进行动态分配,当该内存被释放时,将释放过后的内存在放回池内,并尽量与周边的空闲内存块合并,以减少外内存碎片,重复利用。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。
1.2.1 内存池的意义
有两个好处:
1、由于向内存申请的内存块都是比较大的,所以能够降低外碎片问题。
2、一次性向内存申请一块大的内存慢慢使用,避免了频繁的向内存请求内存操作,提高内存分配的效率。
1.2.2 内存碎片
外内存碎片
系统经过一系列的分配内存和回收内存,当遇到需要分配一大块内存空间时,剩余内存空间的总数够,但是内存空间不连续,导致无法分配。
内内存碎片
内部碎片是指一个已分配的块比有效载荷大时发生的。(假设以前分配了10个大小的字节,现在只用了5个字节,则剩下的5个字节就会内碎片)。内部碎片的大小就是已经分配的块的大小和他们的有效载荷之差的和。因此内部碎片取决于以前请求内存的模式和分配器实现的模式。
2. 定长内存池
在实现高并发内存池之前,先写一个假定每次申请的内存空间都是固定值的内存池,即定长内存池。
介绍:
实现一个 FreeList,每个 FreeList 用于分配固定大小的内存块,比如用于分配 32字节对象的固
定内存分配器等,使用模板,使定长内存池可以根据分配的对象而发生改变。定长内存池中有两个指针, _memory和_freeList 。_memory是指向大块内存的指针,当需要分配内存的时候,他根据(模板类型)字节数,向后移动,将该块内存分配出去。_freeList是指向由还回的内存块的链接而成的链表的头指针。分配时如果内存池中剩余不够,我们就再创建一个新的大块,不必保存前一块大内存的地址,因为如果它始终被占用,申请空间时就用不上它,如果它后续被释放,也会进入_freeList,供我们再次使用。
2.1 定长内存池——回收内存
先上框架
template<class T>
class ObjectPool
{
public:
//分割内存块
T* New()
{}
//回收内存
void Delete(T* obj) //obj所指向的这一块空间被回收了
{}
private:
char* _memory = nullptr;//指向大块内存的指针
size_t _remainBytes = 0;//大块内存存在切分过程中的剩余字节数
void* _freeList = nullptr;//还回来的内存块由_freeList指针指向的链表链接起来
};
由于每个内存块回收后都是放到_freeList里面,形成一个单链表。所以内存块中的起始一部分空间需要变成 一个指针,指向下一个节点。
- 情况一:当单链表为空的时候,回收一个内存块。首先需要将当前内存块中的第一个指针大小的空间置成nullptr。然后使指向当前内存块的指针作为单链表的头结点。
- 情况二:当单链表不为空的时候,回收一个内存块。首先如果使用尾插法,那么每次都需要遍历一遍链表,时间复杂度高,所以我们采用头插法。先让当前内存块中的第一个指针大小的空间的置成单链表的头结点,然后将头结点赋给指向当前内存块的指针
问题:我们要想将归还的内存块使用一张链表链接起来,那么这个内存块一定需要大于一个指针的大小!不然我们无法修改内存块的前指针变量个大小的字节,使其指向下一块归还的内存。
但是在32位和64位平台下,指针的大小是不一致的。我们怎么知道使用者处于哪个平台?
针对于这一问题,本质是由于指针变量的大小不确定。但是真正处于某一平台下,指针的大小是唯一的。所以我们不应该显示的规定将前4个或8个字节进行修改,而应该使用一种可以依平台而定的修改方案,即按照当前平台的指针变量大小作为需要修改的字节数。
采用解引用 (void**)obj || (int**)obj || || (char**)obj
而非依平台而定解引用 (int*)obj || (long long*)obj
void Delete(T* obj) //obj所指向的这一块空间被回收了
{
/*
if (_freelist == nullptr)
{
*(void**)obj = nullptr;
_freelist = obj;
}
else
{
*(void**)obj = _freelist;
_freelist = obj;
}发现_freeList是空的时候依然符合 else 所以合并一下
*/
//要回收了 拿析构函数清理一下
obj->~T();
* (void**)obj = _freeList;
_freeList = obj;
}
2.2 定长内存池——分割内存
template<class T>
class ObjectPool
{
public:
T* New()
{
//当剩余的字节数小于需要的字节数,就重新申请一个新的大空间
T* obj = nullptr;
//优先使用换回来的内存块对象,重复利用
if (_freeList)
{
void* next = *(void**)_freeList;//_freeList的前指针大小个字节 解引用为next
obj = (T*)_freeList;
_freeList = next;
}
else
{
if (_remainBytes < sizeof(T))
{
_remainBytes = 128 * 1024;
_memory = (char*)malloc(128 * 1024);
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
obj = (T*)_memory;
_memory += sizeof(T);
_remainBytes -= sizeof(T);
}
//空间有了 初始化一下 定位new
new(obj)T;
return obj;
}
//归还内存
void Delete(T* obj)
{}
private:
char* _memory = nullptr;//指向大块内存的指针
size_t _remainBytes = 0;//大块内存存在切分过程中的剩余字节数
void* _freeList = nullptr;//还回来的内存块由_freeList指针指向的链表链接起来
};
2.3 测试代码及结果
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;
std::vector<TreeNode*> v1;
v1.reserve(N);
size_t begin1 = clock();//记录使用new和delete场景下的开始时间
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();//记录使用定长内存池的New和Delete的开始时间
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;
}