C++高性能内存池,支持动态分配内存块

六眼飞鱼酱① 提交于 2019-12-28 09:18:15

     在C/C++中内存的管理是非常头痛的事情,这里作者不再多解释,请参考这篇文章:https://blog.csdn.net/business122/article/details/80566230,作者也是参考这篇文章进行对内存池的改进和进化。

    1、封装一个类用于管理内存池的使用如下,很容易看得懂,其实就是向内存池申请size个空间并进行构造,返回是首个元素的地址。释放也是一样,不过释放多个的时候需要确保这多个元素的内存是连续的。

#pragma once
#include <set>

template<typename T, typename Alloc = std::allocator<T>>
class AllocateManager
{
private:

	typedef typename Alloc::template rebind<T>::other other_;
	other_ m_allocate;//创建一个内存池管理器

public:
	//MemoryPool申请空间
	T * allocate(size_t size = 1)
	{
		//_SCL_SECURE_ALWAYS_VALIDATE(size != 0);
		T * node = m_allocate.allocate(size);
		m_allocate.construct(node, size);
		return node;
	}
	//Allocator申请空间
	T * allocateJJ(size_t size = 1)
	{
		//_SCL_SECURE_ALWAYS_VALIDATE(size != 0);
		T * node = m_allocate.allocate(size);
		m_allocate.construct(node);
		return node;
	}
	//释放并回收空间
	void destroy(T * node, size_t size = 1)
	{
		//_SCL_SECURE_ALWAYS_VALIDATE(size != 0);
		for (int i = 0; i < size; i++)
		{
			m_allocate.destroy(node);
			m_allocate.deallocate(node,1);
			node++;
		}
	}
	//获得当前内存池的大小
	const size_t getMenorySize()
	{
		return m_allocate.getMenorySize();
	}
	//获得当前内存池的块数
	const size_t getBlockSize()
	{
		return m_allocate.getBlockSize();
	}
};

rebind的设计跟C++stl里的设计是同样套路,stl设计代码如下:


