nei cun

這是我翻譯的文章,來自 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章