0、内存池之引言
这是关于内存池的一系列简短文章,当然它不是短期的研究结果,而是长期使用经验的总结,介绍得可能不会很详细,一些别人介绍得很细节的东西我就基本掠过。
转载请署名作者:袁斌
内容如下:
1、 单线程内存池。
2、 多线程内存池。
3、 Dlmalloc nedmalloc
4、 实现线程关联的内存池。
5、 线程关联内存池再提速。
关于内存池有一些经典资源可参考,如早期侯杰《池内春秋》,云风(网易核心开发人员)《我的编程感悟》中提及的内存池,以及许世伟(原金山CTO,现盛大创新院技术专家)关于内存池的系列文章。
1、单线程内存池
内存池的基本思想是大块向系统申请内存,内部切割为小块,内部cache之后有选择的分配,不够的时候继续向系统大块申请内存,示例代码如下:
struct tm_memblock
{
tm_memblock *next;
};
class tm_pool
{
…
tm_bufunit *next; //pool中自由块链
tm_memblock *mbk; //trunk表
…
};
void *tm_pool::newobj()
{
if(! next)
{
expand();
}
tm_bufunit *head = next;
next = head->next;
return (void *)head;
}
void tm_pool::delobj(void *pbuf)
{
tm_bufunit *head = (tm_bufunit*)(pbuf);
head->next = next;
next = head;
}
详细实现建议看云风的内存池,我也不过是学习了它的实现而已。
不要小看了单线程内存池,它是我们走向更复杂应用的基础,它是我们后面提及的多线程内存池以及线程关联内存池的基础。
这种单线程的内存池分配释放速度是很快的,比dlmalloc更快近1倍,大约相当于malloc/free的50-100倍(具体倍率视分配的大小而不同,分配小块倍率小,分配大块倍率大)。
有的朋友可能会考虑使用std::list之类的东西来构建内存池,我奉劝有这种想法的人打住,std::list是效率很低的,此外用一个高层的东西构建底层模块,总体上属于本末倒置。
2、多线程内存池
上一节很简略的说了下单线程内存池,单线程内存池如果要放在多线程环境下使用是不安全的,我们需要进行保护,如何保护,最简单的方法就是加临界区,云风的实现里面是用原子操作模拟一个临界区,我实测跟临界区性能非常接近,甚至很多时候不如临界区,所以我就不追求用原子操作模拟临界区了,还是直接用临界区最简单。
class CMemPool
{
public:
struct memory_list
{
memory_list *_next;
};
struct alloc_node
{
size_t _size;
size_t _number;
size_t _bksize;
long _guard;
memory_list *_free_list;
};
struct chunk_list
{
chunk_list *_next;
memory_list *_data;
size_t _size;
size_t _idx;
};
~CMemPool();
static CMemPool &instance()
{
if(!_instance)
{
create_instance();
}
return *_instance;
}
static int chunk_index(size_t size);
void *allocate(size_t size, size_t *psize=NULL);
void deallocate(void *p, size_t size);
//以下为几个检测和设置预分配参数的函数,2007.06.08增
size_t getallocnumber(size_t size, size_t *psize=NULL);
size_t setallocnumber(size_t size, size_t number);
static void dumpallocnode();
private:
static alloc_node _vnode[92];
chunk_list *_chunk_list;
long _chunk_guard;
static CMemPool *_instance;
static long _singleton_guard;
static bool _singleton_destroyed;
static void create_instance();
static void build_chunknode();
CMemPool();
memory_list *alloc_chunk(size_t idx);
};
CMemPool *CMemPool::_instance = 0;
long CMemPool::_singleton_guard = 0;
bool CMemPool::_singleton_destroyed = false;
CMemPool::alloc_node CMemPool::_vnode[92];
struct chunk_desc
{
int s; // 区间起始字节
int e; // 区间终止字节
int align; // 区间内分段对齐值
int number; // 区间内分段个数,计算属性
}_cd[] =
{
{0, 1024, 16, 0},
{1024, 8192, 256, 0}
};
void CMemPool::create_instance()
{
thread_guard guard(&_singleton_guard);
if(_instance)
return;
assert(!_singleton_destroyed);
static CMemPool obj;
_instance = &obj;
}
void CMemPool::build_chunknode()
{
const int cdnum = sizeof(_cd)/sizeof(_cd[0]);
int index=0, number;
for(int j=0; j<cdnum; j++)
{
_cd[j].number = (_cd[j].e-_cd[j].s)/_cd[j].align;
for(int i=0; i<_cd[j].number; i++)
{
int unitsize = (i+1)*_cd[j].align+_cd[j].s;
_vnode[index]._size = unitsize;
if(unitsize < 512)
number = 4096/unitsize;
else if(unitsize < 1024)
number = 16384/unitsize;
else
number = 65536/unitsize;
_vnode[index]._number = number;
_vnode[index]._bksize = unitsize * number;
_vnode[index]._guard = 0;
_vnode[index]._free_list = NULL;
++index;
}
}
}
size_t CMemPool::getallocnumber(size_t size, size_t *psize/*=NULL*/)
{
int idx = chunk_index(size);
if(idx >= 0)
return _vnode[idx]._number;
return 0;
}
size_t CMemPool::setallocnumber(size_t size, size_t number)
{
int idx = chunk_index(size);
if(idx >= 0)
{
size_t on = _vnode[idx]._number;
_vnode[idx]._number = number;
return on;
}
return 0;
}
void CMemPool::dumpallocnode()
{
int size = sizeof(_vnode)/sizeof(_vnode[0]);
printf("_vnode.size = %d/r/n", size);
for(int i=0; i<size; ++i)
{
printf("vnode.size %d, vnode.number %d/r/n", _vnode[i]._size, _vnode[i]._number);
}
}
int CMemPool::chunk_index(size_t bytes)
{
#if(0)
int idx = 0;
const int cdnum = sizeof(_cd)/sizeof(_cd[0]);
if(bytes > _cd[cdnum-1].e)
idx = -1;
else
{
for(int i=0; i<cdnum; i++)
{
if((bytes > _cd[i].s) && (bytes <= _cd[i].e))
{
idx += (bytes-_cd[i].s+_cd[i].align-1)/_cd[i].align-1;
break;
}
idx += _cd[i].number;
}
}
// printf("bytes %d idx = %d/r/n", bytes, idx);
return idx;
#else
//下面的代码是根据静态数据和上面的代码做了优化的,
//如果修改了基础数据需要对应的修改下面的代码
if(bytes > 8192)
{
return -1;
}
else if(bytes > 1024)
// idx = _cd[0].number+(bytes-_cd[1].s+_cd[1].align-1)/_cd[1].align-1;
return 64+(bytes-1024+255)/256-1;
// return _cd[0].number+(bytes-1024+255)/256-1;
else
// idx = (bytes-_cd[0].s+_cd[0].align-1)/_cd[0].align-1;
return (bytes+15)/16-1;
#endif
}
CMemPool::CMemPool()
{
_chunk_list = NULL;
_chunk_guard = 0;
build_chunknode();
}
CMemPool::~CMemPool()
{
int s = 0;
chunk_list *temp = _chunk_list;
while(temp)
{
++s;
temp = temp->_next;
}
void **chunk = reinterpret_cast<void **>(malloc(s * sizeof(void *)));
temp = _chunk_list;
int i=0;
while(temp)
{
chunk[i] = temp->_data;
++i;
temp = temp->_next;
}
for(i=0; i<s; i++)
{
free(chunk[i]);
}
free(chunk);
_singleton_destroyed = true;
_instance = 0;
}
CMemPool::memory_list *CMemPool::alloc_chunk(size_t idx)
{
thread_guard guard(&_chunk_guard);
memory_list *¤t_list = _vnode[idx]._free_list;
if(current_list)
return current_list;
const size_t node_size = _vnode[idx]._size;
const size_t number = _vnode[idx]._number;
const size_t chunk_size = node_size * number;
memory_list *ret = current_list = reinterpret_cast<memory_list *>(malloc(chunk_size));
memory_list *iter = ret;
//for(size_t i=0; i<=chunk_size-node_size*2; i+=node_size)
for(size_t i=0; i<number-1; ++i)
{
iter = iter->_next = iter+node_size/sizeof(*iter);
}
iter->_next = 0;
return ret;
}
void *CMemPool::allocate(size_t size, size_t *psize/*=NULL*/)
{
int idx = chunk_index(size);
if(idx < 0)
{
if(psize) *psize = size;
return malloc(size);
}
if(psize)
*psize = _vnode[idx]._size;
thread_guard guard(&_vnode[idx]._guard);
memory_list *&temp = _vnode[idx]._free_list;
if(!temp)
{
memory_list *new_chunk = alloc_chunk(idx);
chunk_list *chunk_node;
if(chunk_index(sizeof(chunk_list))==idx)
{
chunk_node = reinterpret_cast<chunk_list *>(temp);
temp = temp->_next;
}
else
{
chunk_node = reinterpret_cast<chunk_list *>(allocate(sizeof(chunk_list)));
}
thread_guard guard(&_chunk_guard);
chunk_node->_next = _chunk_list;
chunk_node->_data = new_chunk;
chunk_node->_size = _vnode[idx]._bksize;
chunk_node->_idx = idx;
_chunk_list = chunk_node;
}
void *ret = temp;
temp = temp->_next;
return ret;
}
void CMemPool::deallocate(void *p, size_t size)
{
int idx = chunk_index(size);
if(idx < 0)
{
free(p);
}
else
{
memory_list *free_block = reinterpret_cast<memory_list *>(p);
thread_guard guard(&_vnode[idx]._guard);
memory_list *&temp = _vnode[idx]._free_list; //_free_list[idx];
free_block->_next = temp;
temp = free_block;
}
}
以上基本是云风内存池的一个简单修改版,这种模式的内存池由于每次分配释放都要lock unlock,所以效率很低,大概只相当于malloc/free 2-4倍的速度,提速也不是很明显,大概相当于nedmalloc速度的一半左右。
3、dlmalloc、nedmalloc
Dlmalloc、nedmalloc等知名分配器估计搞内存池的人都知道,dlmalloc是单线程的,不考虑锁,nedmalloc是多线程的,带锁,其实nedmalloc也是线程缓存式的内存池,具体实现我就不说了,nedmalloc我大致看了一下,dlmalloc我也没有深入分析过。
关于两者分配性能前面都提到过,就不再说了,如果不想自己研究内存池,用这两者绝对是很合适的一个选择,取代crt里面的malloc系列是正合适,我曾经很长一段时间将nedmalloc作为server程序的默认内存分配器,而不是用前面提到的多线程内存池,我甚至为程序定义了一个IAllocator,根据不同选项采用不同的内存分配模式,采用nedmalloc的时候就用
#pragma warning(disable : 4291)
class ned_alloc
{
public:
static void *operator new(size_t size)
{
return nedmalloc(size);
}
static void operator delete(void *p, size_t size)
{
nedfree(p);
}
};
typedef ned_alloc galloc;
#define gmalloc nedmalloc
#define gfree nedfree
#define gcalloc nedcalloc
#define grealloc nedrealloc
如此的确满足了很多时候的需求,而且calloc realloc等都支持,的确是个大而全的好解决办法。如果不是最近有空亲自实现了一个简单的线程相关内存池,我估计我还会继续使用nedmalloc作为默认的server程序内存分配方案。
4、线程关联的内存池
每每想到单线程下内存池飞一般的速度和多线程下蜗牛一般的速度我就不能原谅自己,为什么差这么多,就不能让多线程下内存分配更快一点吗?解决方法有了,那就是让缓存线程化,各个线程有自己私有的缓存,分配的时候预先从当前线程私有缓存分配,分配空了的时候去全局free表取一组freeunit或直接向系统申请一大块缓存(各个线程缓存完全独立),不管具体采用什么方式,速度都大幅度的提高了,虽然还是比单线程下内存池慢了许多,不过比前面提到的多线程内存池以及nedmalloc都要快很多,我的实现大概比nedmalloc快1.6 ~ 2倍,离单线程下内存池速度也很近了,只是多了些查找线程id比较线程id等动作而已,基本上达到了自己的目标。
看看第一版线程关联内存池的一些代码:
struct tm_bufunit
{
tm_pool *pool; //pool指针
union
{
tm_bufunit *next; //下一个块指针
char data[4]; //数据区域
};
};
struct tm_gcontrol
{
tm_bufunit *gfree;
CRITICAL_SECTION gcs;
tm_gcontrol() : gfree(NULL) { InitializeCriticalSection(&gcs); }
~tm_gcontrol() { DeleteCriticalSection(&gcs); }
Inline void lock() { EnterCriticalSection(&gcs); }
Inline void unlock() { LeaveCriticalSection(&gcs); }
void free(tm_bufunit *buf)
{
lock();
buf->next = gfree;
gfree = buf;
unlock();
}
};
struct tm_memblock
{
tm_memblock *next;
};
class tm_pool
{
private:
size_t bksize; //一个分配块大小
size_t onebknum; //一次分配多少个bksize
DWORD thid; //线程id
tm_bufunit *next; //pool中自由块链
tm_memblock *mbk; //trunk表
tm_gcontrol gcontrol; //全局free表
friend tm_poolset;
private:
void expand();
public:
tm_pool(size_t size, size_t bknum);
~tm_pool();
void destroy();
void *newobj();
static void delobj(void *pbuf);
};
class tm_poolset
{
public:
tm_poolset();
virtual ~tm_poolset();
//添加分配池
bool addpool(size_t size, size_t allocnum);
void *newobj(size_t size, size_t *osize=NULL);
void delobj(void *pbuf, size_t size);
void destroy();
tm_pool *findpool(size_t size)
{
TMPOOLS::iterator it = tmpools.lower_bound(size);
if(it != tmpools.end())
return it->second;
return NULL;
}
protected:
typedef std::map<size_t, tm_pool *> TMPOOLS;
TMPOOLS tmpools;
};
//公开的数据及函数
extern DWORD tm_tlsindex; //tls索引
//app初始化,分配index
void tm_init();
void tm_free();
//关联到该线程
void tm_attach();
void tm_detach();
tm_poolset *tm_getpoolset();
//添加trunk
bool tm_addtrunk(size_t size, size_t allocnum);
//tls相关分配
void *tm_new(size_t size, size_t *osize=NULL);
//tls相关释放
void tm_del(void *buf, size_t size);
.cpp代码如下:
tm_pool::tm_pool(size_t size, size_t bknum) :
next(NULL), mbk(NULL),
bksize(size), onebknum(bknum)
{
thid = GetCurrentThreadId();
}
tm_pool::~tm_pool()
{
destroy();
}
void tm_pool::destroy()
{
for(tm_memblock *p = mbk; p; )
{
tm_memblock *q = p->next;
free((char *)p);
p = q;
}
mbk = NULL;
next = NULL;
}
void *tm_pool::newobj()
{
if(! next)
{
gcontrol.lock();
if(gcontrol.gfree)
{
next = gcontrol.gfree;
gcontrol.gfree = NULL;
}
gcontrol.unlock();
}
if(! next)
{
expand();
}
tm_bufunit *head = next;
next = head->next;
// return (void *)head;
return (void *)head->data;
}
void tm_pool::delobj(void *pbuf)
{
// tm_bufunit *head = (tm_bufunit*)(pbuf);
tm_bufunit *head = (tm_bufunit *)((char *)pbuf-offsetof(tm_bufunit, data));
tm_pool *pool = head->pool;
if(pool->thid == GetCurrentThreadId())
{
head->next = pool->next;
pool->next = head;
}
else
{
pool->gcontrol.free(head);
}
}
void tm_pool::expand()
{
size_t unitsize = offsetof(tm_bufunit, data) + bksize;
size_t size = (unitsize * onebknum + sizeof(tm_memblock));
tm_memblock *pbk = (tm_memblock *)malloc(size);
pbk->next = mbk;
mbk = pbk;
tm_bufunit *p = (tm_bufunit*)((char *)pbk+sizeof(tm_memblock));
p->pool = this;
next = p;
for(size_t i=0; i<onebknum-1; ++i)
{
p->next = (tm_bufunit *)((char *)p+unitsize);
p = p->next;
p->pool = this;
}
p->next = NULL;
}
…
这一版基本实现了第一步的提速目标,并且每个分配块还记录了来自哪个pool,这样free的时候就省去了查找pool的动作,只是还有一些问题,如何判断一个内存是来源于malloc的分配还是来源于pool的分配没有做终结的判断,而且还留下了一个bug,对于a线程来说,可能只有256,512两个块的缓存,b线程可能多一个块1024,这样a线程分配的1024字节的内存是用malloc分配,到b线程释放的时候会调用pool释放,这个bug将在下一章解决。
5、线程关联内存池再提速
上一节已经提到问题,解决办法是这样的
struct tm_bufunit
{
tm_pool *pool; //pool指针
union
{
tm_bufunit *next; //下一个块指针
char data[4]; //数据区域
};
};
static void *tm_malloc(size_t size, size_t *osize=NULL)
{
tm_bufunit *p = (tm_bufunit *)malloc(sizeof(tm_bufunit)-offsetof(tm_bufunit, data)+size);
if(p)
{
p->pool = NULL;
if(osize) *osize = size;
return p->data;
}
return NULL;
}
看上面的代码应该很容易明白,就是将由该池malloc的内存块也打上统一的标记,这样由该池分配的任何内存块都可用最简单的判断释放,省去了查找线程查找目标池的两次查询,不光提速了而且解决了上一节提到的那个bug。
最终实现的线程关联内存池通用分配函数tm_new大概相当于malloc 15倍左右的速度,定位到pool之后的newobj相当于malloc 45倍左右的速度。通用函数大致相当于nedmalloc速度的2.6-3倍,直接定位到pool的分配速度大概相当于dlmalloc 的2倍。
关于线程关联的内存池还有一些细节问题我没有展开讨论,如free表是每个线程保留一份还是全局保留一份,如果是全局保留一份则涉及到复用的时候如何分配,还有就是tls系列函数我看nedmalloc也在用,我第一版也在用,但后来实测发现这些函数貌似效率不高,后面的版本没有采用tls系列函数。
关于线程关联的内存池我写了5个版本,当然最重要的还是第一个版本,后面的版本除了这一节提到的重要改进之外变化不是很大,最后的第五版增了一些和我的私有lib相关的功能。
以前写文章太少,总是看别人的文章,在网络时代觉得自己挺自私,这次一鼓作气,一口气写了出来,可能写得很粗略,不知道有多少人能看明白,如能给读者一点启示我将感到很欣慰。