65.9K
CodeProject 正在变化。 阅读更多。
Home

为什么使用内存池以及如何实现它

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.41/5 (16投票s)

2008年7月4日

CPOL

4分钟阅读

viewsIcon

240786

downloadIcon

1962

介绍内存池

下载 MemoryPoolSourceCode.zip - 21.82 KB

简介

什么是内存池?我认为我首先必须回答这个问题,因为它对初学者很重要。简而言之,内存池是您从系统获取的一个内存块,并使用其中的一部分单位来替换系统调用 malloc/free 和 new/delete。这项技术的好处在于重用现有的内存块,从而减少系统调用的次数。定义一个精确的定义是一项艰巨的任务。如果您仍然无法理解这个概念,请谷歌搜索。

背景

为什么使用内存池?我有两个原因:[1] 缩短程序分配内存的时间。现在我举一个例子来证明这项技术具有良好的效率。

#include <windows.h>
#include <iostream.h>

class CTestClass
{    
    char m_chBuf[4096];
};

int main()
{
    DWORD count = GetTickCount();    
    
    for(unsigned int i=0; i<0x5fffff; i++)
    {
        CTestClass *p = new CTestClass;
        delete p;
    }
    
    cout << "Interval = " << GetTickCount()-count << " ms" << endl;
    
    return 0;
} 

在我的电脑上(Intel 3.0G Hz CPU / 512 MB RAM / Win XP SP2 / VC6.0),结果是

间隔 = 8735 毫秒

当我使用一个简单的内存池时,代码如下

#include <windows.h>
#include <iostream.h>

char buf[4100];  //Simple Memory Pool 

class CTestClass
{
    char m_chBuf[4096];
public:    
    void *operator new(unsigned int uiSize)
    {
        return (void *)buf;
    }
    void  operator delete(void *p)
    {
    }
};

int main()
{
    DWORD count = GetTickCount();
    
    for(unsigned int i=0; i<0x5fffff; i++)
    {
        CTestClass *p = new CTestClass; //Didn`t use system new operator!
        delete p;
    }
    
    cout << " Interval = " << GetTickCount()-count << " ms" << endl;
    
    return 0;
}
在我的电脑上,结果是

间隔 = 391 毫秒

您可以看到,后者比前者快了大约二十倍!原因是什么?因为系统调用 new/delete 占用了前一个程序很多时间!当然,这是一个极端的例子!无论如何,我们可以从前面的代码中了解到内存池的价值!

[2] 避免内存碎片。遗憾的是,我还没有一个简单而好的例子来证明这个结论。我从一些书籍和理论中得出了这个结论!

请注意,只有当程序频繁使用系统调用 malloc/free 或 new/delete 时,内存池才具有价值。否则,您就不需要使用它。

实现

现在,我们来关注如何实现内存池。我相信内存池至少包含三个要素。

[1] 我们需要一个大的内存块。我们从哪里获得它?操作系统可以从栈、堆、全局数据段等地方为我们提供内存块。也许,我们也可以从映射文件中获取内存块用于内存池;我没有测试过这种方法,也不知道结果!通常,我们从堆中获取内存块,这是一种良好且方便的方法。

[2] 我们需要一种算法,从内存块中将内存单元分配给客户端代码。这不是一个简单的问题!许多操作系统教科书对此进行了详细介绍。内存池的许多实现是将内存块划分为相同大小的单元。

[3] 我们需要一种算法,将内存单元从客户端代码中释放。这个问题与第二点有关。

在这里,我将展示一个内存池的实现示例

#ifndef __MEMPOOL_H__
#define __MEMPOOL_H__
                                          
class CMemPool
{
private:
    //The purpose of the structure`s definition is that we can operate linkedlist conveniently
    struct _Unit                     //The type of the node of linkedlist.
    {
        struct _Unit *pPrev, *pNext;
    };

    void* m_pMemBlock;                //The address of memory pool.

    //Manage all unit with two linkedlist.
    struct _Unit*    m_pAllocatedMemBlock; //Head pointer to Allocated linkedlist.
    struct _Unit*    m_pFreeMemBlock;      //Head pointer to Free linkedlist.

    unsigned long    m_ulUnitSize; //Memory unit size. There are much unit in memory pool.
    unsigned long    m_ulBlockSize;//Memory pool size. Memory pool is make of memory unit.

public:
    CMemPool(unsigned long lUnitNum = 50, unsigned long lUnitSize = 1024);
    ~CMemPool();
    
    void* Alloc(unsigned long ulSize, bool bUseMemPool = true); //Allocate memory unit
    void Free( void* p );                                   //Free memory unit
};

#endif //__MEMPOOL_H__

有一张图片可以帮助您理解这个类。MemoryPoolSorceCode
从图片可以看出,m_pMemBlock 指向一个内存块,该内存块被划分为许多相同大小的内存单元。所有内存单元都由双向链表管理。为什么我们需要双向链表来管理内存单元?答案是,这种方式可以确保 Alloc(...) 和 Free() 函数的效率是 O(1)!有两个双向链表;第一个由 m_pFreeMemBlock 指向,由所有空闲单元组成,第二个由 m_pAllocatedMemBlock 指向,由所有已分配单元组成。当创建一个类的对象时,构造函数 CMemPool(...) 将首先从操作系统分配一个内存块,然后创建一个双向链表来管理所有空闲单元。请注意,这是一个静态双向链表。因此,我们不需要在 ~CMemPool() 中释放链表的每个节点,只需释放 m_pMemBlock 指向的内存块。Alloc(...) 函数的工作是从“空闲”链表中获取一个节点并将其插入到“已分配”链表中,然后将地址返回给客户端代码。Free() 函数是 Alloc(...) 函数的反向过程。

