C++内存池(C++ Memory Pool)

本文深入探讨了C++中内存池的工作原理、实现细节及应用案例,旨在提升应用程序性能,减少内存碎片并加速内存分配与释放过程。通过详细解释内存池的架构与操作流程,包括内存预分配、内存分割、内存请求与释放机制,以及如何通过内存池管理内存块,文章还展示了内存池在实际应用中的高效表现与优势。

/--------------------MonkeyAndy 翻译,转载请注明出处------------------------/

http://blog.youkuaiyun.com/MonkeyAndy

/--------------------MonkeyAndy 翻译,转载请注明出处------------------------/


目录

  • 前言
  • 它的工作原理是什么?
  • 例子
  • 使用代码
  • 兴趣点
  • 运行效率测量
  • 关于代码

前言

    在C/C++中(通过malloc或者new)来进行内存分配会花费很多时间。
    更糟的是,随着运行时间的推移,内存碎片会逐渐增加,因此应用程序的性能就会降低,当其运行在一个很长时间段 和/或 执行大量的内存(释放)分配时。特别是如果你进行频繁的极少量内存分配,堆将变的支离破碎。( 注:动态分配内存内存开辟是在堆中

    解决方案:你自己的内存池

    (可能)解决方案可以是一个内存池。
      一个“内存池”,在启动时的内存分配一个大的一块空间,然后根据分配内存的申请将块分割成更小的块。每次你从池中请求内存,它是从先前分配的块中申请,而不是从操作系统。最大的优点是:
     
  •     很少(或没有)堆碎片
  •     速度比正常的内存分配(释放)要快(eg.通过 malloc, new)
此外,还有下面的好处:
  •     检查一个任意的指针是否在内存池中
  •     为你的硬盘驱动器写一个“堆回收站”(对于后续调试非常有益)
  •     一些“内存泄漏检测”:当你还没有释放所有以前分配的内存时,内存池将抛出一个断言

它的工作原理是什么?

让我们来看看内存池UML架构


                              Memory Pool UML schema

这个架构只显示的CMemoryPool类的一小部分,详细的描述请参见程序生成器文档。
因此,它是实际如何工作?

关于的MemoryChunks一个词

正如你可以从UML架构看到,内存池管理一个SMemoryChunk 结构体指针(m_ptrFirstChunkm_ptrLastChunk, and m_ptrCursorChunk)。这些块建立起一个内存块链表。每一个指向链表中的下一个块。当一个内存块向操作系统申请内存时,它将完全由SMemoryChunks管理。让我们仔细看看在这样一个块:

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 ;

每块持有一个指针指向:

  • 小块的内存(Data),
  • 该块中有效的总的内存大小(DataSize)
  • 实际使用的内存大小(UsedSize)
  • 链表中指向下一个内存块中的指针(Next)

第一步:预分配内存

当调用CMemoryPool的构造函数时,内存池会从操作系统分配第一块(大)内存块。
/******************
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
******************/
<CODE>bool</CODE> CMemoryPool::AllocateMemory(const std::size_t &sMemorySize)
{
  std::size_t sBestMemBlockSize = CalculateBestMemoryBlockSize(sMemorySize) ;
  // allocate from Operating System
  TByte *ptrNewMemBlock = (TByte *) malloc(sBestMemBlockSize) ;
  ...

那么,内存池 如何管理数据?

第二步:分配的内存的分割

如前所述,内存池管理在SMemoryChunks中管理所有的数据。在从操作系统请求内存后,我们所建立的内存池中的块和实际的内存块还没有联系。


                              
                            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()将会根据现有内存的总量计算需要管理的内存块的数量。在通过malloc分配完内存块后,ptrNewChunks  指向一个SMemoryChunks的数组。请注意,数组中的块中目前只有垃圾数据,因为我们还没有分配任何有意义的数据块成员。内存池 “堆” 看起来像这样:


 Memory Pool after SMemoryChunk allocation

尽管如此,仍没有数据块和实际内存块之间的连接(Still, there is no connection between the data block and the chunks. )。但AllocateMemory()将管理它。LinkChunksToData()将会最终将内存块和内存池中块链接在一起。这也将有效的数据分配到每块成员...

//(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) ;
}

让我们一步一步的看这个重要的方法:第一线检查,如果已经有有效的块在列表中:

  
...
m_ptrFirstChunk = SetChunkDefaults(&(ptrNewChunks[0])) ;
m_ptrLastChunk = m_ptrFirstChunk ;
m_ptrCursorChunk = m_ptrFirstChunk;
...

m_ptrFirstChunk 现在指向 块数组中的 第一块。每块精确的管理   m_sMemoryChunkSize   字节的内存块。 “偏移”值 将被计算出,使每块将指向一个内存块的特定部分。


  
uiMemOffSet = (i * ((unsigned int) m_sMemoryChunkSize)) ;
m_ptrLastChunk->Data = &(ptrNewMemBlock[uiMemOffSet]) ;

此外,每一个数组中的新的 SMemoryChunk 将会被添加到链表的最后一个元素(最终将成为最后一个元素本身):
  ...
  m_ptrLastChunk->Next = ptrNewChunk ;
  m_ptrLastChunk = ptrNewChunk ;
  ...
在随后的 for 循环中,内存池将为 数组中的所有块 陆续分配到有效的数据。


Memory and chunks linked together, pointing to valid data

最后,我们必须重新计算每个块可以管理的内存总量。这是相当费时的,而且必须在每一次新的内存从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 后 ,每块都知道其所指向的内存的空闲内存。因此,它变得非常容易去确定一个内存块是否能够容纳一个特定的内存量:当 DataSize  成员大于(或等于)请求的内存总量并且UsedSize 成员为0时,数据块能够容纳下请求的内存,最后,内存分割完成。为了使问题更加具体,我们假设内存池含有 600 字节 ,每块 100字节。



Memory segmentation finished. Each chunk manages exactly 100 bytes

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

如果用户从内存池中请求内存的话,会发生什么?
最初,所有的内存池中的数据是可以有效的:



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) ;
    <CODE>if</CODE>(!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->

当用户从内存池中请求内存时,它会从块的链表中查找一个能够容纳请求的内存总量的块,这意味着:

  • 块的 DataSize 必须大于或等于请求的内存总量
  • 块的 UsedSize 必须为0

这是由FindChunkSuitableToHoldMemory 方法完成。如果其返回 NULL, 则在内存池中没有可用的内存。这将导致AllocateMemory 调用(如上所述),它将从操作系统请求更多的内存。如果返回值不为NULL,一个有效的内存块被发现。 SetMemoryChunkValues ​​将调整该内存块的内部值,最后Data 指针返回给用户...
/******************
SetMemoryChunkValues
******************/
void CMemoryPool::SetMemoryChunkValues(SMemoryChunk *ptrChunk, 
     const std::size_t &sMemBlockSize)
{
  if(ptrChunk) 
  {
    ptrChunk->UsedSize = sMemBlockSize ;
  }
  ...
}

例子

我们假设,用户从我们的内存池中请求250个字节:



 Memory in use


正如你可以看到,每个内存块管理100个字节,因此250字节不适合在那里。接下来会发生什么?GetMemory 将会返回一个指向第一内存块的Data 指针,并且设置它的UsedSize 成员为300字节,因为300字节是内存块可以管理的最小内存量并且>= 250字节。剩余的 (300 - 250 = 50)字节被称为所谓的内存池的 “内存开销”(除了块本身所需的内存),这并不是很糟,因为它仍可使用,(它仍然是在内存池中)。当FindChunkSuitableToHoldMemory 搜索有效的内存块时,它会从一个“空块”跳到另一个“空块”搜索。这意味着,如果有人请求另一个内存块,例子中的第四块(例如在一个300字节的内存池中)将成为下一个“有效”的块。





使用代码

使用代码简单明了:只需include “CMemoryPool.h” 在您的应用程序中,以及添加所有相关文件到您的IDE / Makefile中:
  • CMemoryPool.h
  • CMemoryPool.cpp
  • IMemoryBlock.h
  • SMemoryChunk.h
你只需要创建一个CMemoryPool类的实例,你就可以开始从它分配内存。所有内存池的配置在CMemoryPool的(可选参数)构造函数中完成。查看头文件 (" CMemoryPool.h ")或者Doxygen-doku。所有的文件都有详细的文档。

使用例子

MemPool::CMemoryPool *g_ptrMemPool = new MemPool::CMemoryPool() ;

char *ptrCharArray = (char *) g_ptrMemPool->GetMemory(100) ;
...
g_ptrMemPool->FreeMemory(ptrCharArray, 100) ;

delete g_ptrMemPool ;


兴趣点

内存转存

你可以通过WriteMemoryDumpToFile(strFileName)在任何时间写一个“内存转存”到你的硬盘。我们看下 简单的test-class类的构造函数(使用重载的newdelete操作符在内存池中)

/******************
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") ;

看下内存转存文件  memory dump-file (" MemoryDump.bin "):




正如你可以看到,这些值都从是内存转储中的MyTestClass类成员得到的。显然,“Hello”字符串和 整数m_iMyInt (3930 0000 = 0x3039 = 12345 decimal) 等都出现在这里。这非常有利于事后研究。

运行效率测量

我已经通过timeGetTime()在Windows下做了一些非常简单的速度测试,结果显示使用内存池可以大大的提高应用程序的执行效率。所有的测试为在Microsoft Visual Studio .NET 2003 debug build 环境下(测试机器:Intel Pentium IV Processor (32 bit), 1GB RAM, MS Windows XP Professional





                             Results for the "array-test"



  
//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)


About the code...

The code has been tested under MS Windows and Linux with the following C++ compiler(s):

  • Microsoft Visual C++ 6.0
  • Microsoft Visual C++ .NET 2003
  • MinGW (GCC) 3.4.4 (Windows)
  • GCC 4.0.X (Debian GNU Linux)

Project files for Microsoft Visual C++ 6.0 (*.dsw*.dsp) and Microsoft Visual C++ .NET 2003 (*.sln*.vcproj) are included in the download. This memory pool uses only ANSI/ISO C++, so it should compile on any OS with a decent C++ compiler. There should be no problem using it on a 64-bit processor.

Note: The memory pool is not thread-safe!


ToDo

This memory pool is far from being perfect ;-) The ToDo-list includes:

  • For huge amounts of memory, the memory-"overhead" can be quite large.
  • Some CalculateNeededChunks calls can be stripped off by redesigning some methods => Even more speed up. ;-)
  • More stability tests (especially for very long-running applications).
  • Make it thread-safe.


Histroy

  • 05.09.2006: Initial release.

EoF

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

DanDanger2000

Software Developer

Germany Germany

Member







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值