C++ 記憶體池 — C++ Memory Pool 翻譯版

來源:互聯網
上載者:User

原文連結: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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.