内存复用的三种技术(内存池技术的原理与实现)

序言

  最近在网上看到了几篇篇讲述内存池技术的文章,有一篇是有IBM中国研发中心的人写的,写的不错~~文章地址在本篇blog最后。原文的讲述比我的要清晰很多,我在这只是把我的一些理解和遇到的一些问题和大家分享一下~~

一、为什么要使用内存池技术呢

  主要有两个原因:1、减少new、delete次数,减少运行时间;2、避免内存碎片。

  1、效率

  c语言中使用malloc/free来分配内存,c 中使用new/delete来分配内存,他们的内存申请与释放都是与操作系统进行交互的。具体的内容在严蔚敏数据结构的第八章有相关讲述,主要就是系统要维护一个内存链表,当有一个内存申请过来时,根据相应的分配算法在链表中找个一个合适的内存分配给它。这些算法有的是分配最先找到的不小于申请内存的内存块,有的是分配最大的内存块,有的是分配最接近申请内存大小的内存块。分配的内存块可能会大于所申请的内存大小,这样还有进行切割,将剩余的内存插入到空闲链表中。当释放的时候,系统可能要对内存进行整理,判断free的内存块的前后是否有空闲,若有的话还要进行合并。此外,new/delete还要考虑多线程的情况。总之一句话,调用库中的内存分配函数,十分的耗时~~

  2、内存碎片

  什么是内存碎片内,从字面意思就很好理解了,就是内存不再是一整块的了,而是碎了。因为连续的这种new/delete操作,一大块内存肯能就被分割成小的内存分配出去了,这些小的内存都是不连续的。当你再去分配大的连续内存的时候,尽管剩余内存的总和可能大于所要分配的内存大小,但系统就找不到连续的内存了,所以导致分配错误。malloc的时候会导致返回NULL,而new的时候再vc6.0中返回NULL,vs2003以上则是抛出异常。

二、原理

  要解决上述两个问题,最好的方法就是内存池技术。具体方法就是大小固定、提前申请、重复利用。

  因为内存的申请和释放是很低效的,所以我们只在开始时申请一块大的内存(在该块内存不够用时再二次分配),然后每次需要时都从这块内存中取出,并标记下这块内存被用了,释放时标记此内存被释放了。释放时,并不真的把内存释放给操作系统,只要在一大块内存都空闲的时候,才释放给操作系统。这样,就减少了new/delete的操作次数,从而提高了效率。

  在调用内存分配函数的时候,大部分时间所分配的内存大小都是一定的,所以可以采用每次都分配固定大小的内存块,这样就避免了内存碎片产生的可能。

三、具体实现

  我所采用的内存池的构造方法完全是按照文章1所介绍的方法,内存池的结构图如下:

内存复用的三种技术(内存池技术的原理与实现)(1)

  如图所示MemoryPool是一个内存池类,其中pBlock是一个指向了一个内存块的指针,nUintSzie是分配单元的大小,nInitSize是第一次分配时向系统申请的内存的大小,nGrouSize是后面每次向系统申请的内存的大小。

  MemoryBloc代表一个内存块单元,它有两部分构成,一部分时MemoryBlock类的大小,另一部分则是实际的内存部分。一个MemoryBlock的内存是在重载的new操作符中分配的,如下所示: 

void* MemoryBlock::operator new(size_t, int nUnitSize,int nUnitAmount )

{

return ::operator new( sizeof(MemoryBlock) nUnitSize * nUnitAmount );

}

    MemoryBlock内中,nSize代码该内存块的大小(系统分配内存大小-MemoryBlock类的大小),nFree是空闲内存单元的个数,nFirst代表的是下一个要分配的内存单元的序号。aData是用来记录待分配内存的位置的。因为要分配的内存是在new中一起向系统申请的,并没有一个指针指向这块内存的位置,但它的位置就在MemoryBlock这个类的地址开始的,所以可以用MemoryBlock的最后一个成员的位置来表示待分配内存的位置。

  带分配内存中,是以nUnitSize为单位的,一个内存单元的头两个字节都记录了下一个要分配的内存单元的序号,序号从0开始。这样实际也就构成了一个数组链表。由MemoryBlock的构造函数来完成这个链表的初始化工作:

MemoryBlock::MemoryBlock( int nUnitSize,int nUnitAmount )

: nSize (nUnitAmount * nUnitSize),

nFree (nUnitAmount - 1), //构造的时候,就已将第一个单元分配出去了,所以减一

nFirst (1), //同上

pNext (NULL)

{

//初始化数组链表,将每个分配单元的下一个分配单元的序号写在当前单元的前两个字节中

char* pData = aData;

//最后一个位置不用写入

for( int i = 1; i < nSize - 1; i )

{

(*(USHORT*)pData) = i;

pData = nUnitSize;

}

}

  在MemoryPool的Alloc()中,遍历block链表,找到nFree大于0的block,从其上分配内存单元。然后将nFree减一,修改nFirst的值。

  在MemoryPool的Free(pFree)函数中,根据pFree的值,找到它所在的内存块,然后将它的序号作为nFirst的值(因为它绝对是空闲的),在pFree的头两个字节中写入原来nFirst的值。然后要判断,该block是否全部为free,方法是检测nFree * nUnitSize == nSize。若是,则向系统释放内存,若不是,则将该block放到链表的头部,因为该block上一定含有空隙的内存单元,这样可以减少分配时遍历链表所消耗的时间。

四、使用

  内存池一般都是作为一个类的静态成员,或者全局变量。使用时,重载new操作符,使其到MemoryPool中去分配内存,而不是向系统申请。这样,一个类的所有对象都在一个内存池中开辟空间。

void CTest::operator delete( void* pTest )

{

Pool.Free(pTest);

}

void* CTest::operator new(size_t)

{

return (CTest*)Pool.Alloc();

}

五、代码

MemoryPool.h

View Code

 MemoryPool.cpp

View Code

 CTest.cpp

View Code

六、问题

  在编写代码时,遇到了一些小问题,现与大家分享如下:

  1、重载new操作符时,编译器要求是第一个参数必须是size_t,返回值必须是void*;free的第一个参数必须是void*.

  2、一般要在类的成员中重载new操作符,而不要重载全局的new操作符。

  3、一个类中要是重载了一个new操作符,一定要有一个相应类型的delete操作符,可以什么都不干,但必须有,否则在构造函数失败时,找不到对应的delete函数。

例如:  

1

2

static void* operator new (size_t,int nUnitSize,int nUnitAmount);

static void operator delete (void* ,int nUnitSize,int nUnitAmount){};

  4、带参数的new操作符

pBlock = (MemoryBlock*)new(nUnitSize,nInitSize) MemoryBlock(nUnitSize,nUnitSize);

  第一个nUnitSize nInitSize是new操作符的参数,该new操作符是new了一个MemoryBlock对象,在new返回的地址上构造MemoryBlock的对象。

  5、如果在类的内部不能进行静态成员的定义的话,可以只在内部进行声明,在外部定义:

MemoryPool CTest::Pool(sizeof(CTest));

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页