一:
class simple_segregated_storage; boost/pool/simple_segregated_storage.hpp
simple_segregated_storage (以下用SSS替代)实现一个单向链表,
链表的每个节点是一个固定大小并且未被使用的内存块。
具体做法:
1,void* first 标记链表头。//如果first=NULL,则这是一个空链表
void* next = *first; //第二个节点,如果next=NULL,则表明first没有子节点。
这里的实现有个小技巧:举个小例子
char buf[12]; 分成3块,每块大小为4个字节。
每个指针的大小是4个字节,buf[0]-b[3]刚好能保存下一个节点的地址。
同理 buf[4]-b[7], b[8]-[11]都能保存下一个节点的信息。
(void*)(buf+8) = 0,表示连表尾部。
这样这个链表不需要多余的空间,只需要一个first便可以了。
类中nextof函数返回 ptr的下一个节点。
void *& nextof(void * const ptr)
{ return *(static_cast<void **>(ptr)); }
SSS类实现的功能:
1,void add_block(void* const block, const zize_t size,
const size_t small_size);
把一段连续的大小为size大内存块加入到链表中,每个块大小为small_size
2,void add_ordered_block(void* const block, const zize_t size,
const size_t small_size);
功能同上,如果原链表有序,则保证插入后链表有序。
3,void* malloc();
申请一个块。
4,void free(void * const chunk);
释放一个块,加入链表中。
5,void ordered_free(void * const chunk);
同free,如果原链表有序,则此插入后仍然有序。(前后都是升序)
6,void * malloc_n(size_type n, size_type partition_size);
申请n块, 大小为partition_size的连续内存。有序链表更容易申请成功。
这个函数只是遍历链表
void * iter = nextof(start);
while (--n != 0)
{
void * next = nextof(iter);
if (next != static_cast<char *>(iter) + partition_size)
{
start = iter;
return 0;
}
iter = next;
}
return iter;
7,void free_n(void * const chunks, const size_type n,
const size_type partition_size);
调用add_block函数,把n块每块大小为partition_size的连续内存插入链表。
8,void ordered_free_n(void * const chunks, const size_type n,
const size_type partition_size);
二:class pool // boost/pool/pool.hpp
1: class PODptr; 这个类保存一个固定大小的内存块,SSS类保存的是PODptr中内存块分割后的小块。
成员:
char * ptr; // 块的首地址
size_type sz; // 块的大小
如果PODptr保存100个块,每个块的大小是10。则:
char *buf = new char[100*10 + 8];
ptr = buf; size=100*10+8;
其中前100个块交给SSS来管理((ordered_)add_block)。
最后8个字节,可以看成1个指针(buf[1000]-buf[1003])和1个unsigned int(buf[1004]-buf[1007])。
这个指针保存下一个PODptr(如果存在的话)的内存快的位置,
这个unsigned int值,保存下一个PODptr内存快的大小。
其实PODptr是一个链表的节点...。
pool用两个类SSS和PODptr来管理内存。用户从boost::pool中申请内存的时候,
首先判断SSS链表是否为空,如果非空,则直接分配SSS链表保存的内存快,并把分出去的块从链表中删除;
如果SSS为空,则申请一个大的内存块,PODptr来保存这个大内存快的信息。并把这个大的内存块分割成N个
小的内存块,分配给SSS。
2,class pool;
pool 提供的功能:
1,void * malloc();
2,void * ordered_malloc(); // 这个是在SSS空间不够的时候,需要增加新的PODptr,保证PODptr有序。
3,void * ordered_malloc(size_type n);
4,void free(void * const chunk);
void orderd_free(void* const chunk);
5,void free(void * const chunks, const size_type n);
6,void ordered_free(void * const chunks, const size_type n); 有序释放(添加到SSS中,并不是deleted掉)
7,bool is_from(void * const chunk); //判断chunk是否是此pool分配出去的。
8,release_memory();//释放掉pool的所有内存(delete)。这个函数会在pool的析构函数中调用。
1: pool只能分配固定大小的内存块。(SMALL_BLOCK_SIZE)
2: pool的默认的策略,第一次先申请32块,即第一个PODptr中内存大小是 32* SMALL_BLOCK_SIZE + 8;
之后每次申请前一次申请空间的2倍。(32,64,128...),这样做空间利用率为1/(2^N/N +1) > 50%,(N表示第几次申请)
并且不会频繁向系统申请大的内存块。如果你最多需要N块内存,则向系统申请内存的次数是lgN,lgN是一个很小的复杂度。
3: pool如果不析构,空间只会慢慢变大,即巅峰期,你用掉了100000个块的内存,即使全部还给了pool,pool也不会释放PODptr。
这个与stl::vector实现相同。在vector对象不析构的时候,空间只会变大(assign,resize操作除外,不过pool没有提供类似操作)。
4: pool的实现非常好,很多人可以用更精简的代码写出更高常数效率的pool,但是boost::pool的实现方方面面考虑很周到。
三:
boost作为一个极其很庞大并且牛X的C++后备库,当然不会紧紧提供这些基础功能。
个人感觉boost一直致力于让C++看起来更像高级语言(甚至脚本语言), 效率又远远高于脚本语言和其他高级OO。
给boost的使用者提供更好的体验。
boost::object_pool 是一个真正不用释放内存,在几乎所有情况下都不会有内存泄漏的内存管理器。
声明:
tempate<class element_type>
public object_pool : publib pool{}
成员函数:
1,element_type * construct(); 申请一个用默认值的对象指针。
//析构对象,并把内存交给pool,C++使用者的一个良好习惯或者说C++的一个限制是,
//很多东西一定要成对使用(譬如申请内存new,delete;文件IO open,close;互斥锁lock,unlock)
//但是使用ojbect_pool 申请对象,却不需要destory
2,void destroy(element_type * const chunk);//destory 会调用chunk的析构函数
class Sample
{
char *buf;
size_t buf_size;
public :
Sample(){ buf = new char [1000]; buf_size=1000;}
~Sample() { delete[]buf; }
};
Sample* pa = new Sample();
delete pa; // 首先调用 pa.~Sample(); 释放buf申请的内存,其次释放pa所占用的内存。
pa = obj_pool.construct(); // 从pool中分配一个空间给pa,并调用pa的构造函数。
obj_pool.destroy(pa); // 先调用pa.~Sample(); 然后把pa所占的空间交给pool。
对于大多数内存管理器,必须成对的调用construct和destroy。因为pool对象析构的时候,只会释放对象占用的空间,
但是不会调用对象的析构函数,此时,对象动态申请的内存就会泄露掉。(此例子中的buf指向的内存)。
但是object_pool则允许你忘记调用 destory。
boost::object_pool做法:
object_pool 会一直保证 SSS链表和PODptr链表有序,在object_pool的析构函数中,会帮你调用对象的析构函数。
做法就是遍历PODptr链表,如果其中某个PODptr的某一块内存不在SSS中(即不是空闲内存,没有被释放),
则调用此块内存所指向对象的析构函数。
object_pool的析构函数实现大概实现如下:
PODptr* head;
for(PODptr* node = head; node != NULL;)
{
for( i=0; i<node.size(); ++i )
{
if ( node[i] != SSS.first )
{
// node的第[i]个节点没有被释放。
(element_type *)(node[i]).~element_type(); //调用析构函数。
sss.first = sss.first.next();
}
}
PODptr* tmp = node;
node = node.next();
delete tmp;
};
object_pool析构的复杂度是O(N)
由于要一直保证链表SSS有序。(当然也需要保证PODptr有序,但是PODptr的链表大小相对很小,lg级别的)
在一个有序链表中插入一个节点的最坏复杂度是0(N)。
为了保证SSS有序,每次destory的最坏复杂度是0 (N) 。
很容易构造一种最坏复杂度的情况:
const int N = 10000;
Sample* a[N];
for( i=0; i<N; ++i)
a[i] = pool.cunstruct();
for(i=0; i<N; ++i)
pool.destory( a[i] );
每次destory的复杂读是O(N)。总复杂度是0(N*N)。
如果pool的生存周期比较长,那总的destory的复杂度会高的离谱,在一些特别需要效率的程序中,完全不可接受。
一种可能的优化方案: 每次destory并不保证 SSS有序,而是在 pool_object的析构函数中,对链表SSS排序,
这样每次申请,释放的平均复杂度是0(1)。析构的复杂度是0(NlgN)。在pool_object对象生命周期相对比较长的情况下。
这个复杂度完全可以接受。
当然,优化只是针对个别使用场合。不可能存在一个方法能够使得在任意情况下都是最优的。
我目前的大多数程序,都是在程序开始时需要申请一个pool对象,在程序结束的时候释放这个pool对象,
所以我更倾向于在析构中排序的策略。当然,我的这种使用内存分配的方法,完全没有必要使用boost::object_pool。
假如中间某次不调用destory,必然会导致程序内存占用越来越大,所以,object_pool的对象生命周期应该不要太长。
如果boost::object_pool 保证每次申请释放的复杂度是0(1),pool对象析构的复杂度即使是0(N*N)我也可以接受。
一个在linux服务器上跑的程序,总是希望它能一直跑下去。
PS:boost::object_poll 的实现用了很多C++技巧,强烈建议C++爱好者读下源码。任何技术类的文档,都不会比直接阅读
源码来的实在。