template<class _Elem,
	class _Traits,
	class _Ax>
	class basic_string
		: public _String_val<_Elem, _Ax>
	{
	...
	typedef _String_val<_Elem, _Ax> _Mybase;
	typedef typename _Mybase::_Alty _Alloc;
	...


template<class _Ty,
	class _Alloc>
	class _String_val
		: public _String_base
	{
	...
	typedef typename _Alloc::template
		rebind<_Ty>::other _Alty;
	...


template<class _Ty>
	class allocator
		: public _Allocator_base<_Ty>
	{
	...
	template<class _Other>
		struct rebind
		{	// convert an allocator<_Ty> to an allocator <_Other>
		typedef allocator<_Other> other;
		};
	...

2、内存池设计代码,下面会一个一个方法抛开说明

#pragma once
#include <mutex>

template<typename T, int BlockSize = 6, int Block = sizeof(T) * BlockSize>
class MemoryPool
{
public:
	template<typename F>
	struct rebind
	{
		typedef MemoryPool<F, BlockSize> other;
	};
	MemoryPool()
	{
		m_FreeHeadSlot = nullptr;
		m_headSlot = nullptr;
		m_currentSlot = nullptr;
		m_LaterSlot = nullptr;
		m_MenorySize = 0;
		m_BlockSize = 0;
	}
	~MemoryPool()
	{
		//将每一块内存delete
		while (m_headSlot)
		{
			Slot_pointer pre = m_headSlot;
			m_headSlot = m_headSlot->next;
			operator delete(reinterpret_cast<void*>(pre));
		}
	}
	//申请空间
	T * allocateOne()
	{
		//空闲的位置有空间用空闲的位置
		if (m_FreeHeadSlot)
		{
			Slot_pointer pre = m_FreeHeadSlot;
			m_FreeHeadSlot = m_FreeHeadSlot->next;
			return reinterpret_cast<T*>(pre);
		}
		//申请一块内存
		if (m_currentSlot >= m_LaterSlot)
		{
			Char_pointer blockSize = reinterpret_cast<Char_pointer>(operator new(Block + sizeof(Slot_pointer)));

			m_MenorySize += (Block + sizeof(Slot_pointer));
			m_BlockSize++;

			reinterpret_cast<Slot_pointer>(blockSize)->next = m_headSlot;//将新内存放在表头
			m_headSlot = reinterpret_cast<Slot_pointer>(blockSize);

			m_currentSlot = reinterpret_cast<Slot_pointer>(blockSize + sizeof(Slot_pointer));//跳过指向下一块的指针这段内存
			m_LaterSlot = reinterpret_cast<Slot_pointer>(blockSize + Block + sizeof(Slot_pointer) - sizeof(Slot_)+1);//指向最后一个内存的开头位置
		}

		return reinterpret_cast<T*>(m_currentSlot++);
	}

	/*动态分配空间,注意:分配超过2个空间会在块里面创建占用4字节的空间存放数组的指针,
	这个空间不会被回收,所以动态分配最好分配大空间才使用动态
	*/
	T * allocate(size_t size = 1)
	{
		std::unique_lock<std::mutex> lock{ this->m_lock };

		//申请一个空间
		if (size == 1)
			return allocateOne();

		Slot_pointer pReSult = nullptr;
		/*先计算最后申请的块空间够不够,不适用回收的空间,因为回收空间不是连续*/
		int canUseSize = reinterpret_cast<int>(m_LaterSlot) + sizeof(Slot_) - 1 - reinterpret_cast<int>(m_currentSlot);

		int applySize = sizeof(T) * size + sizeof(T*);//创建数组对象时多了个指针,所以内存要加个指针的大小
		if (applySize <= canUseSize) //空间足够,把剩余空间分配出去
		{
			pReSult = m_currentSlot;
			m_currentSlot = reinterpret_cast<Slot_pointer>(reinterpret_cast<Char_pointer>(m_currentSlot) + applySize);
			return reinterpret_cast<T*>(pReSult);
		}

		/*空间不够动态分配块大小,不把上一块剩余的空间使用是因为空间是需要连续,
		所以上一块会继续往前推供下次使用*/
		Char_pointer blockSize = reinterpret_cast<Char_pointer>(operator new(applySize + sizeof(Slot_pointer)));
		m_MenorySize += (applySize + sizeof(Slot_pointer));
		m_BlockSize++;
		if (!m_headSlot)//目前没有一块内存情况
		{
			reinterpret_cast<Slot_pointer>(blockSize)->next = m_headSlot;
			m_headSlot = reinterpret_cast<Slot_pointer>(blockSize);
			m_currentSlot = reinterpret_cast<Slot_pointer>(blockSize + sizeof(Slot_pointer));
			m_LaterSlot = reinterpret_cast<Slot_pointer>(blockSize + Block + sizeof(Slot_pointer) - sizeof(Slot_) + 1);
			pReSult = m_currentSlot;
			m_currentSlot = m_LaterSlot;//第一块内存且是动态分配,所以这一块内存是满的
		}
		else
		{
			//这个申请一块动态内存就用完,直接往头后面移动
			Slot_pointer currentSlot = nullptr;
			Slot_pointer next = m_headSlot->next;
			currentSlot = reinterpret_cast<Slot_pointer>(blockSize);
			currentSlot->next = next;
			m_headSlot->next = currentSlot;
			pReSult = reinterpret_cast<Slot_pointer>(blockSize + sizeof(Slot_pointer));//跳过指向下一块的指针这段内存
		}
		return reinterpret_cast<T*>(pReSult);
	}

	//使用空间
	void construct(T * p, size_t size = 1)
	{
		//_SCL_SECURE_ALWAYS_VALIDATE(size != 0);
		if (size == 1)
			new (p)T();
		else
			new (p)T[size]();
	}

	//析构一个对象
	void destroy(T * p)
	{
		p->~T();
	}

	//回收一个空间
	void deallocate(T * p, size_t count = 1)
	{
		std::unique_lock<std::mutex> lock{ this->m_lock };

		reinterpret_cast<Slot_pointer>(p)->next = m_FreeHeadSlot;
		m_FreeHeadSlot = reinterpret_cast<Slot_pointer>(p);
	}

	const size_t getMenorySize()
	{
		return m_MenorySize;
	}
	const size_t getBlockSize()
	{
		return m_BlockSize;
	}
private:
	union Slot_
	{
		T _data;
		Slot_ * next;
	};
	typedef Slot_* Slot_pointer;
	typedef char*  Char_pointer;

	Slot_pointer m_FreeHeadSlot;//空闲的空间头部位置
	Slot_pointer m_headSlot;//指向的头位置
	Slot_pointer m_currentSlot;//当前所指向的位置
	Slot_pointer m_LaterSlot;//指向最后一个元素的开始位置

	size_t m_MenorySize;
	size_t m_BlockSize;

	// 同步
	std::mutex m_lock;
	static_assert(BlockSize > 0, "BlockSize can not zero");
};

3、申请一个空间,当回收的内存没有或内存块空间不够时,新开辟一块内存,并将新内存放在表头,返回新内存的头地址,如果内存块还有空间,那么返回首个空余的空间

//申请空间
	T * allocateOne()
	{
		//空闲的位置有空间用空闲的位置
		if (m_FreeHeadSlot)
		{
			Slot_pointer pre = m_FreeHeadSlot;
			m_FreeHeadSlot = m_FreeHeadSlot->next;
			return reinterpret_cast<T*>(pre);
		}
		//申请一块内存
		if (m_currentSlot >= m_LaterSlot)
		{
			Char_pointer blockSize = reinterpret_cast<Char_pointer>(operator new(Block + sizeof(Slot_pointer)));

			m_MenorySize += (Block + sizeof(Slot_pointer));
			m_BlockSize++;

			reinterpret_cast<Slot_pointer>(blockSize)->next = m_headSlot;//将新内存放在表头
			m_headSlot = reinterpret_cast<Slot_pointer>(blockSize);

			m_currentSlot = reinterpret_cast<Slot_pointer>(blockSize + sizeof(Slot_pointer));//跳过指向下一块的指针这段内存
			m_LaterSlot = reinterpret_cast<Slot_pointer>(blockSize + Block + sizeof(Slot_pointer) - sizeof(Slot_)+1);//指向最后一个内存的开头位置
		}

		return reinterpret_cast<T*>(m_currentSlot++);
	}

4、当分配超过2个元素空间时,先判断空闲块的空间够不够分配,够分配,不够新开辟一个大小跟申请元素个数一样的内存块,并将该块内存向表头置后,返回该快首地址。注意,由于分配多个元素的空间也就是分配一个数组,这个时候在下一步调用构造函数时会构造数组对象,数组对象会多一个指针空间指向该数组,所以申请n+1个元素时加上一个指针的空间,否则会泄漏。

这个指针的空间是没有用的,释放和回收空间是不会回收这个指针,这样它就会占用了内存块一个指针空间,就相当于磁盘分区有未分配的内存一样,分配多个元素空间时这个是无法避免的。

/*动态分配空间,注意:分配超过2个空间会在块里面创建占用4字节的空间存放数组的指针,
	这个空间不会被回收,所以动态分配最好分配大空间才使用动态
	*/
	T * allocate(size_t size = 1)
	{
		std::unique_lock<std::mutex> lock{ this->m_lock };

		//申请一个空间
		if (size == 1)
			return allocateOne();

		Slot_pointer pReSult = nullptr;
		/*先计算最后申请的块空间够不够,不适用回收的空间,因为回收空间不是连续*/
		int canUseSize = reinterpret_cast<int>(m_LaterSlot) + sizeof(Slot_) - 1 - reinterpret_cast<int>(m_currentSlot);

		int applySize = sizeof(T) * size + sizeof(T*);//创建数组对象时多了个指针,所以内存要加个指针的大小
		if (applySize <= canUseSize) //空间足够,把剩余空间分配出去
		{
			pReSult = m_currentSlot;
			m_currentSlot = reinterpret_cast<Slot_pointer>(reinterpret_cast<Char_pointer>(m_currentSlot) + applySize);
			return reinterpret_cast<T*>(pReSult);
		}

		/*空间不够动态分配块大小,不把上一块剩余的空间使用是因为空间是需要连续,
		所以上一块会继续往前推供下次使用*/
		Char_pointer blockSize = reinterpret_cast<Char_pointer>(operator new(applySize + sizeof(Slot_pointer)));
		m_MenorySize += (applySize + sizeof(Slot_pointer));
		m_BlockSize++;
		if (!m_headSlot)//目前没有一块内存情况
		{
			reinterpret_cast<Slot_pointer>(blockSize)->next = m_headSlot;
			m_headSlot = reinterpret_cast<Slot_pointer>(blockSize);
			m_currentSlot = reinterpret_cast<Slot_pointer>(blockSize + sizeof(Slot_pointer));
			m_LaterSlot = reinterpret_cast<Slot_pointer>(blockSize + Block + sizeof(Slot_pointer) - sizeof(Slot_) + 1);
			pReSult = m_currentSlot;
			m_currentSlot = m_LaterSlot;//第一块内存且是动态分配,所以这一块内存是满的
		}
		else
		{
			//这个申请一块动态内存就用完,直接往头后面移动
			Slot_pointer currentSlot = nullptr;
			Slot_pointer next = m_headSlot->next;
			currentSlot = reinterpret_cast<Slot_pointer>(blockSize);
			currentSlot->next = next;
			m_headSlot->next = currentSlot;
			pReSult = reinterpret_cast<Slot_pointer>(blockSize + sizeof(Slot_pointer));//跳过指向下一块的指针这段内存
		}
		return reinterpret_cast<T*>(pReSult);
	}

其他小的方法就不介绍了,代码也有注释很容易看得懂。

5、性能测试

动态分配时,num1表示分配块数,num2表示分配的每块大小。

逐步申请和释放一千万个空间(元素为单位),速度如下,C++是最慢的,MemoryPool快乐接近20倍,MemoryPool动态分配会更快乐些(面对疾风吧)。

测试代码:

#include <iostream>
#include <string>
#include <set>
#include <ctime>
#include <thread>
#include"MemoryPool.h"
#include"AllocateManager.h"
using namespace std;
//动态分配时,num1表示块数,num2表示每块大小
#define num1 1000
#define num2 10000  
class Test
{
public:
	int a;
	~Test()
	{
		//cout << a << " ";
	}
};

void TestByCjj()
{
	clock_t start;
	start = clock();
	Test * p[num1][num2];
	Test * t;
	AllocateManager<Test, allocator<Test>> pool;
	start = clock();
	int count = 0;
	//向内存池申请空间并构造出对象
	for (int i = 0; i < num1; i++)
	{
		for (int j = 0; j < num2; j++)
		{
			t = pool.allocateJJ(1);
			t->a = count++;
			p[i][j] = t;
		}
	}
	//根据对象从内存池释放并回收该空间
	for (int i = 0; i < num1; i++)
	{
		for (int j = 0; j < num2; j++)
		{
			t = p[i][j];
			pool.destroy(t);
		}
	}
	std::cout << "C++ Time: ";
	std::cout << (((double)clock() - start) / CLOCKS_PER_SEC) << endl;
}

void TestByOne()
{
	clock_t start;
	start = clock();
	Test * p[num1][num2];
	Test * t;
	AllocateManager<Test, MemoryPool<Test, 1024>> memoryPool;
	start = clock();
	int count = 0;
	for (int i = 0; i < num1; i++)
	{
		for (int j = 0; j < num2; j++)
		{
			t = memoryPool.allocate(1);
			t->a = count++;
			p[i][j] = t;
		}
	}
	for (int i = 0; i < num1; i++)
	{
		for (int j = 0; j < num2; j++)
		{
			t = p[i][j];
			memoryPool.destroy(t);
		}
	}
	std::cout << "MemoryPool One Time: ";
	std::cout << (((double)clock() - start) / CLOCKS_PER_SEC);
	std::cout << "  内存块数量:" << memoryPool.getBlockSize();
	std::cout << "  内存消耗(byte):" << memoryPool.getMenorySize() << std::endl;
}
void TestByBlock()
{
	clock_t start;
	start = clock();
	Test * p[num1][num2];
	Test * t;
	AllocateManager<Test, MemoryPool<Test, 1024>> memoryPool;
	start = clock();
	int count = 0;
	for (int i = 0; i < num1; i++)
	{
		t = memoryPool.allocate(num2);
		for (int j = 0; j < num2; j++)
		{
			t->a = count++;
			p[i][j] = t++;
		}
	}
	for (int i = 0; i < num1; i++)
	{
		for (int j = 0; j < num2; j++)
		{
			Test * t = p[i][j];
			memoryPool.destroy(t);
		}
	}
	std::cout << "MemoryPool Block Time: ";
	std::cout << (((double)clock() - start) / CLOCKS_PER_SEC);
	std::cout << "  内存块数量:" << memoryPool.getBlockSize();
	std::cout << "  内存消耗(byte):" << memoryPool.getMenorySize() << std::endl;
}

int main()
{
	TestByCjj();
	TestByOne();
	TestByBlock();
	
	return 0;
}

工程文件链接

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!