1.什么是内存池技术及为什么要设计自己的内存池?
通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。C/C++的内存分配(通过malloc或new)可能需要花费很多时。更糟糕的是,随着时间的流逝,内存(memory)将形成碎片,所以一个应用程序的运行会越来越慢当它运行了很长时间和/或执行了很多的内存分配(释放)操作的时候。特别是,你经常申请很小的一块内存,堆(heap)会变成碎片的,这就是为什么我们经常在运行自己的C/C++程序时一开始还好好的,可是越到后面速度越慢,最后甚至直接罢工了。
而对于以上,一个可行的的解决方案就是设计一个自己的内存分配策略,也即设计一个你自己的内存池。内存池(Memory Pool)是一种内存分配方式。 内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是尽量避免了内存碎片,使得内存分配效率得到提升。在启动的时候,一个”内存池”(Memory Pool)分配一块很大的内存,并将会将这个大块(block)分成较小的块(smaller chunks)。每次你从内存池申请内存空间时,它会从先前已经分配的块(chunks)中得到,而不是从操作系统。
最大的优势在于:
1.非常少(几没有) 堆碎片
2.比通常的内存申请/释放(比如通过malloc, new等)的方式快
3.检查任何一个指针是否在内存池里
4.写一个”堆转储(Heap-Dump)”到你的硬盘(对事后的调试非常有用)
5.某种”内存泄漏检测(memory-leak detection)”:当你没有释放所有以前分配的内存时,内存池(Memory Pool)会抛出一个断言(assertion).
2.目前有哪些优秀的内存池技术
1.固定大小缓冲池代码很简单,如下:
template<typename T>
class CMemoryPool
{
public:
enum { EXPANSION_SIZE = 32};
CMemoryPool(unsigned int nItemCount = EXPANSION_SIZE)
{
ExpandFreeList(nItemCount);
}
~CMemoryPool()
{
//free all memory in the list
CMemoryPool<T>* pNext = NULL;
for(pNext = m_pFreeList; pNext != NULL; pNext = m_pFreeList)
{
m_pFreeList = m_pFreeList->m_pFreeList;
delete [](char*)pNext;
}
}
void* Alloc(unsigned int /*size*/)
{
if(m_pFreeList == NULL)
{
ExpandFreeList();
}
//get free memory from head
CMemoryPool<T>* pHead = m_pFreeList;
m_pFreeList = m_pFreeList->m_pFreeList;
return pHead;
}
void Free(void* p)
{
//push the free memory back to list
CMemoryPool<T>* pHead = static_cast<CMemoryPool<T>*>(p);
pHead->m_pFreeList = m_pFreeList;
m_pFreeList = pHead;
}
protected:
//allocate memory and push to the list
void ExpandFreeList(unsigned nItemCount = EXPANSION_SIZE)
{
unsigned int nSize = sizeof(T) > sizeof(CMemoryPool<T>*) ? sizeof(T) : sizeof(CMemoryPool<T>*);
CMemoryPool<T>* pLastItem = static_cast<CMemoryPool<T>*>(static_cast<void*>(new char[nSize]));
m_pFreeList = pLastItem;
for(int i=0; i<nItemCount-1; ++i)
{
pLastItem->m_pFreeList = static_cast<CMemoryPool<T>*>(static_cast<void*>(new char[nSize]));
pLastItem = pLastItem->m_pFreeList;
}
pLastItem->m_pFreeList = NULL;
}
private:
CMemoryPool<T>* m_pFreeList;
};
它的实现思想就是每次从List的头上取内存, 如果取不到则重新分配一定数量; 用完后把内存放回List头部,这样的话效率很高,因为每次List上可以取到的话,肯定是空闲的内存。当然上面的代码只是针对单线程的,要支持多线程的话也很简单,外面加一层就可以了,
代码如下:
class CCriticalSection
{
public:
CCriticalSection()
{
InitializeCriticalSection(&m_cs);
}
~CCriticalSection()
{
DeleteCriticalSection(&m_cs);
}
void Lock()
{
EnterCriticalSection(&m_cs);
}
void Unlock()
{
LeaveCriticalSection(&m_cs);
}
protected:
CRITICAL_SECTION m_cs;
};
template<typename POOLTYPE, typename LOCKTYPE>
class CMTMemoryPool
{
public:
void* Alloc(unsigned int size)
{
void* p = NULL;
m_lock.Lock();
p = m_pool.Alloc(size);
m_lock.Unlock();
return p;
}
void Free(void* p)
{
m_lock.Lock();
m_pool.Free(p);
m_lock.Unlock();
}
private:
POOLTYPE m_pool;
LOCKTYPE m_lock;
};
2.dlmalloc
应该来说相当优秀的内存池, 支持大对象和小对象,并且已被广泛使用。到这里下载:ftp://g.oswego.edu/pub/misc/malloc.c关于dlmalloc的内部原理和使用资料可以参考:内存分配器dlmalloc 2.8.3源码浅析.doc
SGI STL 的 allocator 应该是目前设计最优秀的 C++ 内存分配器之一了,它的运作原理候捷老师在《 STL 源码剖析》里讲解得非常清楚。基本思路是设计一个 free_list[16]数组,负责管理从 8 bytes 到 128 bytes 不同大小的内存块( chunk ),每一个内存块都由连续的固定大小( fixed size block )的很多 chunk 组成,并用指针链表串接起来。
比如说free_list[3]->start_notuse->next_notuse->next_notuse->...->end_notuse;当用户要获取此大小的内存时,就在 free_list 的链表找一个最近的 free chunk 回传给用户,同时将此 chunk 从 free_list 里删除,即把此 chunk 前后chunk 指针链结起来。用户使用完释放的时候,则把此chunk 放回到 free_list 中,应该是放到最前面的 start_free 的位置。这样经过若干次 allocator 和 deallocator 后, free_list 中的链表可能并不像初始的时候那么是 chunk 按内存分布位置依次链接的。假如free_list 中不够时, allocator 会自动再分配一块新的较大的内存区块来加入到 free_list 链表中。
可以自动管理多种不同大小内存块并可以自动增长的内存池,这是 SGI STL 分配器设计的特点。
4.Loki 中的小对象分配器( small object allocator )
Loki 的分配器与 SGI STL 的原理类似,不同之处是它管理 free_list 不是固定大小的数组,而是用一个 vector 来实现,因此可以用户指定 fixed size block 的大小,不像 SGI STL 是固定最大 128 bytes 的。另外它管理 free chunks 的方式也不太一样, Loki 是由一列记录了 free block 位置等信息的 Chunk 类的链表来维护的,free blocks 则是分布在另外一个连续的大内存区间中。而且 free Chunks 也可以根据使用情况自动增长和减少合适的数目,避免内存分配得过多或者过少。
5.Boost 的 object_pool
Boost 中的 object_pool 也是一个可以根据用户具体应用类的大小来分配内存块的,也是通过维护一个 free nodes 的链表来管理的。可以自动增加 nodes 块,初始是 32 个 nodes ,每次增加都以两倍数向 system heap 要内存块。 object_pool 管理的内存块需要在其对象销毁的时候才返还给 system heap
ACE 框架中也有一个可以维护固定大小的内存块的分配器,原理与上面讲的内存池都差不多。它是通过在 ACE_Cached_Allocator 中定义个 Free_list 链表来管理一个连续的大内存块的,里面包含很多小的固定大小的未使用的区块( free chunk ),同时还使用 ACE_unbounded_Set 维护一个已使用的 chuncks ,管理方式与上面讲的内存池类似。也可以指定 chunks 的数目,也可以自动增长,定义大致如下所示:
template<class T>
class ACE_Cached_Allocator : public ACE_New_Allocator<T> {
public:
// Create a cached memory pool with @a n_chunks chunks
// each with sizeof (TYPE) size.
ACE_Cached_Allocator(SIZET n_chunks = ACE_DEFAULT_INIT_CHUNKS);
T* allocate();
void deallocate(T* p);
private:
// List of memory that we have allocated.
Fast_Unbounded_Set<char *> _allocated_chunks;
// Maintain a cached memory free list.
ACE_Cached_Free_List<ACE_Cached_Mem_Pool_Node<T> > _free_list;
};
7.TCMalloc
Google的开源项目gperftools, 主页在这里:https://code.google.com/p/gperftools/,该内存池也被大家广泛好评,并且在google的各种开源项目中被使用, 比如webkit就用到了它。