【小项目】内存池的实现

本文详细介绍了内存池的出现背景及其实现原理,探讨了解决内存外碎片问题的方法,并通过一个具体的C++模板类实现了一个对象池,最后通过实验验证了其效率优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.为什么会有内存池的出现。

我们在频繁的开辟和释放小块空间时,很有可能造成内存外碎片的问题。为什么呢?看下图:

上图中还回来的20k和剩下的20k就可以称之为内存外碎片问题。除了外碎片的问题,频繁的在内存申请和释放小块空间是相当耗时的,那当需要频繁的申请和释放小块内存时,如何才能高效一点呢?所以就有了内存池的概念。

2.怎么设计一个内存池。

明确了要解决的问题,然后就是怎么设计了。
首先,考虑到,既然频繁的申请和释放是消耗时间的,那就可以直接向内存申请一块空间,专门用来给需要小块内存的场景下使用。使用完后不归还给操作系统,而是归还给给这块空间。
这样就产生了新的问题,如果这块固定的空间使用完了呢?那就必须向内存再次申请了,为保证不频繁的向内存申请,每一次开辟的大小定义为上一次的2倍。当然不能无限增长,得有一个最大值,当增长到最大值时,每次就只给最大值那么大的块。那如果新开辟的块的一部分使用完毕要归还给该块的时候该怎么找呢?我们知道,每一次分配内存基本上不会连续,如果没有一个指针指向,那就没办法找到那个位置再归还给它了。所以,每一次开辟一块内存,都用一个指针指向这块内存,并且用链表,把所有指向内存块的指针链起来,这样,就能找到每一个内存块了。
如果频繁的释放,那释放的这些空间不被重复利用的话,该内存池又会频繁的向内存申请了。所以,我们需要一个指针,记录下这些被释放回来的空间(具体做法,后面进行讲解)。在进行内存申请时,先使用释放回来的内存空间。
为了方便分配,在这里,我以一个对象而不是一个字节作为内存分配的单位。由于对象内可能要存储上一次释放的空间地址,所以,如果对象大小比一个指针的大小小,就直接给每个对象分配一个指针的大小。故,此版的内存池称之为对象池更加确切一点。
以上,就是设计的大致思路,我用画图展现如下。

那它是怎么做到用一个指针(_lastDelete)就能记录所有释放回来的内存对象呢?实际上是利用了内存对象本身,作为一块内存对象,它本身就能存储内容。做法是这样的:让_lastDelete最初为空,有内存对象释放回来的时候,让_lastDelete指向它。后面如果再有内存对象还回来,就把_lastDelete的值也就是上一个还回来的内存对象的地址放进新还回来的内存对象中,再让_lastDelete指向新的内存对象。如下图:

你可以把它想象成一个链表,只是它只占用一个指针空间。

3.源码

#pragma once
#include<iostream>
#include<vector>
#include<string>
using namespace std;

template<class T>
class ObjectPool    //对象池
{
	struct BlockNode
	{
		void* _memory;    //内存块
		BlockNode* _next;   //指向下一个结点的指针。
		size_t _objNum;   //内存块对象的个数。

		BlockNode(size_t objNum)
			:_next(NULL)
			, _objNum(objNum)
		{
			_memory = malloc(objNum*_itemSize);
		}
	};
public:
	ObjectPool(size_t initNum = 32, size_t maxNum = 100000)
		:_maxNum(maxNum)
		, _useIn(0)
		, _lastDelete(NULL)
	{
		_first = _last = new BlockNode(initNum);
	}
	~ObjectPool()
	{
		_Destory();
		_lastDelete = NULL;
		_first = NULL;
		_last = NULL;
		_maxNum = 0;
		_useIn = 0;
	}
	T* New()   //开辟内存
	{
		if (_lastDelete)   //优先使用还回来的内存对象
		{
			T* obj = _lastDelete;
			_lastDelete = *((T**)_lastDelete);   //取出_lastDelete所指对象的内容,让lastDelete指向下一块还回来的内存对象
			return new(obj)T();//定位new表达式把此对象内存初始化后返回
		}
		if (_useIn >= _last->_objNum)   //需要开辟新的内存块
		{
			size_t objNum = GetobjNum(_last->_objNum);
			_last->_next = new BlockNode(objNum);
			_last = _last->_next;
			_useIn = 0;
		}
		T* obj1 = new((T*)((char*)_last->_memory + _useIn*_itemSize))T();
		_useIn++;
		return  obj1;
	}
	static size_t InitItemSize()
	{
		if (sizeof(T) < sizeof(void*))
		{
			return sizeof(void*);
		}
		else
		{
			return sizeof(T);
		}
	}

	void Delete(T* ptr)
	{
		if (ptr)
		{
			*(T**)ptr = _lastDelete;  //把lastDelete的值放进释放的内存单元
			_lastDelete = ptr;    //再让lastDelete指向释放的内存对象。
		}

	}
protected:
	size_t GetobjNum(size_t oldNum)
	{
		size_t objNum = 2 * oldNum;
		if (objNum > _maxNum)
		{
			return _maxNum;
		}
		else
		{
			return objNum;
		}
	}
	void _Destory()
	{
		BlockNode* cur = _first;
		while (cur)
		{
			BlockNode* del = cur;
			cur = cur->_next;
			free(del->_memory);
			del->_objNum = 0;
			delete del;
		}
	}
protected:
	static size_t _itemSize;    //每个对象的大小,传进来的对象可能比一个指针小,这时需要给它一个指针的大小。
	size_t _maxNum;   //内存块的最大个数
	size_t _useIn;    //当前使用到第几个对象了
	T* _lastDelete;  //指向还回来的内存对象。
	BlockNode* _first; //链表的头结点
	BlockNode* _last;   //链表的尾结点
};

template<class T>
size_t ObjectPool<T>::_itemSize = ObjectPool<T>::InitItemSize();

void TestObjectPool()//测试正确性。
{
	vector<string*> v;

	ObjectPool<string> pool;
	for (size_t i = 0; i < 32; ++i)
	{
		v.push_back(pool.New());
		printf("Pool New [%d]: %p\n", i, v.back());
	}

	while (!v.empty())
	{
		pool.Delete(v.back());
		v.pop_back();
	}

	for (size_t i = 0; i < 32; ++i)
	{
		v.push_back(pool.New());
		printf("Pool New [%d]: %p\n", i, v.back());
	}

	v.push_back(pool.New());
}

#include <Windows.h>

// 针对当前的内存对象池进行简单的性能测试(和系统相比较)
void TestObjectPoolOP()   
{
	size_t begin, end;
	vector<string*> v;
	const size_t N = 1000000;
	v.reserve(N);

	cout << "pool new/delete===============================" << endl;
	// 反复申请释放5次
	begin = GetTickCount();
	ObjectPool<string> pool;
	for (size_t i = 0; i < N; ++i)
	{
		v.push_back(pool.New());
	}

	while (!v.empty())
	{
		pool.Delete(v.back());
		v.pop_back();
	}

	for (size_t i = 0; i < N; ++i)
	{
		v.push_back(pool.New());
	}

	while (!v.empty())
	{
		pool.Delete(v.back());
		v.pop_back();
	}

	for (size_t i = 0; i < N; ++i)
	{
		v.push_back(pool.New());
	}

	while (!v.empty())
	{
		pool.Delete(v.back());
		v.pop_back();
	}

	for (size_t i = 0; i < N; ++i)
	{
		v.push_back(pool.New());
	}

	while (!v.empty())
	{
		pool.Delete(v.back());
		v.pop_back();
	}

	for (size_t i = 0; i < N; ++i)
	{
		v.push_back(pool.New());
	}

	while (!v.empty())
	{
		pool.Delete(v.back());
		v.pop_back();
	}


	end = GetTickCount();
	cout << "Pool:" << end - begin << endl;

	cout << "new/delete==================================" << endl;
	begin = GetTickCount();

	for (size_t i = 0; i < N; ++i)
	{
		v.push_back(new string);
	}

	while (!v.empty())
	{
		delete v.back();
		v.pop_back();
	}

	for (size_t i = 0; i < N; ++i)
	{
		v.push_back(new string);
	}

	while (!v.empty())
	{
		delete v.back();
		v.pop_back();
	}

	for (size_t i = 0; i < N; ++i)
	{
		v.push_back(new string);
	}

	while (!v.empty())
	{
		delete v.back();
		v.pop_back();
	}

	for (size_t i = 0; i < N; ++i)
	{
		v.push_back(new string);
	}

	while (!v.empty())
	{
		delete v.back();
		v.pop_back();
	}

	for (size_t i = 0; i < N; ++i)
	{
		v.push_back(new string);
	}

	while (!v.empty())
	{
		delete v.back();
		v.pop_back();
	}

	end = GetTickCount();
	cout << "new/delete:" << end - begin << endl;
}

4.结果分析


可以看到,在申请32个对象空间后又释放回去,再次申请,拿到的还是同样的地址,说明复用的效果达到了

这是在Release下测试内存池申请和释放以及系统申请和释放的时间,对象池要比系统快好多。这种内存池在一些比较好的库里都有,比如STL就有自己的内存池,当然和这里实现的不一样。因为内存池在某些特点的场景下很有用。
源项目上传至gitub: 点击打开链接


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值