C++高性能內存池,支持動態分配內存塊

     在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;
}

工程文件鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章