原文連結:http://blog.csdn.net/060/archive/2006/10/08/1326025.aspx
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