最近研究了下内存池技术,原以为比较简单,没想到深入之后,发现要想写一个“漂亮”的内存池,要考虑到很多东西,并不简单,下面一一道来。
为什么要使用内存池?
当频繁地用malloc/new申请内存,然后再用free/delete释放内存时,会存在两个主要问题:一是频繁的分配释放内存可能导致系统内存碎片过多;二是分配释放内存花费的时间可能比较多。
内存池的思想:
首先程序通过malloc/new向系统申请一块很大的内存(这块内存因为很大,以致于我们常称它为memory pool)。当你需要分配内存时,你用GetMemory函数从内存池上取一小块给使用者(后文称之为小内存块),当你不需要此小内存块时用FreeMemory函数进行释放,这个小内存块并不归还给系统,而只是还给内存池。
写内存池要做的准备:
我在网上看了不少关于内存池的资料,关于内存池的基本思想是一致的,但是实现方法还是有差距的。这种差距主要是依赖与你的程序需要什么样的内存池。如你所写的内存池所提供的接口是否允许用户设置大块内存的大小以及每一小内存块的大小,是否每次只分配固定大小的小内存块,是否允许当内存池不够分配时实现自动增长以及每次增长的内存大小是否固定等等。所以,我认为内存池与其说是一种技术,更不如是一种解决问题的策略,对待不同的需求,使用不同的实现,这样才能高效率。那么有没有一种通用的内存池呢?我的答案是:有。但是使用起来没有为某一程序特制的内存池效率高,这也是很明显的。
一个通用的内存池应至少具有以下特性:
1:允许用户设定大块内存的大小及小块内存的大小(允许小内存块的大小设定会导致内存池占用较多空间,后面会讲)
2:当现有内存池不够分配时,再申请一大块内存(此大块内存的大小可以灵活变化)
3:根据用户要求的内存大小进行分配,并非每次分配固定大小(实现这个功能使内存池的分配算法变得复杂)
设计方案:
为了满足上述三个特性,我使用了下面二个结构来管理内存池:
struct MemoryChunk
{
BYTE * Data; //Data只是个指针,指向大内存块地址空间
int ChunkNum; //所在大内存块的小内存块数目(每个大内存块大小不一定相同)
int DataSize; //大内存块中可分配的大小
bool IsAlloctaeMemory; //该小内存块是否已被分配
MemoryChunk * nextChunk;
}
struct MemoryBlock
{
MemoryChunk * FirstMemoryChunk; //指向管理整个内存池第一个小内存块的结构
MemoryChunk *LastMemoryChunk; //指向管理整个内存池最后一个小内存块的结构
int freeChunkNum; //可使用的小内存块数
}
为了能够使用户设定小内存块的大小,使用了上面二个结构来管理内存池,MemoryChunk用来管理每个小的内存块,其中Data成员指向大内存块的地址空间,MemoryBlock用来管理每个MemoryChunk,成员freeChunkNum表示可分配的小内存块数。该设计对每个小内存块都要用一个MemoryChunk管理,所以当小内存块比较多时,该设计占用了不少内存,此设计用空间换取了灵活性。(如果你有好的设计方法,请麻烦告诉我:kulong0105@163.com)
具体实现代码:
下载地址:http://download.csdn.net/detail/kulong0105/3629255
运行测试程序,我们可以发现采用内存池后效果居然变差了,其实这个结果我之前就已经预料到的,跟我程序的设计有关。因为这个内存池的分配算法是通过一个链表管理的,每次分配和释放都要检索链表,而检索的算法效率不高(虽然已经采取一定的快速检索措施),导致结果很槽糕。
分析原因:因为该内存池采用的是用一个MemoryChunk结构管理一个固定大小的小内存块,然后用一个MemoryBlock结构管理所有MemoryChunk结构,采用的是一种叫一级管理方式,因为这种设计的缺陷,导致每次检索链表的时候很费时。
解决方法:可以采用二级管理方式,如每个小内存块用一个MemoryChunk结构管理,每10个MemoryChunk结构用一个MemoryChunkArray结构管理,最后用一个MemoryBlock结构管理所有的MemoryChunk结构,这样的话,每次分配/释放内存的时候检索链表可以先检索某个MemoryChunkArray是否满足要求,如果满足的话就继续检索这个MemoryChunkArray管理的MemoryChunk块,不行的话就检索下一个MemoryChunkArray,这样的话可以加快检索速度,提高效率。
我只是提供了一个方法,当然还有别的更好的设计方法,就要靠你自己琢磨了。本文主要介绍内存池的思想,通过了解其思想后,便可灵活地针对不同的应用需求设计不同的内存池方案,这样才能最大的提高程序运行效率。
如:程序每次申请都是需求分配固定大小的内存,我们完全可以使用下面的结构:
union MemoryChunk
{
BYTE Data[100]; //这里的100就是小块内存的大小
MemoryChunk *nextMemoryChunk;
}
struct MemoryBlock
{
MemoryChunk * FirstMemoryChunk; //指向某个大内存块的第一个小内存块
MemoryChunk * LastMemoryChunk; //指向某个大内存块的最后一个小内存块
MemoryBlock * nextMemoryBlock //指向下一个大内存块
}
struct MemoryPool
{ MemoryBlock *FirstMemoryBlock; //指向第一个大内存块
MemoryBlock *LastMemoryBlock; //指向最后一个大内存块
MemoryChunk *HeadMemoryChunk; //指向可使用的小内存块的头部
int freeChunkNum; //可使用的小内存块数
}
MemoryChunk表示存储实际数据的小内存块,MemoryBlock用来管理某个大内存块,用MemoryPool来管理所有大内存块,我们完全可以用这种设计来做,而且效果还不错。
具体实现代码:
下载地址:http://download.csdn.net/detail/kulong0105/3629640
运行测试程序,我们可以发现采用内存池后程序运行效率提高很多,这是因为我们每次都申请相同大小的内存块,省去了每次分配/释放都要检索链表的过程。
总结:通过上面的分析,可以得出,只有根据不同的应用需求来设计不同的内存池方案,这样才能最大的发挥内存池的优势,才能最大的提高程序运行效率,所以不要像我一样,刚开始还想着什么通用的内存池,没有最好,只有更好!