定长内存池的设计
定长内存池
为什么我们要设计这个定长内存池呢?首先malloc是c标准库中向堆申请空间的接口,变相的说malloc是普遍性,而我们要实现的高并发内存池是特殊性,是针对某种情况下把效率提高到极致的一种申请空间的方法。在后续我们的项目中也会用到定长内存池,所以定长内存池的设计有两方面,一是让我们熟悉一下内存的分配问题,二是他会作为我们的高并发内存池的一个基础组件。
定长内存池的原理讲解
首先我们需要在内存中申请一块固定大小的空间,如下图:
后面我们考虑如何分配空间以及管理后续的空间。这里我们像系统申请了一大块空间,如果再有用户需要申请空间这个时候我们就可以直接从我们管理的那一大块空间中取出空间,从而避免了重复申请和释放空间的效率问题。但是这里用户归还的空间我们也不能直接还给操作系统,所以我们需要使用一个freelist(自由链表)将归还的空间管理起来
每次从上面取完空间后,归还空间我们就直接头插到链表中。下次如果在需要申请,我们就可以直接使用_freelist中的空间直接给到用户使用。
代码实现
定义对象
我们先定义一个class ObjectPool类对象
cpp
#pragma once
#include<iostream>
using std::endl;
using std::cout;
template<class T>
class Object {
public:
private:
char* _memory = nullptr; //指向内存池的起始位置
size_t _remain = 0; //内存剩余空间大小
void* _freelist = nullptr; //指向自由链表的第一个节点的指针
};
这里定义三个私有成员变量,具体用处见上述代码注释。
New对象的主要逻辑
首先我们对于空间申请的类,肯定就是new 和 delete两个重要的接口。我们先来写new:
cpp
T* New()
{
T* obj = nullptr; //先顶一个返回的obj
if (_freelist) //如果自由链表不为空,此时我们直接使用自由链表的空间
{
//这里就相当于链表的头删
void* next = *(void**)_freelist; //定义一个next存储链表下一个节点的地址
obj = (T*)_freelist; //再让obj指向分配内存的首地址
_freelist = next; //再将自由链表连接到下一个节点处
}
else{
if (_remain<sizeof(T)) //如果内存空间剩余大小小于当前类型的大小
{
//重新像系统申请一块大空间
_remain = 128 * 1024; //更新内存剩余空间大小
_memory = (char*)malloc(_remain); //开辟空间
if (_memory == nullptr) //如果_memory为空则报错
{
throw std::bad_alloc();
}
}
obj = (T*)_memory; //obj接收被切割的首地址
size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T); //这里为了防止开辟的空间没有办法储存地址,所以如果类型大小小于地址的大小,则开辟地址大小字节的空间,否则开辟类型大小的空间。
_memory += objsize; //随后_memory指向后面的空间
_remain -= objsize; //_remain减少
//显示调用一下构造,应对string等这些类型的构造
new(obj)T;
return obj;
}
}
delete对象的主要逻辑
delete就相当于是链表的头插
cpp
void Delete(T* obj)
{
obj->~T();
*(void**)obj = _freelist;
_freelist = obj;
}
这里的主要逻辑就结束了,接下来对代码进行优化一下。
我们这里使用的还是malloc,所以我们还可以完全脱离malloc使用系统调用接口virtualalloc这个接口直接在堆上申请空间,接下来就是完整代码,还有一段测试的例子我们可以清楚的看到我们的定长内存池在申请同一类型的空间时效率的提升有多夸张。
完整代码
这里我们使用了条件编译用来区分windows和Linux中不同的系统调用接口。
cpp
#pragma once
#include<iostream>
#include<vector>
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;
obj = (T*)_freelist;
_freelist = next;
}
else {
if (_remain < sizeof(T))
{
_remain = 128 * 1024;
//_memery = (char*)malloc(_remain);
_memery = (char*)SystemAlloc(_remain>>13);
if (_memery == nullptr)
{
throw std::bad_alloc();
}
}
obj = (T*)_memery;
size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memery += objsize;
_remain -= objsize;
}
new(obj)T;
return obj;
}
void Delete(T* obj)
{
obj->~T();
*(void**)obj = _freelist;
_freelist = obj;
}
private:
char* _memery = nullptr;//内存池起始地址
size_t _remain = 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 = 100000;
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;
}
Debug:
Release: