1. 程式人生 > >C++記憶體池介紹與經典記憶體池的實現

C++記憶體池介紹與經典記憶體池的實現

程式碼編譯執行環境:VS2012+Debug+Win32

1.預設記憶體管理函式的不足

利用預設的記憶體管理操作符new/delete和函式malloc()/free()在堆上分配和釋放記憶體會有一些額外的開銷。

系統在接收到分配一定大小記憶體的請求時,首先查詢內部維護的記憶體空閒塊表,並且需要根據一定的演算法(例如分配最先找到的不小於申請大小的記憶體塊給請求者,或者分配最適於申請大小的記憶體塊,或者分配最大空閒的記憶體塊等)找到合適大小的空閒記憶體塊。如果該空閒記憶體塊過大,還需要切割成已分配的部分和較小的空閒塊。然後系統更新記憶體空閒塊表,完成一次記憶體分配。類似地,在釋放記憶體時,系統把釋放的記憶體塊重新加入到空閒記憶體塊表中。如果有可能的話,可以把相鄰的空閒塊合併成較大的空閒塊。預設的記憶體管理函式還考慮到多執行緒的應用,需要在每次分配和釋放記憶體時加鎖,同樣增加了開銷。

可見,如果應用程式頻繁地在堆上分配和釋放記憶體,會導致效能的損失。並且會使系統中出現大量的記憶體碎片,降低記憶體的利用率。預設的分配和釋放記憶體演算法自然也考慮了效能,然而這些記憶體管理演算法的通用版本為了應付更復雜、更廣泛的情況,需要做更多的額外工作。而對於某一個具體的應用程式來說,適合自身特定的記憶體分配釋放模式的自定義記憶體池可以獲得更好的效能。

2.記憶體池簡介

2.1記憶體池的定義

記憶體池(Memory Pool)是一種記憶體分配方式。通常我們習慣直接使用new、malloc等API申請記憶體,這樣做的缺點在於所申請記憶體塊的大小不定,當頻繁使用時會造成大量的記憶體碎片並進而降低效能。

2.2記憶體池的優點

記憶體池則是在真正使用記憶體之前,預先申請分配一定數量、大小相等(一般情況下)的記憶體塊留作備用。當有新的記憶體需求時,就從記憶體池中分出一部分記憶體塊,若記憶體塊不夠再繼續申請新的記憶體。這樣做的一個顯著優點是,使得記憶體分配效率得到提升。

2.3記憶體池的分類

應用程式自定義的記憶體池根據不同的適用場景又有不同的型別。從執行緒安全的角度來分,記憶體池可以分為單執行緒記憶體池和多執行緒記憶體池。單執行緒記憶體池整個生命週期只被一個執行緒使用,因而不需要考慮互斥訪問的問題;多執行緒記憶體池有可能被多個執行緒共享,因此需要在每次分配和釋放記憶體時加鎖。相對而言,單執行緒記憶體池效能更高,而多執行緒記憶體池適用範圍更加廣泛。

從記憶體池可分配記憶體單元大小來分,可以分為固定記憶體池和可變記憶體池。所謂固定記憶體池是指應用程式每次從記憶體池中分配出來的記憶體單元大小事先已經確定,是固定不變的;而可變記憶體池則每次分配的記憶體單元大小可以按需變化,應用範圍更廣,而效能比固定記憶體池要低。

3.經典的記憶體池技術

記憶體池技術因為其對記憶體管理有著顯著的優點,在各大專案中廣泛應用,備受推崇。但是,通用的記憶體管理機制要考慮很多複雜的具體情況,如多執行緒安全等,難以對演算法做有效的優化,所以,在一些特殊場合,實現特定應用環境的記憶體池在一定程度上能夠提高記憶體管理的效率。

經典記憶體池技術,是一種用於分配大量大小相同的小物件的技術。通過該技術可以極大加快記憶體分配/釋放過程。既然是針對特定物件的記憶體池,所以記憶體池一般設定為類模板,根據不同的物件來進行例項化。

3.1經典記憶體池的設計

3.1.1經典記憶體池實現過程

