引言
最近想实现一个消息队列的程序。消息的基本思想和快递类似: 实际就是数据结构的打包和发送: 申请一块内存,存入需要的数据,把地址告诉接收方让接收方去拿。难免会对内存频繁的请求释放,导致内存碎片化。 所以就想到用内存池来管理内存的申请释放。发送方在内存池中申请内存, 接收方到指定地址取数据,用完之后放回内存池。
网上搜了一下,内存池的实现方式五花八门。其中比较典型的是这篇文章中提到的方法:
http://wenku.baidu.com/view/a46d5e6348d7c1c708a1450a.html
以及
http://www.ibm.com/developerworks/cn/linux/l-cn-ppp/index6.html?ca=drs-cn
不过此文中的内存池只能申请单个内存单元。对于申请较大的内存,除非修改内存单元的大小。 但那样会造成很大浪费。 于是就在这个内存池的基础上进行了修改
源码地址:
http://www.oschina.net/code/snippet_133867_22703
基本架构
原版内存池结构:
http://s4/mw690/4868f986ge0adc3901663&690
改进版内存池结构:
http://s10/mw690/4868f986ge0adc4fe9a79&690
名词
首先确定一下名词:
1) Memorypool叫内存池
2) Memorybolck叫内存块。
3) aData里面可以分配的的每一小段叫内存单元
4) aData里面的连续空闲内存单元叫做空闲内存单元片(简称空闲片),他是由多个内存单元组成的。
改进方法
1) 与原版所不同的在于对于本改进版的memoryblock 是可以取用内存单元片的,而不仅仅是一块内存单元。
2) 对于空闲内存片: 利用内存片的开头四个字节(原例子是两个字节,这里做了扩展)存储特定数据 ,前两个字节用于存储下一个空闲片的偏移, 其后两个字节存储本空闲片的长度。
3) 分配内存的时候,首先根据申请长度计算出所需内存单元的数量。然后逐块查找有无空闲内存片。如果没有,则开辟新的内存块供分配。待分配内存片的前两个字节存储分配的内存单元个数。
4) 内存归还的时候,首先找到归还地址所对应的内存块,再找到此地址对应内存片的起始位置, 如果该内存片前后有空闲片,则将前后的空闲片合并。
5) Mempool构造时不申请内存块。 内存不够用的时候申请内存块。内存块个数有上限, 默认200个。内存块完全空闲的时候不释放,最后析构的时候统一释放。
内存片合并示意图
http://s4/mw690/4868f986ge0adc4eec803&690
数据结构
对于MemoryBlock结构,有如下参数:
nFree 表示本内存块中剩余内存单元的个数
nFirst 指向第一个空闲片的偏移
nSize 本内存块中, 内存单元的个数
pNext 存储下一个内存块地址
aData 存储内存片位置
对于memorypool结构,有如下参数:
MemoryBlock* m_pBlock; //起始内存块地址
SHORT m_nUnitSize; //每一个内存单元的大小,默认64字节
SHORT m_nInitUnitCount; //初始内存块中内存单元个数
SHORT m_nGrowUnitCount; //新增内存块中内存单元个数
SHORT m_BlockCount; //内存块个数
SHORT m_nMaxBlockCount; //内存块最大个数,默认200块
测试结果:
用以下程序测试:
//测试内存池速度
{
list<void *> used_list;
printf("00000\n");
for(int i=0; i < 1000000; i++){
UINT rand_num = rand();
UINT to_do = rand_num%2;
void *data;
if(to_do == 0){ //申请内存
LOGE("Alloc memory!!!");
data = mem_pool.Alloc(rand_num%60);
if(data)
used_list.push_back(data);
}else{//释放内存
if(used_list.empty())
continue;
LOGE("give back memory!!!");
data = used_list.front();
used_list.pop_front();
mem_pool.Free(data);
}
}
printf("11111\n");
while(! used_list.empty()){
void *data = used_list.front();
used_list.pop_front();
mem_pool.Free(data);
}
printf("over!!!\n\n\n");
fflush(stdout);
}
//测试系统new 和delete的速度
{
list<BYTE *> used_list;
printf("22222\n");
for(int i=0; i < 1000000; i++){
UINT rand_num = rand();
UINT to_do = rand_num%2;
BYTE *data;
if(to_do == 0){ //申请内存
LOGE("Alloc memory!!!");
data = new BYTE[rand_num%60];
if(data)
used_list.push_back(data);
}else{//释放内存
if(used_list.empty())
continue;
LOGE("give back memory!!!");
data = (BYTE *)used_list.front();
used_list.pop_front();
delete [] data;
}
}
printf("33333\n");
while(! used_list.empty()){
BYTE *data = used_list.front();
used_list.pop_front();
delete [] data;
}
printf("over!!!\n");
}
同样随机存取100万次,每次申请的大小小于60字节。 内存池速度大约0.8s, 略慢于系统new delete的速度。 但是1000万次则会慢于new 和delete。 估计是由于链表查找速度比较慢。
结论:
内存池能够有效避免内存碎片化,控制内存泄露,至少使内存泄露只发生在池中,避免影响整个系统,且便于跟踪内存使用情况。这是其很大的优点,所以对于长期运行的系统来说,内存池的好处是显而易见的 。但是要在速度上优于new和delete,恐怕不是那么容易,要深入的研究算法。
本文的改进版内存池,可以分配多块内存单元。较原版通用性更好。不过相应的性能会有所下降。
由于通常情况下,单台机器短时间内很少有百万次内存申请释放。所以还是可以很好地应用到程序中。
后续可改进的地方:
对比tcmalloc的实现,这个内存池显然差的远。可以发现一个显著的可以改进的地方:
内存块里面的内存单元大小可以不同。 可以构造一些内存单元小的块,用于小内存申请。
一些内存单元大的块用于大内存申请。 如此区分一下可以减少内存合并的情况,提高速度
加载中,请稍候......