代码编译运行环境:VS2012+Debug+Win32
利用默认的内存管理函数new/delete或malloc/free在堆上分配和释放内存会有一些额外的开销。
系统在接收到分配一定大小内存的请求时,首先查找内部维护的内存空闲块表,并且需要根据一定的算法(例如分配最先找到的不小于申请大小的内存块给请求者,或者分配最适于申请大小的内存块,或者分配最大空闲的内存块等)找到合适大小的空闲内存块。如果该空闲内存块过大,还需要切割成已分配的部分和较小的空闲块。然后系统更新内存空闲块表,完成一次内存分配。类似地,在释放内存时,系统把释放的内存块重新加入到空闲内存块表中。如果有可能的话,可以把相邻的空闲块合并成较大的空闲块。
默认的内存管理函数还考虑到多线程的应用,需要在每次分配和释放内存时加锁,同样增加了开销。 可见,如果应用程序频繁地在堆上分配和释放内存,则会导致性能的损失。并且会使系统中出现大量的内存碎片,降低内存的利用率。
默认的分配和释放内存算法自然也考虑了性能,然而这些内存管理算法的通用版本为了应付更复杂、更广泛的情况,需要做更多的额外工作。而对于某一个具体的应用程序来说,适合自身特定的内存分配释放模式的自定义内存池则可以获得更好的性能。
内存池(Memory Pool)是一种内存分配方式。通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。
内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。
应用程序自定义的内存池根据不同的适用场景又有不同的类型。从线程安全的角度来分,内存池可以分为单线程内存池和多线程内存池。单线程内存池整个生命周期只被一个线程使用,因而不需要考虑互斥访问的问题;多线程内存池有可能被多个线程共享,因此则需要在每次分配和释放内存时加锁。相对而言,单线程内存池性能更高,而多线程内存池适用范围更广。
内存池(Memory Pool)技术因为其对内存管理有着显著的优点,在各大项目中应用广泛,备受推崇。但是,通用的内存管理机制要考虑很多复杂的具体情况,如多线程安全等,难以对算法做有效的优化,所以,在一些特殊场合,实现特定应用环境的内存池在一定程度上能够提高内存管理的效率。
经典的内存池(Memory Pool)技术,是一种用于分配大量大小相同的小对象的技术。通过该技术可以极大加快内存分配/释放过程。既然针对是特定对象的内存池,所以内存池一般设置为类模板,根据不同的对象来进行实例化。
(1)先申请一块连续的内存空间,该段内存空间能够容纳一定数量的对象。
(2)每个对象连同一个指向下一个对象的指针一起构成一个内存节点(Memory Node)。各个空闲的内存节点通过指针来形成一个链表,链表的每一个内存节点都是一块可供分配的内存空间。
(3)某个内存节点一旦分配出去,就将从链表中去除。
(4)一旦释放了某个内存节点的空间,又将该节点重新加入自由内存节点链表。
(5)如果一个内存块的所有内存节点分配完毕,若程序继续申请新的对象空间,则会再次申请一个内存块来容纳新的对象。新申请的内存块会加入内存块链表中。
经典内存池的实现过程大致如上面所述,其形象化的过程如下图所示:
如上图所示,蓝色框表示申请的内存块,里面存放三个可供分配的空闲节点。空闲节点由空闲节点链表管理,如果分配出去,将其从空闲节点链表删除,如果释放,将其重新插入到链表的头部(注意,本文实现的不是插入到链表的尾部,当然也可以插入到尾部,读者可自行实现)。如果内存块中的空闲节点不够用,则重新申请内存块,申请的内存块由内存块链表来管理。
按照上面的过程设计,内存池类模板有这样几个成员。
两个常量:
内存块大小:MemBlockSize;
节点大小:ItemSize;
两个指针变量:
内存块链表头指针:pMemBlockHeader;
空闲节点链表头指针:pFreeNodeHeader;
空闲节点结构体:
struct FreeNode{
FreeNode* pNext;
char data[ObjectSize];
}
内存块结构体:
Struct MemBlock{
MemBlock *pNext;
FreeNode data[NumofObjects];
}
根据以上经典内存池的设计,编码实现如下。
/***mempool.h***/
template<int ObjectSize,int NumofObjects=20> class MemPool{
private:
const int MemBlockSize; //每个内存块的大小
const int ItemSize; //每个内存节点的大小
//空闲节点结构体
struct FreeNode{
FreeNode* pNext;
char data[ObjectSize];
};
//内存块结构体
struct MemBlock{
MemBlock *pNext;
FreeNode data[NumofObjects];
};
FreeNode* freeNodeHeader;
MemBlock* memBlockHeader;
public:
MemPool():ItemSize(ObjectSize+sizeof(FreeNode*)),MemBlockSize(sizeof(MemBlock*)+NumofObjects*(ObjectSize+sizeof(FreeNode*))){
freeNodeHeader=NULL;
memBlockHeader=NULL;
}
~MemPool(){
MemBlock* ptr;
while(memBlockHeader){
ptr=memBlockHeader->pNext;
delete memBlockHeader;
memBlockHeader=ptr;
}
}
void* malloc();
void free(void*);
};
//分配空闲的节点
template<int ObjectSize,int NumofObjects>
void* MemPool<ObjectSize,NumofObjects>::malloc(){
if(freeNodeHeader==NULL){ //无空闲节点
MemBlock* newBlock=new MemBlock;
newBlock->data[0].pNext=NULL; //设置内存块的第一个节点为空闲节点链表的最后一个
for(int i=1; i<NumofObjects;++i)
newBlock->data[i].pNext=&newBlock->data[i-1];
freeNodeHeader=&newBlock->data[NumofObjects-1];
newBlock->pNext=memBlockHeader;
}
//返回空节点闲链表的第一个节点
void* freeNode=freeNodeHeader;
freeNodeHeader=freeNodeHeader->pNext;
return freeNode;
}
//释放已经分配的节点
template<int ObjectSize,int NumofObjects>
void MemPool<ObjectSize,NumofObjects>::free(void* p){
FreeNode* pNode=(FreeNode*)p;
pNode->pNext=freeNodeHeader;//将释放的节点插入空闲节点头部
freeNodeHeader=pNode;
}
/***end mempool.h***/
/***main.cpp***/
#include <iostream>
using namespace std;
#include "mempool.h"
class ActualClass{
static int count;
int No;
public:
ActualClass(){
No=count;
count++;
}
void print(){
cout<<this<<": ";
cout<<"the "<<No<<" object"<<endl;
}
void* operator new(size_t size);
void operator delete(void* p);
};
//定义内存池对象
MemPool<sizeof(ActualClass),2> mp;
void* ActualClass::operator new(size_t size){
return mp.malloc();
}
void ActualClass::operator delete(void* p){
mp.free(p);
}
int ActualClass::count=0;
int main(){
ActualClass* p1=NULL;
p1=new ActualClass;
p1->print();
ActualClass* p2=new ActualClass;
p2->print();
delete p1;
p1=new ActualClass;
p1->print();
ActualClass* p3=new ActualClass;
p3->print();
delete p1;
delete p2;
delete p3;
getchar();
}
/***end main.cpp***/
程序运行结果:
阅读以上程序,应注意以下几点。 (1)对一种特定的类对象而言,内存池中内存块的大小是固定的,内存节点的大小也是固定的。内存块在申请之初就被划分为多个内存节点,每个Node的大小为ItemSize(对象的大小加上一个指针的大小)。刚开始,所有的内存节点都是自由的,被串成链表。
(2)指针标量memBlockHeader是用来把所有申请的内存块(Memory Block)串成一个内存块链表,以便通过它可以释放所有申请的内存。freeNodeHeader变量则是把所有自由内存节点(Free Node)串成一个链表。freeNodeHeader为空则表明没有可用的自由内存节点,必须申请新的内存块。
(3)申请空间的过程如下。在自由内存节点链表(freeNodeHeader)非空的情况下,malloc过程只是从链表中取下自由内存节点链表的头一个节点,然后把链表头指针移动到下一个节点上去。否则,意味着需要一个新的内存块(Memory Block)。这个过程需要申请新的内存块切割成多个内存节点,并把它们串起来,内存池技术的主要开销就在这里。
(4)释放对象的过程就是把被释放的内存节点重新插入到内存节点链表的开头。最后被释放的节点就是下一个即将被分配的节点。
(5)内存池技术申请/释放内存的速度很快,其内存分配过程多数情况下复杂度为O(1),主要开销在freeNodeHeader为空时需要生成新的内存块。内存节点释放过程复杂度为O(1)。
(6) 在上面的程序中,指针p1和p2连续两次申请空间,它们代表的地址之间的差值为12,正好为一个内存节点的大小(sizeof(ActualClass)+4)的大小。指针p1所指向的对象被释放后,再次申请空间,得到的地址与刚刚释放的地址正好相同。指针p3多代表的地址与前两个对象的地址相聚很远,原因是第一个内存块中的自由内存节点已经分配完了,p3指向的对象位于第二个内存块中。
(7)以上内存池方案并不完美,比如,只能单个单个申请对象空间,不能申请对象数组,内存池中内存块的个数只能增大不能减少,未考虑多线程安全等问题。现在,已经有很多改进的方案,请读者自行查阅相关资料。
[1]http://blog.csdn.net/xushiweizh/article/details/1402967 [2]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008[7.8(P268-P272)]