(1)先申請一塊連續的記憶體空間,該段記憶體空間能夠容納一定數量的物件;
(2)每個物件連同一個指向下一個物件的指標一起構成一個記憶體節點(Memory Node)。各個空閒的記憶體節點通過指標形成一個連結串列,連結串列的每一個記憶體節點都是一塊可供分配的記憶體空間;
(3)某個記憶體節點一旦分配出去,從空閒記憶體節點連結串列中去除;
(4)一旦釋放了某個記憶體節點的空間,又將該節點重新加入空閒記憶體節點連結串列;
(5)如果一個記憶體塊的所有記憶體節點分配完畢,若程式繼續申請新的物件空間,則會再次申請一個記憶體塊來容納新的物件。新申請的記憶體塊會加入記憶體塊連結串列中。

經典記憶體池的實現過程大致如上面所述,其形象化的過程如下圖所示:
在這裡插入圖片描述

如上圖所示,申請的記憶體塊存放三個可供分配的空閒節點。空閒節點由空閒節點連結串列管理,如果分配出去,將其從空閒節點連結串列刪除,如果釋放,將其重新插入到連結串列的頭部。如果記憶體塊中的空閒節點不夠用,則重新申請記憶體塊,申請的記憶體塊由記憶體塊連結串列來管理。

注意,本文涉及到的記憶體塊連結串列和空閒記憶體節點連結串列的插入,為了省去遍歷連結串列查詢尾節點,便於操作,新節點的插入均是插入到連結串列的頭部,而非尾部。當然也可以插入到尾部,讀者可自行實現。

3.1.2經典記憶體池資料結構設計

按照上面的過程設計,記憶體池類模板有這樣幾個成員。

兩個指標變數:
記憶體塊連結串列頭指標:pMemBlockHeader;
空閒節點連結串列頭指標:pFreeNodeHeader;

空閒節點結構體:

struct FreeNode
{
	FreeNode* pNext;
	char data[ObjectSize];
};

記憶體塊結構體:

struct MemBlock
{
	MemBlock *pNext;
	FreeNode data[NumofObjects];
};

3.2經典記憶體池的實現

根據以上經典記憶體池的設計,編碼實現如下。

#include <iostream>
using namespace std;

template<int ObjectSize, int NumofObjects = 20>
class MemPool
{
private:
	//空閒節點結構體
	struct FreeNode
	{
		FreeNode* pNext;
		char data[ObjectSize];
	};

	//記憶體塊結構體
	struct MemBlock
	{
		MemBlock* pNext;
		FreeNode data[NumofObjects];
	};

	FreeNode* freeNodeHeader;
	MemBlock* memBlockHeader;

public:
	MemPool()
	{
		freeNodeHeader = NULL;
		memBlockHeader = NULL;
	}

	~MemPool()
	{
		MemBlock* ptr;
		while (memBlockHeader)
		{
			ptr = memBlockHeader->pNext;
			delete memBlockHeader;
			memBlockHeader = ptr;
		}
	}
	void* malloc();
	void free(void*);
};

//分配空閒的節點
template<int ObjectSize, int NumofObjects>
void* MemPool<ObjectSize, NumofObjects>::malloc()
{
	//無空閒節點,申請新記憶體塊
	if (freeNodeHeader == NULL)
	{
		MemBlock* newBlock = new MemBlock;
		newBlock->pNext = NULL;

		freeNodeHeader=&newBlock->data[0];	 //設定記憶體塊的第一個節點為空閒節點連結串列的首節點
		//將記憶體塊的其它節點串起來
		for (int i = 1; i < NumofObjects; ++i)
		{
			newBlock->data[i - 1].pNext = &newBlock->data[i];
		}
		newBlock->data[NumofObjects - 1].pNext=NULL;

		//首次申請記憶體塊
		if (memBlockHeader == NULL)
		{
			memBlockHeader = newBlock;
		}
		else
		{
			//將新記憶體塊加入到記憶體塊連結串列
			newBlock->pNext = memBlockHeader;
			memBlockHeader = newBlock;
		}
	}
	//返回空節點閒連結串列的第一個節點
	void* freeNode = freeNodeHeader;
	freeNodeHeader = freeNodeHeader->pNext;
	return freeNode;
}

