這是我翻譯的文章,來自 Code Project,
原文作者: DanDanger2000.
原文鏈接: http://www.codeproject.com/cpp/MemoryPool.asp
C++ 內存池
l 下載示例工程 – 105Kb
l 下載源代碼 – 17.3Kb
目錄
l 引言
l 它怎樣工作
l 示例
l 使用這些代碼
l 好處
l 關於代碼
l ToDo
l 歷史
引言
C/C++的內存分配(通過malloc或new)可能需要花費很多時。
更糟糕的是,隨着時間的流逝,內存(memory)將形成碎片,所以一個應用程序的運行會越來越慢當它運行了很長時間和/或執行了很多的內存分配(釋放)操作的時候。特別是,你經常申請很小的一塊內存,堆(heap)會變成碎片的。
解決方案:你自己的內存池
一個(可能的)解決方法是內存池(Memory Pool)。
在啓動的時候,一個”內存池”(Memory Pool)分配一塊很大的內存,並將會將這個大塊(block)分成較小的塊(smaller chunks)。每次你從內存池申請內存空間時,它會從先前已經分配的塊(chunks)中得到,而不是從操作系統。最大的優勢在於:
l 非常少(幾沒有) 堆碎片
l 比通常的內存申請/釋放(比如通過malloc, new等)的方式快
另外,你可以得到以下好處:
l 檢查任何一個指針是否在內存池裏
l 寫一個”堆轉儲(Heap-Dump)”到你的硬盤(對事後的調試非常有用)
l 某種”內存泄漏檢測(memory-leak detection)”:當你沒有釋放所有以前分配的內存時,內存池(Memory Pool)會拋出一個斷言(assertion).
它怎樣工作
讓我們看一看內存池(Memory Pool)的UML模式圖:
這個模式圖只顯示了類CMemoryPool的一小部分,參看由Doxygen生成的文檔以得到詳細的類描述。
一個關於內存塊(MemoryChunks)的單詞
你應該從模式圖中看到,內存池(Memory Pool)管理了一個指向結構體SMemoryChunk (m_ptrFirstChunk, m_ptrLastChunk, and m_ptrCursorChunk)的指針。這些塊(chunks)建立一個內存塊(memory chunks)的鏈表。各自指向鏈表中的下一個塊(chunk)。當從操作系統分配到一塊內存時,它將完全的被SMemoryChunks管理。讓我們近一點看看一個塊(chunk)。
typedef struct SMemoryChunk
...{
TByte *Data ; // The actual Data
std::size_t DataSize ; // Size of the "Data"-Block
std::size_t UsedSize ; // actual used Size
bool IsAllocationChunk ; // true, when this MemoryChunks
// Points to a "Data"-Block
// which can be deallocated via "free()"
SMemoryChunk *Next ; // Pointer to the Next MemoryChunk
// in the List (may be NULL)
} SmemoryChunk;
每個塊(chunk)持有一個指針,指針指向:
l 一小塊內存(Data),
l 從塊(chunk)開始的可用內存的總大小(DataSize),
l 實際使用的大小(UsedSize),
l 以及一個指向鏈表中下一個塊(chunk)的指針。
第一步:預申請內存(pre-allocating the memory)
當你調用CmemoryPool的構造函數,內存池(Memory Pool)將從操作系統申請它的第一塊(大的)內存塊(memory-chunk)
/**//*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最終完成了從操作系統申請內存。
/**//******************
AllocateMemory
******************/
bool CMemoryPool::AllocateMemory(const std::size_t &sMemorySize)
...{
std::size_t sBestMemBlockSize = CalculateBestMemoryBlockSize(sMemorySize) ;
// allocate from Operating System
TByte *ptrNewMemBlock = (TByte *) malloc (sBestMemBlockSize) ;
...
那麼,是如何管理數據的呢?
第二步:已分配內存的分割(segmentation of allocated memory)
正如前面提到的,內存池(Memory Pool)使用SMemoryChunks管理所有數據。從OS申請完內存之後,我們的塊(chunks)和實際的內存塊(block)之間就不存在聯繫:
Memory Pool after initial allocation
我們需要分配一個結構體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()負責計算爲管理已經得到的內存需要的塊(chunks)的數量。分配完塊(chunks)之後(通過malloc),ptrNewChunks將指向一個SmemoryChunks的數組。注意,數組裏的塊(chunks)現在持有的是垃圾數據,因爲我們還沒有給chunk-members賦有用的數據。內存池的堆(Memory Pool-"Heap"):
Memory Pool after SMemoryChunk allocation
還是那句話,數據塊(data block)和chunks之間沒有聯繫。但是,AllocateMemory()會照顧它。LinkChunksToData()最後將把數據塊(data block)和chunks聯繫起來,並將爲每個chunk-member賦一個可用的值。
// (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]) ;
// The first Chunk assigned to the new Memory-Block will be
// a "AllocationChunk". This means, this Chunks stores the
// "original" Pointer to the MemBlock and is responsible for
// "free()"ing the Memory later....
if(!bAllocationChunkAssigned)
...{
m_ptrLastChunk->IsAllocationChunk = true ;
bAllocationChunkAssigned = true ;
}
}
return RecalcChunkMemorySize(m_ptrFirstChunk, m_uiMemoryChunkCount) ;
}
讓我們一步步地仔細看看這個重要的函數:第一行檢查鏈表裏是否已經有可用的塊(chunks):
...
if(!m_ptrFirstChunk)
...
我們第一次給類的成員賦值:
...
m_ptrFirstChunk = SetChunkDefaults(&(ptrNewChunks[0])) ;
m_ptrLastChunk = m_ptrFirstChunk ;
m_ptrCursorChunk = m_ptrFirstChunk ;
...
m_ptrFirstChunk現在指向塊數組(chunks-array)的第一個塊,每一個塊嚴格的管理來自內存(memory block)的m_sMemoryChunkSize個字節。一個”偏移量”(offset)——這個值是可以計算的所以每個(chunk)能夠指向內存塊(memory block)的特定部分。
uiMemOffSet = (i * ((unsigned int) m_sMemoryChunkSize)) ;
m_ptrLastChunk->Data = &(ptrNewMemBlock[uiMemOffSet]) ;
另外,每個新的來自數組的SmemoryChunk將被追加到鏈表的最後一個元素(並且它自己將成爲最後一個元素):
...
m_ptrLastChunk->Next = ptrNewChunk ;
m_ptrLastChunk = ptrNewChunk ;
...
在接下來的"for loop" 中,內存池(memory pool)將連續的給數組中的所有塊(chunks)賦一個可用的數據。
Memory and chunks linked together, pointing to valid data
最後,我們必須重新計算每個塊(chunk)能夠管理的總的內存大小。這是一個費時的,但是在新的內存追加到內存池時必須做的一件事。這個總的大小將被賦值給chunk的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之後,每個chunk都知道它指向的空閒內存的大小。所以,將很容易確定一個chunk是否能夠持有一塊特定大小的內存:當DataSize成員大於(或等於)已經申請的內存大小以及DataSize成員是0,於是chunk有能力持有一塊內存。最後,內存分割完成了。爲了不讓事情太抽象,我們假定內存池(memory pool )包含600字節,每個chunk持有100字節。
Memory segmentation finished. Each chunk manages exactly 100 bytes
第三步:從內存池申請內存(requesting memory from the memory pool)
那麼,如果用戶從內存池申請內存會發生什麼?最初,內存池裏的所有數據是空閒的可用的:
All memory blocks are available
我們看看GetMemory:
/**//******************
GetMemory
******************/
void *CMemoryPool::GetMemory(const std::size_t &sMemorySize)
...{
std::size_t sBestMemBlockSize = CalculateBestMemoryBlockSize(sMemorySize) ;
SMemoryChunk *ptrChunk = NULL ;
while(!ptrChunk)
...{
// Is a Chunks available to hold the requested amount of Memory ?
ptrChunk = FindChunkSuitableToHoldMemory(sBestMemBlockSize) ;
if (!ptrChunk)
...{
// No chunk can be found
// => Memory-Pool is to small. We have to request
// more Memory from the Operating-System....
sBestMemBlockSize = MaxValue(sBestMemBlockSize,
CalculateBestMemoryBlockSize(m_sMinimalMemorySizeToAllocate)) ;
AllocateMemory(sBestMemBlockSize) ;
}
}
// Finally, a suitable Chunk was found.
// Adjust the Values of the internal "TotalSize"/"UsedSize" Members and
// the Values of the MemoryChunk itself.
m_sUsedMemoryPoolSize += sBestMemBlockSize ;
m_sFreeMemoryPoolSize -= sBestMemBlockSize ;
m_uiObjectCount++ ;
SetMemoryChunkValues(ptrChunk, sBestMemBlockSize) ;
// eventually, return the Pointer to the User
return ((void *) ptrChunk->Data) ;
}
當用戶從內存池中申請內存是,它將從鏈表搜索一個能夠持有被申請大小的chunk。那意味着:
l 那個chunk的DataSize必須大於或等於被申請的內存的大小;
l 那個chunk的UsedSize 必須是0。
這由 FindChunkSuitableToHoldMemory 方法完成。如果它返回NULL,那麼在內存池中沒有可用的內存。這將導致AllocateMemory 的調用(上面討論過),它將從OS申請更多的內存。如果返回值不是NULL,一個可用的chunk被發現。SetMemoryChunkValues會調整chunk成員的值,並且最後Data指針被返回給用戶...
/**//******************
SetMemoryChunkValues
******************/
void CMemoryPool::SetMemoryChunkValues(SMemoryChunk *ptrChunk,
const std::size_t &sMemBlockSize)
...{
if(ptrChunk)
...{
ptrChunk->UsedSize = sMemBlockSize ;
}
...
}
示例
假設,用戶從內存池申請250字節:
Memory in use
如我們所見,每個內存塊(chunk)管理100字節,所以在這裏250字節不是很合適。發生了什麼事?Well,GetMemory 從第一個chunk返回 Data指針並把它的UsedSize設爲300字節,因爲300字節是能夠被管理的內存的最小值並大於等於250。那些剩下的(300 - 250 = 50)字節被稱爲內存池的"memory overhead"。這沒有看起來的那麼壞,因爲這些內存還可以使用(它仍然在內存池裏)。
當FindChunkSuitableToHoldMemory搜索可用chunk時,它僅僅從一個空的chunk跳到另一個空的chunk。那意味着,如果某個人申請另一塊內存(memory-chunk),第四塊(持有300字節的那個)會成爲下一個可用的("valid") chunk。
Jump to next valid chunk
使用代碼
使用這些代碼是簡單的、直截了當的:只需要在你的應用裏包含"CMemoryPool.h",並添加幾個相關的文件到你的IDE/Makefile:
CMemoryPool.h
CMemoryPool.cpp
IMemoryBlock.h
SMemoryChunk.h
你只要創建一個CmemoryPool類的實例,你就可以從它裏面申請內存。所有的內存池的配置在CmemoryPool類的構造函數(使用可選的參數)裏完成。看一看頭文件("CMemoryPool.h")或Doxygen-doku。所有的文件都有詳細的(Doxygen-)文檔。
應用舉例
MemPool::CMemoryPool *g_ptrMemPool = new MemPool::CMemoryPool() ;
char *ptrCharArray = (char *) g_ptrMemPool->GetMemory(100) ;
...
g_ptrMemPool->FreeMemory(ptrCharArray, 100) ;
delete g_ptrMemPool ;
好處
內存轉儲(Memory dump)
你可以在任何時候通過WriteMemoryDumpToFile(strFileName)寫一個"memory dump"到你的HDD。看看一個簡單的測試類的構造函數(使用內存池重載了new和delete運算符):
/**//******************
Constructor
******************/
MyTestClass::MyTestClass()
...{
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") ;
看一看內存轉儲文件("MemoryDump.bin"):
如你所見,在內存轉儲裏有MyTestClass類的所有成員的值。明顯的,"Hello"字符串(m_cMyArray)在那裏,以及整型數m_iMyInt (3930 0000 = 0x3039 = 12345 decimal)等等。這對調式很有用。
速度測試
我在Windows平臺上做了幾個非常簡單的測試(通過timeGetTime()),但是結果說明內存池大大提高了應用程序的速度。所有的測試在Microsoft Visual Studio .NET 2003的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) ;
}
Results for the "array-test
//Class-Test for MemoryPool and Heap (overloaded new/delete)
//Class-Test for MemoryPool and Heap (overloaded new/delete)
for(unsigned int j = 0; j < TestCount; j++)
...{
MyTestClass *ptrTestClass = new MyTestClass ;
delete ptrTestClass ;
}
Results for the "classes-test" (overloaded new/delete operators)
關於代碼
這些代碼在Windows和Linux平臺的下列編譯器測試通過:
Microsoft Visual C++ 6.0
Microsoft Visual C++ .NET 2003
MinGW (GCC) 3.4.4 (Windows)
GCC 4.0.X (Debian GNU Linux)
Microsoft Visual C++ 6.0(*.dsw, *.dsp)和Microsoft Visual C++ .NET 2003 (*.sln, *.vcproj)的工程文件已經包含在下載中。內存池僅用於ANSI/ISO C++,所以它應當在任何OS上的標準的C++編譯器編譯。在64位處理器上應當沒有問題。
注意:內存池不是線程安全的。
ToDo
這個內存池還有許多改進的地方;-) ToDo列表包括:
l 對於大量的內存,memory-"overhead"能夠足夠大。
l 某些CalculateNeededChunks調用能夠通過從新設計某些方法而去掉
l 更多的穩定性測試(特別是對於那些長期運行的應用程序)
l 做到線程安全。
歷史
l 05.09.2006: Initial release
EoF
DanDanger2000
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/060/archive/2006/10/08/1326025.aspx