内存池(MemPool)技术详解 收藏
本文已经迁移到: http://cpp.winxgui.com/cn:dive-into-memory-pool
这段话涉及两个关键概念:内存块(MemBlock)和自由内存结点(FreeNode)。内存块大小一般固定为MemBlockSize字节(除去用以建立链表的指针外)。内存块在申请之初就被划分为多个内存结点(Node),每个Node大小为ItemSize(小对象的大小),计MemBlockSize/ItemSize个。这MemBlockSize/ItemSize个内存结点刚开始全部是自由的,他们被串成链表。我们看看申请/释放内存过程,就很容易明白这样做的目的。
sgi-stl把内存池(MemPool)技术进行发扬光大,用它来实现其最根本的allocator。
概述
内存池(MemPool)技术备受推崇。我用google搜索了下,没有找到比较详细的原理性的文章,故此补充一个。另外,补充了boost::pool组件与经典MemPool的差异。同时也描述了MemPool在sgi-stl/stlport中的运用。
经典的内存池技术
经典的内存池(MemPool)技术,是一种用于分配大量大小相同的小对象的技术。通过该技术可以极大加快内存分配/释放过程。下面我们详细解释其中的奥妙。
经典的内存池只涉及两个常量:MemBlockSize、ItemSize(小对象的大小,但不能小于指针的大小,在32位平台也就是不能小于4字节),以及两个指针变量MemBlockHeader、FreeNodeHeader。开始,这两个指针均为空。
class MemPool
{
private:
const int m_nMemBlockSize;
const int m_nItemSize;
struct _FreeNode {
_FreeNode* pPrev;
{
private:
const int m_nMemBlockSize;
const int m_nItemSize;
struct _FreeNode {
_FreeNode* pPrev;
BYTE data[m_nItemSize - sizeof(_FreeNode*)];
};
struct _MemBlock {
_MemBlock* pPrev;
_FreeNode data[m_nMemBlockSize/m_nItemSize];
};
_MemBlock* m_pMemBlockHeader;
_FreeNode* m_pFreeNodeHeader;
public:
MemPool(int nItemSize, int nMemBlockSize = 2048)
: m_nItemSize(nItemSize), m_nMemBlockSize(nMemBlockSize),
m_pMemBlockHeader(NULL), m_pFreeNodeHeader(NULL)
{
}
};
};
struct _MemBlock {
_MemBlock* pPrev;
_FreeNode data[m_nMemBlockSize/m_nItemSize];
};
_MemBlock* m_pMemBlockHeader;
_FreeNode* m_pFreeNodeHeader;
public:
MemPool(int nItemSize, int nMemBlockSize = 2048)
: m_nItemSize(nItemSize), m_nMemBlockSize(nMemBlockSize),
m_pMemBlockHeader(NULL), m_pFreeNodeHeader(NULL)
{
}
};
其中指针变量MemBlockHeader是把所有申请的内存块(MemBlock)串成一个链表,以便通过它可以释放所有申请的内存。FreeNodeHeader变量则是把所有自由内存结点(FreeNode)串成一个链。
申请内存过程
代码如下:
void* MemPool::malloc() // 没有参数
{
if (m_pFreeNodeHeader == NULL)
{
const int nCount = m_nMemBlockSize/m_nItemSize;
_MemBlock* pNewBlock = new _MemBlock;
pNewBlock->data[0].pPrev = NULL;
for (int i = 1; i < nCount; ++i)
pNewBlock->data[i].pPrev = &pNewBlock->data[i-1];
m_pFreeNodeHeader = &pNewBlock->data[nCount-1];
pNewBlock->pPrev = m_pMemBlock;
m_pMemBlock = pNewBlock;
}
void* pFreeNode = m_pFreeNodeHeader;
m_pFreeNodeHeader = m_pFreeNodeHeader->pPrev;
return pFreeNode;
}
{
if (m_pFreeNodeHeader == NULL)
{
const int nCount = m_nMemBlockSize/m_nItemSize;
_MemBlock* pNewBlock = new _MemBlock;
pNewBlock->data[0].pPrev = NULL;
for (int i = 1; i < nCount; ++i)
pNewBlock->data[i].pPrev = &pNewBlock->data[i-1];
m_pFreeNodeHeader = &pNewBlock->data[nCount-1];
pNewBlock->pPrev = m_pMemBlock;
m_pMemBlock = pNewBlock;
}
void* pFreeNode = m_pFreeNodeHeader;
m_pFreeNodeHeader = m_pFreeNodeHeader->pPrev;
return pFreeNode;
}
内存申请过程分为两种情况:
- 在自由内存结点链表(FreeNodeList)非空。
在此情况下,Alloc过程只是从链表中摘下一个结点的过程。
- 否则,意味着需要一个新的内存块(MemBlock)。
这个过程需要将新申请的MemBlock切割成多个Node,并把它们串起来。
MemPool技术的开销主要在这。
释放内存过程
代码如下:
void MemPool::free(void* p)
{
_FreeNode* pNode = (_FreeNode*)p;
pNode->pPrev = m_pFreeNodeHeader;
m_pFreeNodeHeader = pNode;
}
{
_FreeNode* pNode = (_FreeNode*)p;
pNode->pPrev = m_pFreeNodeHeader;
m_pFreeNodeHeader = pNode;
}
释放过程极其简单,只是把要释放的结点挂到自由内存链表(FreeNodeList)的开头即可。
性能分析
MemPool技术申请内存/释放内存均极其快(比AutoFreeAlloc慢)。其内存分配过程多数情况下复杂度为O(1),主要开销在FreeNodeList为空需要生成新的MemBlock时。内存释放过程复杂度为O(1)。
boost::pool
boost::pool是内存池技术的变种。主要的变化如下:
- MemBlock改为非固定长度(MemBlockSize),而是:第1次申请时m_nItemSize*32,第2次申请时m_nItemSize*64,第3次申请时m_nItemSize*128,以此类推。不采用固定的MemBlockSize,而采用这种做法预测模型(是的,这是一种用户内存需求的预测模型,其实std::vector的内存增长亦采用了该模型),是一个细节上的改良。
- 增加了ordered_free(void* p) 函数。
ordered_free区别于free的是,free把要释放的结点挂到自由内存链表(FreeNodeList)的开头,ordered_free则假设FreeNodeList是有序的,因此会遍历FreeNodeList把要释放的结点插入到合适的位置。
我们已经看到,free的复杂度是O(1),非常快。但请注意ordered_free是比较费的操作,其复杂度是O(N)。这里N是FreeNodeList的大小。对于一个频繁释放/申请的系统,这个N很可能是个大数。这个boost描述得很清楚:http://www.boost.org/libs/pool/doc/interfaces/pool.html
注意:不要认为boost提供ordered_free是多此一举。后文我们会在讨论boost::object_pool时解释这一点。
基于内存池技术的通用内存分配组件
sgi-stl把内存池(MemPool)技术进行发扬光大,用它来实现其最根本的allocator。
其大体的思想是,建立16个MemPool,<=8字节的内存申请由0号MemPool分配,<=16字节的内存申请由1号MemPool分配,<=24字节的内存有2号MemPool分配,以此类推。最后,>128字节的内存申请由普通的malloc分配。
注意
以上代码属于伪代码(struct _FreeNode、_MemBlock编译通不过),并且去除了出错处理。
发表于 @ 2006年11月22日 00:44:00 | 评论( 17 ) | 举报| 收藏
在网上搜了一吓,用多个内存池来解决,每个内存池对应不同的生存期。除了这个方法还有别的吗?
你想,如果程序某个时刻到达了一个内存使用高峰,那么意味着它接下来很有可能有另一个高峰,如果我们辛辛苦苦把内存回收了,马上有要重新申请,其实不值得。细究boost::pool的实现,它采用了非固定长度的MemBlock,来预测的用户内存需求,其实是同样的道理。
对不同的需求,使用不同的内存池,采用不同的策略。软件初期使用的策略和后期可能截然不同。离开需求设计泛用型内存池可不是个好主意。
Sorry,可能有些不太切合本文的题目。看到关于“性能”的讨论时,突然冒出了这些想法。
内存分配后如何使用,什么时候释放,什么时候归还,处于一种基于需求的考虑,所以我比较倾向于将内存池作为策略来看待。
“泛用型内存池”指的是满足大多数需求的内存池实现。这种内存池考虑了大多数应用的共通特性而舍弃特殊性。C++的标准new就是一种泛用型的,只不过它是向操作系统申请。
《Modern C++ Design》有一章专门用来描述了一种小型对象分配器,其实就是一种内存分配策略。标准new针对大块内存的分配做了专门的优化,而对小到几十个字节的对象使用new的话效率太低。为了解决为大量小型(几十个字节)对象分配内存造成的性能问题,书中采用针对小型对象设计的内存池满足了“大量小型对象分配“对效率的需求。
所以,才有了前边的那些发言。SORRY,我的语文水平不高,见谅。:P
做一个简单数据库,以块为单位,申请一个8kb的怎么来做