1、通常我们申请内存/释放内存用的都是系统默认的函数new和delete,每次调用它们都会造成巨大的开销,申请/释放的次数不多可能看不出有什么不同。如果频繁的申请和释放内存,就会导致系统将大量的时间花在向堆申请、释放内存的操作中,在此情况下我们应该构建自己的内存池,以提高内存申请和释放的速度
2、内存池的原理就是一次申请一大块内存,然后在程序需要的时候从这一大块内存中分出一部分给程序用,释放内存的也不是真的向堆释放内存而是将之放回到内存池,以便下次使用
3、内存池的基本原理就是链表
二、结构
1、
template<class T>
struct Data
{
T m_Data;
Data* m_pNext;
}
这是实际用于存储对象的地方,定义为模板以适用于各种类型。由于Data*占4个字节(事实上指针在32位系统中都占用4个字节),为了提高利用效率,我建议T类型越大越好,这样内存的利用率才不会太低。假如T是char(一个字节),那么内存利用率才20%,可见并不是每种类型都适用这个内存池,我针对的是一些自定义类型或大数据类型
2、
class MemoryPool
{
}
这个就是内存池的定义,我们在这里边进行内存的释放和申请,为了使用方便,我将Data类型整合到MemoryPool中
template<class T>
class MemoryPool
{
struct Data
{
T m_Data;
Data* m_pNext;
}
Data* m_pFreeList;//①
unsigned long m_uSize;//②
MPArray m_AllBlock;//③
}
我们知道,每次申请内存的时候我们都是向系统申请一大块的内存(我喜欢称之为一个Block),然后将这一块内存建成链表,m_pFreeList指向表头,需要的时候我们可以从m_pFreeList表头弹出一个节点供程序使用
m_uSize用于保存该内存池总共有多少个内存节点
m_AllBlock用于保存各个大块的指针(MPArray是一个动态数组,类似于vector,当然功能没那么多,后面我会给出MPArray的实现):我们知道,每次申请内存的时候我们都是向系统申请一大块的内存,但是这一个Block也会有用完的时候,用完时我们必须再向系统申请一块内存,就这样我们可能会有Block1、Block2、Block3、…Blockn,m_AllBlock的作用就是用来保存它们(当然只是保存它们的指针)
3、
template<class T>
class MemoryPool
{
struct Data
{
T m_Data;
Data* m_pNext;
}
Data* m_pFreeList;//①
unsigned long m_uSize;//②
MPArray m_AllBlock;//③
public:
MemoryPool();
~MemoryPool();
T* New();
void Delete(T* p);
void Clear();
private:
void Alloc();
bool Find(T* p,T*& pre,T*& pCur,T*& pNext);
}
这是MemoryPool的完整声明,New是程序向内存池申请内存时调用的接口,Delete用于将不使用的内存还给内存池
Clear用于清理内存池,将内存池申请的所有内存释放
Alloc是内存池向系统申请内存的地方
Find是根据T*指针找到它所属的节点的位置,包括它的上一节点(可能没有)和下一个节点(可能没有)
三、在给出他的具体实现之前,我们先来分析一下:
1、最开始的时候m_pFreeList是空的,它不指向任何节点,因为还没有申请内存,m_uSize当然也是0
2、当程序调用内存池的New接口时,我们先判断m_pFreeList是否为空,这里我们分两种情况:
①、m_pFreeList为空,表示没有可分配的内存节点,这时需要调用Alloc向系统申请内存,内存准备完毕并且将链表建立(链表建立是在Allock中完成的)完成之后,m_pFreeList就不为空了,进行②
②、m_pFreeList不为空,将表头弹出给程序使用,然后m_pFreeList指向表头的下一位置
3、当程序向内存池归还内存(调用Delete)的时候,我们必须先判断这个内存是不是有内存池分配出去的,用Find函数在内存池中查找该指针指向的内存是否在内存池中,如果不是内存池分配的就不做任何处理,如果是内存池分配出去的我们就将这块内存加到m_pFreeList表头,以供下一次继续使用
4、Find函数是一个查找函数,实现可能有点复杂,主要是利用了边界的方式进行查找
5、注意:从内存池分配的内存绝对不要用delete或delete[]释放,要调用内存池接口Delete
template <class T>
class MemoryPool
{
struct Data
{
T m_Data;//存放实际的数据
Data* m_pNext;//下一节点,大小为4
Data()
{
m_pNext = NULL;
}
};
Data* m_pFreeList;//空闲内存链表
unsigned long m_nSize;//内存块个数
MPArray m_AllBlock;//存放每次分配的一大块内存的指针,可以用vector或什么其他的代替
public:
MemoryPool()
{
m_pFreeList = NULL;
m_nSize = 0;
Alloc();
}
~MemoryPool()
{
Clear();
}
T* New()
{
if (m_pFreeList != NULL)
{
Data* pData = m_pFreeList;
m_pFreeList = m_pFreeList->m_pNext;
return &pData->m_Data;
}
else
{
Alloc();
Data* pData = m_pFreeList;
m_pFreeList = m_pFreeList->m_pNext;
return &pData->m_Data;
}
return NULL;
}
void Delete(T* p)
{
Data* pData = (Data*)p;
bool bFind = false;
bFind = Find(p);
if (!bFind)
{
return;
}
//加入到Free表中
pData->m_pNext = NULL;
pData->m_pNext = m_pFreeList;
m_pFreeList = pData;
}
void Clear()
{
while(m_AllBlock.Empty())
{
Data* pData = (Data*)m_AllBlock.Pop_front();
if (pData == NULL)
{
continue;
}
delete[] pData;
}
}
private:
//实际内存分配的地方
void Alloc()
{
Data* pData = new Data[LITTLE_BLOCK_NUM];
int i = 0;
int j = LITTLE_BLOCK_NUM-1;
//建立链表,这样建立时间节约一半
while(i < j)
{
pData[i].m_pNext = &pData[i + 1];
pData[j-1].m_pNext = &pData[j];
++i;
--j;
}
pData[LITTLE_BLOCK_NUM-1].m_pNext = m_pFreeList;
m_pFreeList = pData;
m_nSize += LITTLE_BLOCK_NUM;
m_AllBlock.Add(pData);
}
bool Find(T* p)
{
bool bFind = false;
for (int i= 0; i < m_AllBlock.GetSize(); ++i)
{
Data* pData = (Data*)m_AllBlock.Get(i);
if (pData == NULL)
{
continue;
}
//判断p的地址是否在这一大块的范围之内
//因为一大块的地址是连续的
if (p < &pData->m_Data || p > &pData[LITTLE_BLOCK_NUM-1].m_Data)//4096
{
continue;
}
bFind = true;
if (bFind)
{
break;
}
}
return bFind;
}
};
四、以上就是这个内存池的完全实现,下面对他进行分析
1、LITTLE_BLOCK_NUM=4096,内存池每次向系统申请内存的时候固定申请4096*sizeof(Data)个字节的内存(也就是一个Block),即用new Data[LITTLE_BLOCK_NUM]的方式申请内存,这一块内存是连续的(即它们每个单元的地址都是相邻的),知道了这块内存的第一个地址,其他地址便会快速的计算出来
2、Data这个结构,我们必须将m_Data定义在m_pNext前面,只有这样编译器才会在构造Data的内存布局时将m_Data放在m_pNext的前面,所以m_Data的地址是和Data的地址重叠的,我们知道m_Data的地址的同时也就知道了Data的地址
3、将内存还给内存池的时候,调用Delete(T* p),我们需要知道p所指的这块内存是不是由内存池分配的,查找的时候有一个技巧:我们知道一个内存池由若干个Block构成,m_AllBlock保存着所有Block的第一个地址,而每一个Block在内存中是连续的,我们在比较的时候比较p和Block的边界,这样就可以知道这个p是不是在此Block内,如果p小于Block的下界或p大于Block的上界,就表示p不在这个Block之内,我们就可以查找下一个Block了,而不用比较Block的每一个元素。如果查完所有Block,p不在任何一个Block之内,就表示p不是由内存池分配的。如果能找到一个Block,让p处于它的下界和上界之间,那么就表明p所指的内存是由内存池分配的,并且可以马上找到p所属的节点,即:Data* pData = (Data*)p;就是它所属的节点的地址。然后我们就可以将这个节点加到空闲链表头。
4、Alloc分配内存的时候,一次分配4096个Data单元,这些单元式连续的,我们需要将这些单元连接成空闲链表,由于这些单元式连续的,我们可以用循环的方式调用pData[i].m_pNext = &pData[i+1];即可构造完成。
五、测试:
构造一个类用于测试
class XT
{
char c[100];
int ix[200];
};
typedef XT* PXT;
unsigned long L = 1000000;
PXT* psr = new PXT[L];
#define MEMPOOL//0.109
MemoryPool<XT> mempool;
clock_t start, mid,finish;
double duration;
start = clock();
for (unsigned long i= 0; i < L; ++i)
{
#ifdef MEMPOOL
psr[i] = mempool.New();
#else
psr[i] = new XT;
#endif
}
mid = clock();
duration = (double)(mid - start) / CLOCKS_PER_SEC;
printf( "new用时:%f seconds \n", duration );
for (unsigned long i= 0; i < L; ++i)
{
#ifdef MEMPOOL
mempool.Delete(psr[i]);
#else
delete psr[i];
#endif
}
finish = clock();
duration = (double)(finish - mid) / CLOCKS_PER_SEC;
printf( "delete用时:%f seconds \n", duration );
for (unsigned long i= 0; i < L; ++i)
{
#ifdef MEMPOOL
psr[i] = mempool.New();
#else
psr[i] = new XT;
#endif
}
start = clock();
duration = (double)(start - finish) / CLOCKS_PER_SEC;
printf( "new用时:%f seconds \n", duration );
for (unsigned long i= 0; i < L; ++i)
{
#ifdef MEMPOOL
mempool.Delete(psr[i]);
#else
delete psr[i];
#endif
}
finish = clock();
duration = (double)(finish - start) / CLOCKS_PER_SEC;
printf( "delete用时:%f seconds \n", duration );
for (unsigned long i= 0; i < L; ++i)
{
#ifdef MEMPOOL
psr[i] = mempool.New();
#else
psr[i] = new XT;
#endif
}
start = clock();
duration = (double)(start - finish) / CLOCKS_PER_SEC;
printf( "new用时:%f seconds \n", duration );
for (unsigned long i= 0; i < L; ++i)
{
#ifdef MEMPOOL
mempool.Delete(psr[i]);
#else
delete psr[i];
#endif
}
finish = clock();
duration = (double)(finish - start) / CLOCKS_PER_SEC;
printf( "delete用时:%f seconds \n", duration );
我们循环1000000次进行内存申请和释放,可以比较:通过内存池申请内存大概是new的四倍(这是建立在内存池还没有向系统申请内存的基础上,若内存池已经申请了大量内存,这个速度可以提高到20倍以上),而释放的速度,内存池的释放速度比较固定,都是6.6s左右;delete则有些变化,开始时12s多,后面通常是2s多
优缺点:
优点:内存申请极快,释放速度固定
缺点:内存释放的速度有待提高
附:MPArray的实现
class MPArray
{
MPArray& operator= (const MPArray& srcArray);
public:
MPArray(int iAutoFree = AUTO_FREE)
{
m_iPos = -1;
m_iSize = 0;
m_iAutoFree = iAutoFree;
m_pData = NULL;
}
MPArray(MPArray& srcArray,int iAutoFree = AUTO_FREE)
{
m_iPos = -1;
m_iSize = 0;
m_iAutoFree = iAutoFree;
m_pData = NULL;
Copy(srcArray);
}
~MPArray()
{
RemoveAll();
m_iSize = 0;
m_iPos = -1;
}
void SetAutoFree(int iAutoFree = 1)
{
m_iAutoFree = iAutoFree;
}
int GetSize()
{
return m_iSize;
}
int GetUpperBound()
{
}
void SetSize(int iSize)
{
void** pTemp = new void*[iSize];
int iTemp = 0;
if (m_iSize > iSize)
{
m_iPos = -1;
iTemp = iSize;
}
else
{
iTemp = m_iSize;
}
for (int i = 0; i < iTemp; ++i)
{
pTemp[i] = m_pData[i];
}
delete[] m_pData;
m_pData = pTemp;
m_iSize = iSize;
}
void FreeExtra();
void RemoveAll()
{
int i = 0;
if (AUTO_FREE == m_iAutoFree)
{
for (i = 0 ; i < m_iSize ;++i)
{
delete m_pData[i];
m_pData[i] = NULL;
}
}
delete[] m_pData;
m_pData = NULL;
m_iSize = 0;
m_iPos = -1;
}
void* Get(int iIndex)
{
if (m_iSize <= 0)
{
return NULL;
}
if (iIndex < 0)
{
return NULL;
}
return m_pData[iIndex];
}
void Set(int iIndex,void* pElement)
{
if (iIndex >= m_iSize)
{
SetSize(iIndex);
}
m_pData[iIndex] = pElement;
}
void* Element(int iIndex)
{
if (m_iSize <= 0)
{
return NULL;
}
if (iIndex < 0)
{
return NULL;
}
return m_pData[iIndex];
}
void** GetData()
{
return m_pData;
}
void Add(void* element)
{
SetSize(m_iSize + 1);
m_pData[m_iSize-1] = element;
}
void Append(MPArray& srcArray)
{
int iTemp = m_iSize;
SetSize(m_iSize + srcArray.GetSize());
for (int i = iTemp; i < m_iSize ; i++)
{
m_pData[i] = srcArray.Get(i-iTemp);
}
}
void Copy(MPArray& srcArray)
{
int iTemp = m_iSize;
SetSize(srcArray.GetSize());
for (int i = 0; i < m_iSize ; i++)
{
m_pData[i] = srcArray.Get(i);
}
}
void* operator [] (int iIndex)
{
return Get(iIndex);
}
void Insert(int iIndex,void* element,int iCount = 1)
{
if (iIndex < 0 || iCount < 1 || iIndex > m_iSize)
{
return;
}
int iTemp = m_iSize;
SetSize(m_iSize+iCount);
for (int i = m_iSize-1; i > iIndex; --i)
{
m_pData[i] = m_pData[i-iCount];
}
for (int i = iIndex ; i < iCount ; ++i)
{
m_pData[i] = element;
}
}
void Remove(int iIndex,int iCount = 1)
{
if (iIndex < 0 || iCount > m_iSize || iIndex >= m_iSize)
{
return;
}
int iTemp = m_iSize;
for (int i = iIndex; i < m_iSize-iCount; ++i)
{
m_pData[i] = m_pData[i+iCount];
}
m_iSize -= iCount;
}
void Insert(int iIndex,MPArray& srcArray)
{
if (iIndex < 0 || iIndex > m_iSize)
{
return;
}
int iTemp = srcArray.GetSize();
SetSize(m_iSize+srcArray.GetSize());
for(int i = m_iSize-1 ; i > iIndex + iTemp -1; --i)
{
m_pData[i] = m_pData[i-iTemp];
}
for (int i = iIndex; i < iIndex+ iTemp; ++i)
{
m_pData[i] = srcArray.Get(i-iIndex);
}
}
void Clear()
{
RemoveAll();
m_iPos = -1;
}
void* First()
{
return Get(0);
}
void* Last()
{
return Get(m_iSize-1);
}
void* Prev()
{
return Get(m_iPos-1);
}
void* Next()
{
return Get(m_iPos+1);
}
void* GetCurrentData()
{
return Get(m_iPos);
}
bool Empty()
{
if (m_iSize == 0)
{
return true;
}
return false;
}
void Push_back(void* pElement)
{
Add(pElement);
}
void Push_front(void* pElement)
{
Insert(0,pElement);
}
void* Pop_back()
{
if (m_iSize <= 0)
{
return NULL;
}
void* pData = Last();
Remove(GetSize()-1);
return pData;
}
void* Pop_front()
{
if (m_iSize <= 0)
{
return NULL;
}
void* pData = First();
Remove(0);
return pData;
}
private:
void** m_pData;
int m_iSize;
int m_iPos;
int m_iAutoFree;
};