从零实现一个定长高并发内存池

一、什么是内存池?

从堆上一次性开辟一大片空间,由我们自己管理,这里的一大片的空间我们就将它称为池,当我们需要拿空间的时候去这个池中拿取

二、为什么要有内存池?有什么好处?

我们知道如果要开辟一块内存需要去调用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.后面的代码为测试代码,不断地创建对象又释放对象看看谁更快

结果为我们做的内存池的速度比系统的快,数值越大则差距也越大

因为我们做的是只针对一个类型的内存池,系统则需要照顾全局,所以定长内存池是有缺陷的只能针对一种类型 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值