原帖與示例代碼地址:http://www.codeproject.com/KB/cpp/MemoryPool.aspx
譯者點評:一個簡單的內存池實現,附有源碼,簡單易懂,適合入門。
概述
在c/c++中,內存分配(如malloc或new)會使用很多時間。
一個程序會隨着長時間的運行和內存的申請釋放而變得越來越慢,內存也會隨着時間逐漸碎片化。特別是高頻率的進行小內存申請釋放,此問題變得尤其嚴重。
解決方案:定制內存池
為解決上述問題,一個(可能的)的解決方案就是使用內存池。
“內存池”在初始化時,分配一個大塊內存(稱 原始內存塊),並且將此內存分割為一些小的內存塊。當你需要請求分配內存時,則從內存池中取出事先分配好的內存,而不是向OS申請。內存池最大的優勢在於:
1、極少的(甚至沒有)堆碎片整理
2、較之普通內存分配(如malloc,new),有着更快的速度
額外的,你還將獲得如下好處:
1、檢測任意的指針是否指向內存池內
2、生成"heap-dump"
3、各種 內存泄漏 檢測:當你沒有釋放之前申請的內存,內存池將拋出斷言
如何工作?
讓我們看看內存池的UML模型圖:
圖中簡要的描述了CMemoryPool class,更多的細節請查看源碼中class聲明。
那么,CMemoryPool如何實際工作?
關於 MemoryChunks
正如你在UML圖中所看到的,內存池維護着一個SMemoryChunk鏈表,並管理着三個指向SMemoryChunk結構的指針(m_ptrFirstChunk
, m_ptrLastChunk
, and m_ptrCursorChunk
)。這些指針指向SMemoryChunk鏈表的不同位置。讓我們更深入的觀察
SMemoryChunk:(在內存池實現中,SMemoryChunk封裝了原始內存塊的各個部分 -- 譯者注)
typedef struct SMemoryChunk
{
TByte *Data ; // 常規數據指針
std::size_t DataSize ; // 內存塊容量
std::size_t UsedSize ; // 內存塊當前使用大小
bool IsAllocationChunk ; // 為true時, 內存塊已被分配,可用free之類的函數釋放
SMemoryChunk *Next ; // 指向內存塊鏈表中的下一個內存塊,可能為null
第一步:預分配內存
當你調用CMemoryPool的構造函數,內存池會向OS申請原始內存塊。
/******************
Constructor
******************/
CMemoryPool::CMemoryPool(const std::size_t &sInitialMemoryPoolSize,
const std::size_t &sMemoryChunkSize,
const std::size_t &sMinimalMemorySizeToAllocate,
bool bSetMemoryData)
{
m_ptrFirstChunk = NULL ;
m_ptrLastChunk = NULL ;
m_ptrCursorChunk = NULL ;
m_sTotalMemoryPoolSize = 0 ;
m_sUsedMemoryPoolSize = 0 ;
m_sFreeMemoryPoolSize = 0 ;
m_sMemoryChunkSize = sMemoryChunkSize ;
m_uiMemoryChunkCount = 0 ;
m_uiObjectCount = 0 ;
m_bSetMemoryData = bSetMemoryData ;
m_sMinimalMemorySizeToAllocate = sMinimalMemorySizeToAllocate ;
// Allocate the Initial amount of Memory from the Operating-System...
AllocateMemory(sInitialMemoryPoolSize) ;
}
所有的成員的函數初始化在此完成,最后AllocateMemory將完成向OS申請原始內存塊的任務。
/******************
AllocateMemory
******************/
<CODE>bool CMemoryPool::AllocateMemory(const std::size_t &sMemorySize)
{
std::size_t sBestMemBlockSize = CalculateBestMemoryBlockSize(sMemorySize) ;
// allocate from Operating System
TByte *ptrNewMemBlock = (TByte *) malloc(sBestMemBlockSize) ;
...
那么,內存池如何來管理這些數據呢?
第二步:內存分塊
回憶前述,內存池管理使用SMemoryChunk鏈表來管理數據。在向OS申請原始內存塊后,
我們還沒有在其上建立SMemoryChunk。
圖中所示的為初始化分配后的內存池。
我們需要分配一組SMemoryChunk,用於管理原始內存塊:
//(AllocateMemory() continued) :
...
unsigned int uiNeededChunks = CalculateNeededChunks(sMemorySize) ;
// allocate Chunk-Array to Manage the Memory
SMemoryChunk *ptrNewChunks =
(SMemoryChunk *) malloc((uiNeededChunks * sizeof(SMemoryChunk))) ;
assert(((ptrNewMemBlock) && (ptrNewChunks))
&& "Error : System ran out of Memory") ;
...
CalculateNeededChunks函數用於計算需要分配的SMemoryChunk的數量。分配后,ptrNewChunks指向這組SMemoryChunk。注意,SMemoryChunk中目前只是持有垃圾數據,我們還沒有為SMemoryChunk的成員關聯至原始內存塊。
最后,AllocateMemory函數將為所有的SMemoryChunk關聯至原始內存塊。
//(AllocateMemory() continued) :
...
// Associate the allocated Memory-Block with the Linked-List of MemoryChunks
return LinkChunksToData(ptrNewChunks, uiNeededChunks, ptrNewMemBlock) ;
讓我們進入LinkChunksToData中一窺究竟:
/******************
LinkChunksToData
******************/
bool CMemoryPool::LinkChunksToData(SMemoryChunk *ptrNewChunks,
unsigned int uiChunkCount, TByte *ptrNewMemBlock)
{
SMemoryChunk *ptrNewChunk = NULL ;
unsigned int uiMemOffSet = 0 ;
bool bAllocationChunkAssigned = false ;
for(unsigned int i = 0; i < uiChunkCount; i++)
{
if(!m_ptrFirstChunk)
{
m_ptrFirstChunk = SetChunkDefaults(&(ptrNewChunks[0])) ;
m_ptrLastChunk = m_ptrFirstChunk ;
m_ptrCursorChunk = m_ptrFirstChunk ;
}
else
{
ptrNewChunk = SetChunkDefaults(&(ptrNewChunks[i])) ;
m_ptrLastChunk->Next = ptrNewChunk ;
m_ptrLastChunk = ptrNewChunk ;
}
uiMemOffSet = (i * ((unsigned int) m_sMemoryChunkSize)) ;
m_ptrLastChunk->Data = &(ptrNewMemBlock[uiMemOffSet]) ;
// 第一個SMemoryChunk被稱為“AllocationChunk”。
// 這意味着,它持有原始內存塊的指針並能夠利用它釋放原始內存塊
if(!bAllocationChunkAssigned)
{
m_ptrLastChunk->IsAllocationChunk = true ;
bAllocationChunkAssigned = true ;
}
}
return RecalcChunkMemorySize(m_ptrFirstChunk, m_uiMemoryChunkCount) ;
}
讓我們一步步的來看這個重要的函數:第一行檢查在SMemoryChunk鏈表中是否已經有了可用的
SMemoryChunk:
...
if(!m_ptrFirstChunk)
...
在最初始進入循環,此條件不成立。那么,我們為一些內部的成員進行關聯。
...
m_ptrFirstChunk = SetChunkDefaults(&(ptrNewChunks[0])) ;
m_ptrLastChunk = m_ptrFirstChunk ;
m_ptrCursorChunk = m_ptrFirstChunk ;
...
m_ptrFirstChunk這時指向SMemoryChunk中的第一個元素。每一個SMemoryChunk管理的內存塊大小由m_sMemoryChunkSize指定。這些內存塊來自於原始內存塊,偏移量
uiMemOffSet指示着每一個SMemoryChunk所管理的內存起始點處於原始內存塊的何處。
uiMemOffSet = (i * ((unsigned int) m_sMemoryChunkSize)) ;
m_ptrLastChunk->Data = &(ptrNewMemBlock[uiMemOffSet]) ;
額外的,每個新的SMemoryChunk都將被指定為新的m_ptrLastChunk。
...
m_ptrLastChunk->Next = ptrNewChunk ;
m_ptrLastChunk = ptrNewChunk ;
...
經過循環之后,內存池中的SMemoryChunk鏈表將被成功的與原始內存塊關聯。
最終,我們重新計算每一個SMemoryChunk能管理到的內存尺寸。這個步驟相當耗時,並且必須在每次從OS附加新的內存到內存池后調用。所有被計算出的尺寸,將被DataSize成員持有。
/******************
RecalcChunkMemorySize
******************/
bool CMemoryPool::RecalcChunkMemorySize(SMemoryChunk *ptrChunk,
unsigned int uiChunkCount)
{
unsigned int uiMemOffSet = 0 ;
for(unsigned int i = 0; i < uiChunkCount; i++)
{
if(ptrChunk)
{
uiMemOffSet = (i * ((unsigned int) m_sMemoryChunkSize)) ;
ptrChunk->DataSize =
(((unsigned int) m_sTotalMemoryPoolSize) - uiMemOffSet) ;
ptrChunk = ptrChunk->Next ;
}
else
{
assert(false && "Error : ptrChunk == NULL") ;
return false ;
}
}
return true ;
}
在RecalcChunkMemorySize之后,每一個SMemoryChunk將知道自己需要釋放多大的內存。因此,這使得 確定某個SMemoryChunk能否持有一個指定大小的內存 將變得非常容易:當DataSize
成員大於或等於請求的內存尺寸並且UsedSize
成員值為0,這時此SMemoryChunk將能夠滿足用戶的需要。讓我們來看一個具體的例子來加深對這個機制的理解,假設內存池為600字節,並且每個SMemoryChunk為100字節。
第三步:向內存池請求內存
現在,如果用戶向內存池請求內存,那會發生什么呢?最開始,所有的SMemoryChunk在內存池中都是閑置可用狀態:
讓我們看看GetMemory函數吧:
/******************
GetMemory
******************/
void *CMemoryPool::GetMemory(const std::size_t &sMemorySize)
{
std::size_t sBestMemBlockSize = CalculateBestMemoryBlockSize(sMemorySize) ;
SMemoryChunk *ptrChunk = NULL ;
while(!ptrChunk)
{
// 搜索是否有符合條件的SMemoryChunk?
ptrChunk = FindChunkSuitableToHoldMemory(sBestMemBlockSize) ;
if(!ptrChunk)
{
// 沒有SMemoryChunk符合條件
// 內存池太小了,需要向OS申請新的內存
sBestMemBlockSize = MaxValue(sBestMemBlockSize, CalculateBestMemoryBlockSize(m_sMinimalMemorySizeToAllocate)) ;
AllocateMemory(sBestMemBlockSize) ;
}
}
// 一個合適的SMemoryChunk被找到
// 校正其 TotalSize/UsedSize 成員的值
m_sUsedMemoryPoolSize += sBestMemBlockSize ;
m_sFreeMemoryPoolSize -= sBestMemBlockSize ;
m_uiObjectCount++ ;
SetMemoryChunkValues(ptrChunk, sBestMemBlockSize) ;
// 最終將內存指針返回給用戶
return ((void *) ptrChunk->Data) ;
}
當用戶向內存池發出請求,內存池搜索SMemoryChunk鏈表,並在其中找到滿足條件的SMemoryChunk,“滿足條件”意味着:
1、DataSize必須大於或等於請求的大小
2、UsedSize必須為0
FindChunkSuitableToHoldMemory
如果其返回NULL,那么就表示在內存池中沒有可用的內存。這將會引發AllocateMemory函數的調用(前述),此函數會向OS申請更多的內存。
如果返回非NULL,那么便找到了可用的SMemoryChunk。
示例
假設,用戶向內存池申請250字節:
如你所見,每一個SMemoryChunk管理100字節,所以,250字節並不是100的整數倍。這會引發什么情況呢?GetMemory
將會返回指向第一個SMemoryChunk的指針,並設置其的UsedSize成員為300字節,因為300是100的整數倍數值中最小的,並且其大於250。多出的50字節稱為"memory overhead".
當FindChunkSuitableToHoldMemory
尋找可用的SMemoryChunk時,它將只會從一個閑置的SMemoryChunk跳到另一個閑置的SMemoryChunk。這意味着,如果又有申請內存的請求達到,例子中的第四個SMemoryChunk將是尋找的起始點。
如何使用代碼
代碼的使用簡單而直接:
只需要在你的程序中包含"CMemoryPool.h",並附加源碼文件至你的IDE/makefile:
- CMemoryPool.h
- CMemoryPool.cpp
- IMemoryBlock.h
- SMemoryChunk.h
你需要創建一個CMemoryPool實例,並從中分配內存。所有的內存池配置都在CMemoryPool的構造函數中被完成。
使用示例
MemPool::CMemoryPool *g_ptrMemPool = new MemPool::CMemoryPool() ;
char *ptrCharArray = (char *) g_ptrMemPool->GetMemory(100) ;
...
g_ptrMemPool->FreeMemory(ptrCharArray, 100) ;
delete g_ptrMemPool ;
興趣點
內存診斷
你可以調用WriteMemoryDumpToFile函數來輸出內存診斷信息文件。讓我們看下源碼附帶的MyTestClass_OPOverload類的構造函數。(此類重載了new和delete操作,使用了內存池操作)
MyTestClass_OPOverload()
{
m_cMyArray[0] = 'H' ;
m_cMyArray[1] = 'e' ;
m_cMyArray[2] = 'l' ;
m_cMyArray[3] = 'l' ;
m_cMyArray[4] = 'o' ;
m_cMyArray[5] = NULL ;
m_strMyString = "This is a small Test-String" ;
m_iMyInt = 12345 ;
m_fFloatValue = 23456.7890f ;
m_fDoubleValue = 6789.012345 ;
Next = this ;
}
MyTestClass *ptrTestClass = new MyTestClass ;
g_ptrMemPool->WriteMemoryDumpToFile("MemoryDump.bin") ;
讓我們看看內存診斷文件的內容:
如你所見,這是MyTestClass_OPOverload所有的成員在內存中的表示。
速度測試
我在windows下完成了一個簡單的速度測試(使用timeGetTime()),結果顯示內存池的使用可以大大增加程序的速度。所有的測試均使用vs2003,debug模式編譯(測試機器:Intel Pentium IV Processor (32 bit), 1GB RAM, MS Windows XP Professional)
//Array-test (Memory Pool):
for(unsigned int j = 0; j < TestCount; j++)
{
// ArraySize = 1000
char *ptrArray = (char *) g_ptrMemPool->GetMemory(ArraySize) ;
g_ptrMemPool->FreeMemory(ptrArray, ArraySize) ;
}
//Array-test (Heap):
for(unsigned int j = 0; j < TestCount; j++)
{
// ArraySize = 1000
char *ptrArray = (char *) malloc(ArraySize) ;
free(ptrArray) ;
}
//Class-Test for MemoryPool and Heap (重載了new與delete)
for(unsigned int j = 0; j < TestCount; j++)
{
MyTestClass *ptrTestClass = new MyTestClass ;
delete ptrTestClass ;
}
關於代碼
代碼在ms windows與linux的如下c++編譯器通過測試:
- Microsoft Visual C++ 6.0
- Microsoft Visual C++ .NET 2003
- MinGW (GCC) 3.4.4 (Windows)
- GCC 4.0.X (Debian GNU Linux)
vc6.0的項目文件與vs2003的項目文件已經包含在源碼中。在64位的環境下使用應該沒有問題。
注意:此內存池並非線程安全的。
待辦事項
此內存池實現遠遠不夠完善,待辦事項如下:
1、對於海量的內存,memory overhead可能很大
2、一些CalculateNeededChunks
函數的調用可以通過重構某些函數來被剝離,之后速度可能會更快。
3、更多的穩定性測試(尤其是對長時間運行的程序)
4、線程安全的實現