C++内存池的管理

 

概述

在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封装了原始内存块的各个部分)

 
 
  1. typedef struct SMemoryChunk  
  2. {  
  3.   TByte *Data ;             // 常规数据指针  
  4.   std::size_t DataSize ;    // 内存块容量  
  5.   std::size_t UsedSize ;    // 内存块当前使用大小  
  6.   bool IsAllocationChunk ;  // 为true时, 内存块已被分配,可用free之类的函数释放  
  7.   SMemoryChunk *Next ;      // 指向内存块链表中的下一个内存块,可能为null  
第一步:预分配内存
当你调用CMemoryPool的构造函数,内存池会向OS申请原始内存块。
  1. /****************** 
  2. Constructor 
  3. ******************/  
  4. CMemoryPool::CMemoryPool(const std::size_t &sInitialMemoryPoolSize,  
  5.                          const std::size_t &sMemoryChunkSize,  
  6.                          const std::size_t &sMinimalMemorySizeToAllocate,  
  7.                          bool bSetMemoryData)  
  8. {  
  9.   m_ptrFirstChunk  = NULL ;  
  10.   m_ptrLastChunk   = NULL ;  
  11.   m_ptrCursorChunk = NULL ;  
  12.   m_sTotalMemoryPoolSize = 0 ;  
  13.   m_sUsedMemoryPoolSize  = 0 ;  
  14.   m_sFreeMemoryPoolSize  = 0 ;  
  15.   m_sMemoryChunkSize   = sMemoryChunkSize ;  
  16.   m_uiMemoryChunkCount = 0 ;  
  17.   m_uiObjectCount      = 0 ;  
  18.   m_bSetMemoryData               = bSetMemoryData ;  
  19.   m_sMinimalMemorySizeToAllocate = sMinimalMemorySizeToAllocate ;  
  20.   // Allocate the Initial amount of Memory from the Operating-System...  
  21.   AllocateMemory(sInitialMemoryPoolSize) ;  
  22. }  
  23. 所有的成员的函数初始化在此完成,最后AllocateMemory将完成向OS申请原始内存块的任务。  
  24. /****************** 
  25. AllocateMemory 
  26. ******************/  
  27. <CODE>bool CMemoryPool::AllocateMemory(const std::size_t &sMemorySize)  
  28. {  
  29.   std::size_t sBestMemBlockSize = CalculateBestMemoryBlockSize(sMemorySize) ;  
  30.   // allocate from Operating System  
  31.   TByte *ptrNewMemBlock = (TByte *) malloc(sBestMemBlockSize) ;  
  32.   ...  

那么,内存池如何来管理这些数据呢?
 
第二步:内存分块
回忆前述,内存池管理使用SMemoryChunk链表来管理数据。在向OS申请原始内存块后,

我们还没有在其上建立SMemoryChunk。

MemoryPool after inital allocation

图中所示的为初始化分配后的内存池。
  1. 我们需要分配一组SMemoryChunk,用于管理原始内存块:  
  2. //(AllocateMemory() continued) :   
  3.   ...  
  4.   unsigned int uiNeededChunks = CalculateNeededChunks(sMemorySize) ;  
  5.   // allocate Chunk-Array to Manage the Memory  
  6.   SMemoryChunk *ptrNewChunks =   
  7.     (SMemoryChunk *) malloc((uiNeededChunks * sizeof(SMemoryChunk))) ;  
  8.   assert(((ptrNewMemBlock) && (ptrNewChunks))   
  9.                            && "Error : System ran out of Memory") ;  
  10.   ...  
  11. CalculateNeededChunks函数用于计算需要分配的SMemoryChunk的数量。分配后,ptrNewChunks指向这组SMemoryChunk。注意,SMemoryChunk中目前只是持有垃圾数据,我们还没有为SMemoryChunk的成员关联至原始内存块。  
  12.    
  13. 最后,AllocateMemory函数将为所有的SMemoryChunk关联至原始内存块。  
  14. //(AllocateMemory() continued) :   
  15.   ...  
  16.   // Associate the allocated Memory-Block with the Linked-List of MemoryChunks  
  17.   return LinkChunksToData(ptrNewChunks, uiNeededChunks, ptrNewMemBlock) ;  
  18.    
  19. 让我们进入LinkChunksToData中一窥究竟:  
  20. /****************** 
  21. LinkChunksToData 
  22. ******************/  
  23. bool CMemoryPool::LinkChunksToData(SMemoryChunk *ptrNewChunks,   
  24.      unsigned int uiChunkCount, TByte *ptrNewMemBlock)  
  25. {  
  26.   SMemoryChunk *ptrNewChunk = NULL ;  
  27.   unsigned int uiMemOffSet = 0 ;  
  28.   bool bAllocationChunkAssigned = false ;  
  29.   for(unsigned int i = 0; i < uiChunkCount; i++)  
  30.   {  
  31.     if(!m_ptrFirstChunk)  
  32.     {  
  33.       m_ptrFirstChunk = SetChunkDefaults(&(ptrNewChunks[0])) ;  
  34.       m_ptrLastChunk = m_ptrFirstChunk ;  
  35.       m_ptrCursorChunk = m_ptrFirstChunk ;  
  36.     }  
  37.     else  
  38.     {  
  39.       ptrNewChunk = SetChunkDefaults(&(ptrNewChunks[i])) ;  
  40.       m_ptrLastChunk->Next = ptrNewChunk ;  
  41.       m_ptrLastChunk = ptrNewChunk ;  
  42.     }  
  43.     uiMemOffSet = (i * ((unsigned int) m_sMemoryChunkSize)) ;  
  44.     m_ptrLastChunk->Data = &(ptrNewMemBlock[uiMemOffSet]) ;  
  45.     // 第一个SMemoryChunk被称为“AllocationChunk”。  
  46.     // 这意味着,它持有原始内存块的指针并能够利用它释放原始内存块  
  47.     if(!bAllocationChunkAssigned)  
  48.     {  
  49.       m_ptrLastChunk->IsAllocationChunk = true ;  
  50.       bAllocationChunkAssigned = true ;  
  51.     }  
  52.   }  
  53.   return RecalcChunkMemorySize(m_ptrFirstChunk, m_uiMemoryChunkCount) ;  
  54. }  
  55. 让我们一步步的来看这个重要的函数:第一行检查在SMemoryChunk链表中是否已经有了可用的  
  56. SMemoryChunk:  
  57.   ...  
  58.   if(!m_ptrFirstChunk)  
  59.   ...  
  60. 在最初始进入循环,此条件不成立。那么,我们为一些内部的成员进行关联。  
  61.  ...  
  62.   m_ptrFirstChunk = SetChunkDefaults(&(ptrNewChunks[0])) ;  
  63.   m_ptrLastChunk = m_ptrFirstChunk ;  
  64.   m_ptrCursorChunk = m_ptrFirstChunk ;  
  65.   ...  
  66. m_ptrFirstChunk这时指向SMemoryChunk中的第一个元素。每一个SMemoryChunk管理的内存块大小由m_sMemoryChunkSize指定。这些内存块来自于原始内存块,偏移量  
  67. uiMemOffSet指示着每一个SMemoryChunk所管理的内存起始点处于原始内存块的何处。  
  68.   uiMemOffSet = (i * ((unsigned int) m_sMemoryChunkSize)) ;  
  69.   m_ptrLastChunk->Data = &(ptrNewMemBlock[uiMemOffSet]) ;  
  70. 额外的,每个新的SMemoryChunk都将被指定为新的m_ptrLastChunk。  
  71.   ...  
  72.   m_ptrLastChunk->Next = ptrNewChunk ;  
  73.   m_ptrLastChunk = ptrNewChunk ;  
  74.   ...  

经过循环之后,内存池中的SMemoryChunk链表将被成功的与原始内存块关联。

Memory and chunks linked togehter

最终,我们重新计算每一个SMemoryChunk能管理到的内存尺寸。这个步骤相当耗时,并且必须在每次从OS附加新的内存到内存池后调用。所有被计算出的尺寸,将被DataSize成员持有。

  1. /****************** 
  2. RecalcChunkMemorySize 
  3. ******************/  
  4. bool CMemoryPool::RecalcChunkMemorySize(SMemoryChunk *ptrChunk,   
  5.                   unsigned int uiChunkCount)  
  6. {  
  7.   unsigned int uiMemOffSet = 0 ;  
  8.   for(unsigned int i = 0; i < uiChunkCount; i++)  
  9.   {  
  10.     if(ptrChunk)  
  11.     {  
  12.       uiMemOffSet = (i * ((unsigned int) m_sMemoryChunkSize)) ;  
  13.       ptrChunk->DataSize =   
  14.         (((unsigned int) m_sTotalMemoryPoolSize) - uiMemOffSet) ;  
  15.       ptrChunk = ptrChunk->Next ;  
  16.     }  
  17.     else  
  18.     {  
  19.      assert(false && "Error : ptrChunk == NULL") ;  
  20.      return false ;  
  21.     }  
  22.   }  
  23.   return true ;  
  24. }  

在RecalcChunkMemorySize之后,每一个SMemoryChunk将知道自己需要释放多大的内存。因此,这使得 确定某个SMemoryChunk能否持有一个指定大小的内存 将变得非常容易:当DataSize成员大于或等于请求的内存尺寸并且UsedSize成员值为0,这时此SMemoryChunk将能够满足用户的需要。让我们来看一个具体的例子来加深对这个机制的理解,假设内存池为600字节,并且每个SMemoryChunk为100字节。

Memory-Segmentation finished

 

第三步:向内存池请求内存

现在,如果用户向内存池请求内存,那会发生什么呢?最开始,所有的SMemoryChunk在内存池中都是闲置可用状态:

All Memory available

让我们看看GetMemory函数吧:

  1. /****************** 
  2. GetMemory 
  3. ******************/  
  4. void *CMemoryPool::GetMemory(const std::size_t &sMemorySize)  
  5. {  
  6.   std::size_t sBestMemBlockSize = CalculateBestMemoryBlockSize(sMemorySize) ;    
  7.   SMemoryChunk *ptrChunk = NULL ;  
  8.   while(!ptrChunk)  
  9.   {  
  10.     // 搜索是否有符合条件的SMemoryChunk?  
  11.     ptrChunk = FindChunkSuitableToHoldMemory(sBestMemBlockSize) ;  
  12.     if(!ptrChunk)  
  13.     {  
  14.       // 没有SMemoryChunk符合条件  
  15.       // 内存池太小了,需要向OS申请新的内存  
  16.       sBestMemBlockSize = MaxValue(sBestMemBlockSize, CalculateBestMemoryBlockSize(m_sMinimalMemorySizeToAllocate)) ;  
  17. AllocateMemory(sBestMemBlockSize) ;  
  18.     }  
  19.   }  
  20.   // 一个合适的SMemoryChunk被找到  
  21.   // 校正其 TotalSize/UsedSize 成员的值  
  22.   m_sUsedMemoryPoolSize += sBestMemBlockSize ;  
  23.   m_sFreeMemoryPoolSize -= sBestMemBlockSize ;  
  24.   m_uiObjectCount++ ;  
  25. SetMemoryChunkValues(ptrChunk, sBestMemBlockSize) ;  
  26.   // 最终将内存指针返回给用户  
  27.   return ((void *) ptrChunk->Data) ;  
  28. }  

当用户向内存池发出请求,内存池搜索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的构造函数中被完成。


使用示例

  1. MemPool::CMemoryPool *g_ptrMemPool = new MemPool::CMemoryPool() ;  
  2. char *ptrCharArray = (char *) g_ptrMemPool->GetMemory(100) ;  
  3. ...  
  4. g_ptrMemPool->FreeMemory(ptrCharArray, 100) ;  
  5. delete g_ptrMemPool ;  

兴趣点
 
内存诊断
你可以调用WriteMemoryDumpToFile函数来输出内存诊断信息文件。让我们看下源码附带的MyTestClass_OPOverload类的构造函数。(此类重载了new和delete操作,使用了内存池操作)
  1. MyTestClass_OPOverload()  
  2.     {  
  3.       m_cMyArray[0] = 'H' ;  
  4.       m_cMyArray[1] = 'e' ;  
  5.       m_cMyArray[2] = 'l' ;  
  6.       m_cMyArray[3] = 'l' ;  
  7.       m_cMyArray[4] = 'o' ;  
  8.       m_cMyArray[5] = NULL ;  
  9.       m_strMyString = "This is a small Test-String" ;  
  10.       m_iMyInt = 12345 ;  
  11.       m_fFloatValue = 23456.7890f ;  
  12.       m_fDoubleValue = 6789.012345 ;  
  13.       Next = this ;  
  14.     }  
  15.    
  16. MyTestClass *ptrTestClass = new MyTestClass ;   
  17. g_ptrMemPool->WriteMemoryDumpToFile("MemoryDump.bin") ;  

让我们看看内存诊断文件的内容:

如你所见,这是MyTestClass_OPOverload所有的成员在内存中的表示。
 
速度测试
我在windows下完成了一个简单的速度测试(使用timeGetTime()),结果显示内存池的使用可以大大增加程序的速度。所有的测试均使用vs2003,debug模式编译(测试机器:Intel Pentium IV Processor (32 bit), 1GB RAM, MS Windows XP Professional
  1. //Array-test (Memory Pool):   
  2. for(unsigned int j = 0; j < TestCount; j++)  
  3. {  
  4.     // ArraySize = 1000  
  5.     char *ptrArray = (char *) g_ptrMemPool->GetMemory(ArraySize)  ;  
  6.     g_ptrMemPool->FreeMemory(ptrArray, ArraySize) ;  
  7. }  
  8. //Array-test (Heap):   
  9. for(unsigned int j = 0; j < TestCount; j++)  
  10. {  
  11.     // ArraySize = 1000  
  12.     char *ptrArray = (char *) malloc(ArraySize)  ;  
  13.     free(ptrArray) ;  
  14. }  

Speed test Results for the array-test
 
  1. //Class-Test for MemoryPool and Heap (重载了new与delete)  
  2. for(unsigned int j = 0; j < TestCount; j++)  
  3. {  
  4.     MyTestClass *ptrTestClass = new MyTestClass ;  
  5.     delete ptrTestClass ;  
  6. }  
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、线程安全的实现



转载:http://blog.csdn.net/tgzjz/article/details/45146323

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值