定长池的实现
目录
一、定长池的框架
二、如何脱离malloc的内存池,直接从堆拿空间?
三、如何设计内存块的指针?
四、代码框架及实现
五、性能测试
一、定长池的框架
在学习高并发内存池之前,我们先来实现一个定长池,他对于我们后面的内存池具有启发意义。定长池也叫对象池,只能存储一个类型的对象,因为该类型的对象大小肯定也是一样的,所有也叫定长池。
作为程序员(C/C++)我们知道申请内存使用的是malloc,malloc其实就是一个通用的大众货,什么场景下都可以用,但是什么场景下都可以用就意味着什么场景下都不会有很高的性能,下面我们就先来设计一个定长内存池做个开胃菜,当然这个定长内存池在我们后面的高并发内存池中也是有价值的,所以学习他目的有两层,先熟悉一下简单内存池是如何控制的,第二他会作为我们后面内存池的一个基础组件。
上面这一幅图就是不同的场景需要使用不同的对象,我们用不同的内存池俩存储对象们,而非使用malloc,这样就减少了内存碎片,所谓术业有专攻正是如此。
当我们申请空间的时候,直接从堆中申请一大块内存,由我们自己维护,如果要释放一个个的小内存块,则是把内存块挂起到_freelist中,这样以后再需要申请内存块则直接从_freelist中拿,不用再找系统的堆中拿了,从而提高了效率。
二、如何脱离malloc的内存池,直接从堆拿空间?
我们知道malloc的底层实际上是调用了brk和mmap等系统调用的(在linux情况下),而且malloc底层是自己维护了一个通用内存池,那么我们只需要跳过malloc,直接调用系统调用就能脱离malloc的内存池了。
在Windows环境中,我们使用的是和brk类似的接口。
//参数:要k页,一页是4096个字节
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage * (1 << 12), MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
#else
// linux下brk mmap等
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
}
三、如何设计内存块的指针?
在学习链表的时候,我们通常会把链表的节点设计成两个部分,一个存放下一个节点的地址的指针,剩下一个才是存放数据的空间。
但是在这里,我们并不采取上述做法,因为每一个链表节点都要存储指针的话,会让链表节点变大,由于我们上层应用并不需要遍历链表等操作,所以我们直接让链表指针使用内存块结构的前4/8个字节(取决于你的操作系统是32还是64位的),在上层申请空间的时候,返回该点。
这样该节点的所有空间都能用于存储数据,而delete之后,由于数据不在被需要,可以直接覆盖式的在前4/8个字节写上下一个内存块节点的指针。
所以如何获取一块空间的前4/8个字节呢?
四、代码框架及实现
(1)当自由链表有空间的时候,优先从自由链表中拿。
(2)当自由链表没有空间的时候,从memory中拿。
(3)当memory中没有空间的时候,才需要调用系统调用从堆中拿空间。
(4)如果一个对象的大小小于指针大小,则无法存储下一个节点的地址,所以一个节点最小4/8个字节。
template<class T>
class ObjectPool
{
public:
T* New();
void Delete(T* obj);
private:
char* _memory=nullptr;
void* _freelist=nullptr;
size_t _remainByte=0;
};
template<class T>
T* ObjectPool<T>::New()
{
T* obj = nullptr;
//1.从freelist中拿
if (_freelist!=nullptr)
{
void* next = *((void**)_freelist);
obj = (T*)_freelist;
_freelist = next;
}
else
{
//3.如果memory没有/不够,从系统调用brk拿
if (_remainByte < sizeof(T))
{
_remainByte = 128 * 1024;
_memory = (char*)SystemAlloc(_remainByte>>12);
//_memory = (char*)malloc(128 * 1024);
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
//2.如果freelist没有,从memory拿
obj = (T*)_memory;
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += objSize;
_remainByte -= objSize;
}
//初始化对象
//定位new,显式调用构造函数
new(obj)T;
return obj;
}
template<class T>
void ObjectPool<T>::Delete(T* obj)
{
//显式调用析构函数
obj->~T();
//头插不用考虑是否为空链表
*(void**)obj = _freelist;
_freelist = obj;
}
五、性能测试
在这里我们构建了一个树型节点,当做对象。每一次申请释放该对象N次,一个使用c++的关键字new,另外一个使用我们ObjectPool中的new()成员函数,观察运行时间。
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 = 1000000;
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;
}
可以看到,效率明显比直接使用new关键字要高许多。