我将根据源代码解释之前的描述。

/*==========================================================
CMemPool:
    Constructor of this class. It allocate memory block from system and create
    a static double linked list to manage all memory unit.

Parameters:
    [in]ulUnitNum
    The number of unit which is a part of memory block.

    [in]ulUnitSize
    The size of unit.
//=========================================================
*/
CMemPool::CMemPool(unsigned long ulUnitNum,unsigned long ulUnitSize) :
    m_pMemBlock(NULL), m_pAllocatedMemBlock(NULL), m_pFreeMemBlock(NULL), 
    m_ulBlockSize(ulUnitNum * (ulUnitSize+sizeof(struct _Unit))), 
    m_ulUnitSize(ulUnitSize)
{    
    m_pMemBlock = malloc(m_ulBlockSize);     //Allocate a memory block.
    
    if(NULL != m_pMemBlock)
    {
        for(unsigned long i=0; i<ulUnitNum; i++)  //Link all mem unit . Create linked list.
        {
            struct _Unit *pCurUnit = (struct _Unit *)( (char *)m_pMemBlock + i*(ulUnitSize+sizeof(struct _Unit)) );
            
            pCurUnit->pPrev = NULL;
            pCurUnit->pNext = m_pFreeMemBlock;    //Insert the new unit at head.
            
            if(NULL != m_pFreeMemBlock)
            {
                m_pFreeMemBlock->pPrev = pCurUnit;
            }
            m_pFreeMemBlock = pCurUnit;
        }
    }    
} 


函数 CMemPool(...) 的主要任务是创建一个静态双向链表。其中有一些指针技巧,对初学者来说可能有点难。我只解释一个语句“struct _Unit *pCurUnit = (struct _Unit *)( (char *)m_pMemBlock + i*(ulUnitSize+sizeof(struct _Unit)) )”;结果是 pCurUnit 指向一个内存单元的起始地址,“i”变量在这个语句中表示哪个单元。如果您还没有掌握指针,请参考教科书。

/*===============================================================
~CMemPool():
    Destructor of this class. Its task is to free memory block.
//===============================================================
*/
CMemPool::~CMemPool()
{
    free(m_pMemBlock);
}
函数 ~CMemPool(...) 很容易理解。我将不作任何解释。

/*================================================================
Alloc:
    To allocate a memory unit. If memory pool can`t provide proper memory unit,
    It will call system function.

Parameters:
    [in]ulSize
    Memory unit size.

    [in]bUseMemPool
    Whether use memory pool.

Return Values:
    Return a pointer to a memory unit.
//=================================================================
*/
void* CMemPool::Alloc(unsigned long ulSize, bool bUseMemPool)
{
    if(    ulSize > m_ulUnitSize || false == bUseMemPool || 
        NULL == m_pMemBlock   || NULL == m_pFreeMemBlock)
    {
        return malloc(ulSize);
    }

    //Now FreeList isn`t empty
    struct _Unit *pCurUnit = m_pFreeMemBlock;
    m_pFreeMemBlock = pCurUnit->pNext;            //Get a unit from free linkedlist.
    if(NULL != m_pFreeMemBlock)
    {
        m_pFreeMemBlock->pPrev = NULL;
    }

    pCurUnit->pNext = m_pAllocatedMemBlock;
    
    if(NULL != m_pAllocatedMemBlock)
    {
        m_pAllocatedMemBlock->pPrev = pCurUnit; 
    }
    m_pAllocatedMemBlock = pCurUnit;

    return (void *)((char *)pCurUnit + sizeof(struct _Unit) );
}
函数 Alloc(...) 的主要任务是将一个单元从“空闲链表”移动到“已分配链表”,并返回单元第九个字节的地址。

/*================================================================
Free:
    To free a memory unit. If the pointer of parameter point to a memory unit,
    then insert it to "Free linked list". Otherwise, call system function "free".

Parameters:
    [in]p
    It point to a memory unit and prepare to free it.

Return Values:
    none
//================================================================
*/
void CMemPool::Free( void* p )
{
    if(m_pMemBlock<p && p<(void *)((char *)m_pMemBlock + m_ulBlockSize) )
    {
        struct _Unit *pCurUnit = (struct _Unit *)((char *)p - sizeof(struct _Unit) );

        m_pAllocatedMemBlock = pCurUnit->pNext;
        if(NULL != m_pAllocatedMemBlock)
        {
            m_pAllocatedMemBlock->pPrev = NULL;
        }

        pCurUnit->pNext = m_pFreeMemBlock;
        if(NULL != m_pFreeMemBlock)
        {
             m_pFreeMemBlock->pPrev = pCurUnit;
        }

        m_pFreeMemBlock = pCurUnit;
    }
    else
    {
        free(p);
    }
}
函数 Free() 的主要任务是将一个单元从“已分配链表”移动到“空闲链表”。

使用内存池

如何使用内存池?一方面,您必须在适当的地方定义一个 CMemPool 对象,例如 g_MemPool。另一方面,您需要重载准备使用内存池的某个类的 new/delete 运算符,就像前面的示例一样。

测试

下图是我的演示代码在我的电脑上的运行结果。
Result_Data.JPG

下图是根据上述结果制作的图表。

zbt.JPG

很明显,内存池可以提高频繁使用系统调用 new/delete 或 malloc/free 的程序的效率。

就此结束。

谢谢。

© . All rights reserved.