//釋放已經分配的節點
template<int ObjectSize, int NumofObjects>
void MemPool<ObjectSize, NumofObjects>::free(void* p)
{
	FreeNode* pNode = (FreeNode*)p;
	pNode->pNext = freeNodeHeader;	//將釋放的節點插入空閒節點頭部
	freeNodeHeader = pNode;
}

class ActualClass
{
	static int count;
	int No;

public:
	ActualClass()
	{
		No = count;
		count++;
	}

	void print()
	{
		cout << this << ": ";
		cout << "the " << No << "th object" << endl;
	}

	void* operator new(size_t size);
	void operator delete(void* p);
};

//定義記憶體池物件
MemPool<sizeof(ActualClass), 2> mp;

void* ActualClass::operator new(size_t size)
{
	return mp.malloc();
}

void ActualClass::operator delete(void* p)
{
	mp.free(p);
}

int ActualClass::count = 0;

int main()
{
	ActualClass* p1 = new ActualClass;
	p1->print();

	ActualClass* p2 = new ActualClass;
	p2->print();
	delete p1;

	p1 = new ActualClass;
	p1->print();

	ActualClass* p3 = new ActualClass;
	p3->print();

	delete p1;
	delete p2;
	delete p3;
}

程式執行結果:

004AA214: the 0th object
004AA21C: the 1th object
004AA214: the 2th object
004AB1A4: the 3th object

3.3程式分析

閱讀以上程式,應注意以下幾點。
(1)對一種特定的類物件而言,記憶體池中記憶體塊的大小是固定的,記憶體節點的大小也是固定的。記憶體塊在申請之初就被劃分為多個記憶體節點,每個Node的大小為ItemSize。剛開始,所有的記憶體節點都是空閒的,被串成連結串列。

(2)成員指標變數memBlockHeader是用來把所有申請的記憶體塊連線成一個記憶體塊連結串列,以便通過它可以釋放所有申請的記憶體。freeNodeHeader變數則是把所有空閒記憶體節點串成一個連結串列。freeNodeHeader為空則表明沒有可用的空閒記憶體節點,必須申請新的記憶體塊。

(3)申請空間的過程如下。在空閒記憶體節點連結串列非空的情況下,malloc過程只是從連結串列中取下空閒記憶體節點連結串列的頭一個節點,然後把連結串列頭指標移動到下一個節點上去。否則,意味著需要一個新的記憶體塊。這個過程需要申請新的記憶體塊切割成多個記憶體節點,並把它們串起來,記憶體池技術的主要開銷就在這裡。

(4)釋放物件的過程就是把被釋放的記憶體節點重新插入到記憶體節點連結串列的開頭。最後被釋放的節點就是下一個即將被分配的節點。

(5)記憶體池技術申請/釋放記憶體的速度很快,其記憶體分配過程多數情況下複雜度為O(1),主要開銷在freeNodeHeader為空時需要生成新的記憶體塊。記憶體節點釋放過程複雜度為O(1)。

(6) 在上面的程式中,指標p1和p2連續兩次申請空間,它們代表的地址之間的差值為8,正好為一個記憶體節點的大小(sizeof(FreeNode))。指標p1所指向的物件被釋放後,再次申請空間,得到的地址與剛剛釋放的地址正好相同。指標p3多代表的地址與前兩個物件的地址相聚很遠,原因是第一個記憶體塊中的空閒記憶體節點已經分配完了,p3指向的物件位於第二個記憶體塊中。

以上記憶體池方案並不完美,比如,只能單個單個申請物件空間,不能申請物件陣列,記憶體池中記憶體塊的個數只能增大不能減少,未考慮多執行緒安全等問題。現在,已經有很多改進的方案,請讀者自行查閱相關資料。

參考文獻

[1] C++ 應用程式效能優化,第 6 章:記憶體池
[2]陳剛.C++高階進階教程[M].武漢:武漢大學出版社,2008.7.8什麼是記憶體池技術