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