C++內存池的管理


原帖與示例代碼地址:http://www.codeproject.com/KB/cpp/MemoryPool.aspx

 

譯者點評:一個簡單的內存池實現,附有源碼,簡單易懂,適合入門。

 

概述

在c/c++中,內存分配(如malloc或new)會使用很多時間。

一個程序會隨着長時間的運行和內存的申請釋放而變得越來越慢,內存也會隨着時間逐漸碎片化。特別是高頻率的進行小內存申請釋放,此問題變得尤其嚴重。

 

解決方案:定制內存池

為解決上述問題,一個(可能的)的解決方案就是使用內存池。

 

“內存池”在初始化時,分配一個大塊內存(稱 原始內存塊),並且將此內存分割為一些小的內存塊。當你需要請求分配內存時,則從內存池中取出事先分配好的內存,而不是向OS申請。內存池最大的優勢在於:

1、極少的(甚至沒有)堆碎片整理

2、較之普通內存分配(如malloc,new),有着更快的速度

 

額外的,你還將獲得如下好處:

1、檢測任意的指針是否指向內存池內

2、生成"heap-dump"

3、各種 內存泄漏 檢測:當你沒有釋放之前申請的內存,內存池將拋出斷言

 

如何工作?

讓我們看看內存池的UML模型圖:

MemoryPool UML schema

圖中簡要的描述了CMemoryPool class,更多的細節請查看源碼中class聲明。

 

那么,CMemoryPool如何實際工作?

 

關於 MemoryChunks

正如你在UML圖中所看到的,內存池維護着一個SMemoryChunk鏈表,並管理着三個指向SMemoryChunk結構的指針(m_ptrFirstChunkm_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。MemoryPool after inital 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函數用於計算需要分配的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鏈表將被成功的與原始內存塊關聯。

Memory and chunks linked togehter

最終,我們重新計算每一個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字節。

Memory-Segmentation finished

 

第三步:向內存池請求內存

現在,如果用戶向內存池請求內存,那會發生什么呢?最開始,所有的SMemoryChunk在內存池中都是閑置可用狀態:

All Memory available

讓我們看看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字節:

Memory in use

如你所見,每一個SMemoryChunk管理100字節,所以,250字節並不是100的整數倍。這會引發什么情況呢?GetMemory將會返回指向第一個SMemoryChunk的指針,並設置其的UsedSize成員為300字節,因為300是100的整數倍數值中最小的,並且其大於250。多出的50字節稱為"memory overhead".

FindChunkSuitableToHoldMemory尋找可用的SMemoryChunk時,它將只會從一個閑置的SMemoryChunk跳到另一個閑置的SMemoryChunk。這意味着,如果又有申請內存的請求達到,例子中的第四個SMemoryChunk將是尋找的起始點。

Jump to next valid chunk

 

如何使用代碼

代碼的使用簡單而直接:

只需要在你的程序中包含"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) ;
}

Speed test Results for the array-test

 
//Class-Test for MemoryPool and Heap (重載了new與delete)
for(unsigned int j = 0; j < TestCount; j++)
{
    MyTestClass *ptrTestClass = new MyTestClass ;
    delete ptrTestClass ;
}

Speed test Results for the classes-test

 

關於代碼

代碼在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、線程安全的實現


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM