CppExplore

一切像雾像雨又像风

  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  29 随笔 :: 0 文章 :: 280 评论 :: 0 Trackbacks

作者:CppExplore 地址:http://www.cppblog.com/CppExplore/
(2)boost::pool
系列。boost的内存池最低层是simple_segregated_storage,类似于Loki中的chunk,在其中申请释放block(boost中把block称为chunk,晕死,这里还是称其为block)采用了和loki的chunk中同样的算法,不同的是simple_segregated_storage使用void*保存block的块序号,loki中使用char,因此boost中的simple_segregated_storage没有255的上限限制,自然也就不需要再其上再封装一层类似与FixedAllocator的层面。另boost没有屏蔽块的大小,直接提供定长的接口给用户,省掉了SmallObjAllocator层面。因此boost的内存池申请释放block的时间复杂度都是O(1)(object_pool和pool_allocator除外),另避免的小内存的浪费,同时boost不能象loki那样在将block归还给内存池的时候根据chunk的空闲数量释放内存归还给系统,只能显式调用释放内存函数或者等内存池销毁的时候,基本上和内存池生命周期内永不释放没什么区别。
    boost的最低层是simple_segregated_storage,主要算法和loki中的chunk一样,不多说了。这里说下影响上层接口的两类实现:add_block/malloc/free、add_ordered_block/malloc/ordered_free,两种低层实现造成boost上层设计的成功与失败,前者效率高,和loki一样直接增加释放,时间复杂度O(1),后者扫描排序,时间复杂度O(n)。
    boost提供了四种内存池模型供使用:pool、object_pool、singleton_pool、pool_allocator/fast_pool_allocator。
1)pool
基本的定长内存池

#include <boost/pool/pool.hpp>
typedef struct student_st
{
   
char name[10];
   
int age;
}
CStudent;
int main()
{
   boost::pool
<> student_pool(sizeof(CStudent));
   CStudent 
* const obj=(CStudent *)student_pool.malloc();
   student_pool.free(obj);
   
return 0;
}

    pool的模版参数只有一个分配子类型,boost提供了两种default_user_allocator_new_delete/default_user_allocator_malloc_free,指明申请释放内存的时候使用new/delete,还是malloc/free,默认是default_user_allocator_new_delete。构造函数有2个参数:nrequested_size,nnext_size。nrequested_size是block的大小(因为void*保存序号,因此boost内置了block的最小值,nrequested_size过小则取内置值),nnext_size是simple_segregated_storage中内存不足的时候,申请的block数量,默认是32。最全面的实例化pool类似这样:boost::pool<boost::default_user_allocator_malloc_free> student_pool(sizeof(CStudent),255);
    pool提供的函数主要有:
malloc/free  基于add_block/malloc/free实现,高效
ordered_malloc/ordered_free 基于add_ordered_block/malloc/ordered_free实现,在pool中无任何意义,切勿使用。
release_memory/purge_memory 前者释放池中未使用内存,后者释放池中所有内存。另池析构也会释放内存

2)object_pool

对象内存池,这是最失败的一个内存池设计。

#include <boost/pool/object_pool.hpp>

class A{
public:
   A():data_(
0){}
private:
   
int data_;
}
;
int main()
{
   boost::object_pool
<A> obj_pool;
   A 
*const pA=obj_pool.construct();
   obj_pool.destroy(pA);
   
return 0;
}

    object_pool继承至pool,有两个模版参数,第一个就是对象类型,第二个是分配子类型,默认同pool是default_user_allocator_new_delete。构造函数参数只有nnext_size,意义以及默认值同pool。最全面的实例化object_pool类似这样:boost::pool<A,boost::default_user_allocator_malloc_free> obj_pool(255);
object_pool提供的函数主要有(继承至父类的略):
malloc/free 复写pool的malloc/free,add_ordered_block/malloc/ordered_free实现
construct/destroy 基于本类的malloc/free实现,额外调用默认构造函数和默认析构函数。
~object_pool 单独拿出这个说下,若析构的时候有对象未被destroy,可以检测到,释放内存前对其执行destroy
    为什么boost::object_pool要设计成这样?能调用构造函数和析构函数显然不是boost::object_pool类设计的出发点,因为构造函数只能执行默认构造函数(首次发表错误:可以调用任意的构造函数,参见代码文件:boost/pool/detail/pool_construct.inc和boost/pool/detail/pool_construct_simple.inc,感谢eXile指正),近似于无,它的重点是内存释放时候的清理工作,这个工作默认的析构函数就足够了。apr_pool内存池中就可以注册内存清理函数,在释放内存的时刻执行关闭文件描述符、关闭socket等操作。boost::object_pool也想实现同样的功能,因此设计了destroy这个函数,而同时为了防止用户遗漏掉这个调用,而又在内存池析构的时候进行了检测回收。为了这个目的而又不至于析构object_pool的时间复杂度是O(n平方),boost::object_pool付出了沉重的代价,在每次的destoy都执行排序功能,时间复杂度O(n),最后析构的时间复杂度是O(n),同样为了这个目的,从simple_segregated_storage增加了add_ordered_block/ordered_free,pool增加了ordered_malloc/ordered_free等累赘多余的功能。
    基于上面讨论的原因,boost::object_pool被设计成了现在的样子,成了一个鸡肋类。类的设计者似乎忘记了内存池使用的初衷,忘记了内存池中内存申请释放的频率很高,远远大于内存池对象的析构。如果你依然想使用类似于此的内存清理功能,可以在boost::object_pool上修改,不复写malloc/free即可,重写object_pool的析构,简单释放内存就好,因此析构object_pool前不要忘记调用destroy,这也是使用placement new默认遵守的规则,或者保持以前的析构函数,牺牲析构时的性能。placement new的作用是为已经申请好的内存调用构造函数,使用流程为(1)申请内存buf(2)调用placement new:new(buf)construtor()(3)调用析构destructor()(4)释放内存buf。#include<new>可以使用placement new。
3)singleton_pool
pool的加锁版本。

#include <boost/pool/singleton_pool.hpp>
typedef struct student_st
{
   
char name[10];
   
int age;
}
CStudent;
typedef struct singleton_pool_tag
{}singleton_pool_tag;
int main()
{
   typedef boost::singleton_pool
<singleton_pool_tag,sizeof(CStudent)>  global;
   CStudent 
* const df=(CStudent *)global::malloc();
   global::free(df);
   
return 0;
}

    singleton_pool为单例类,是对pool的加锁封装,适用于多线程环境,其中所有函数都是静态类型。它的模版参数有5个,tag:标记而已,无意义;RequestedSize:block的长度;UserAllocator:分配子,默认还是default_user_allocator_new_delete;Mutex:锁机制,默认值最终依赖于系统环境,linux下是pthread_mutex,它是对pthread_mutex_t的封装;NextSize:内存不足的时候,申请的block数量,默认是32。最全面的使用singleton_pool类似这样:typedef boost::singleton_pool<singleton_pool_tag,sizeof(CStudent),default_user_allocator_new_delete,details::pool::default_mutex,200>  global;
    它暴露的函数和pool相同。
4)pool_allocator/fast_pool_allocator
    stl::allocator的替换方案。两者都是基于singleton_pool实现,实现了stl::allocator要求的接口规范。两者的使用相同,区别在于pool_allocator的实现调用ordered_malloc/ordered_free,fast_pool_allocator的实现调用malloc/free,因此推荐使用后者。

#include <boost/pool/pool_alloc.hpp>
#include 
<vector>
typedef struct student_st
{
 
char name[10];
 
int age;
}
CStudent;

int main()
{
  std::vector
<CStudent *,boost::fast_pool_allocator<CStudent *> > v(8);
  CStudent 
*pObj=new CStudent();
  v[
1]=pObj;
  boost::singleton_pool
<boost::fast_pool_allocator_tag,sizeof(CStudent *)>::purge_memory(); 
  
return 0;
}

    fast_pool_allocator的模版参数有四个:类型,分配子,锁类型,内存不足时的申请的block数量,后三者都有默认值,不再说了。它使用的singleton_pool的tag是boost::fast_pool_allocator_tag。
评价:boost::pool小巧高效,多多使用,多线程环境下使用boost::singleton_pool,不要使用两者的ordered_malloc/ordered_free函数。boost::object_pool不建议使用,可以改造后使用。pool_allocator/fast_pool_allocator推荐使用后者。


未完 待续.................... 不过这个主题暂时不写了 等有时间了
posted on 2008-02-20 15:09 cppexplore 阅读(6653) 评论(21)  编辑 收藏 引用

评论

# re: 【原创】系统设计之 内存管理(三) 2008-02-20 19:07 空明流转
确实,本来打算用obj_pool的,后来一看complexity,拉倒吧。。。  回复  更多评论
  

# re: 【原创】系统设计之 内存管理(三) 2008-02-20 23:15 eXile
我觉得lz对object_pool 的设计理念和如何使用都存在理解上的错误, 特别是以下几点:
1) 构造函数只能执行默认构造函数
2) 设计了destroy这个函数,而同时为了防止用户遗漏掉这个调用,而又在内存池析构的时候进行了检测回收
3)为了这个目的而又不至于析构object_pool的时间复杂度是O(n平方)


object_pool 主要着眼于“自动析构”,在没有gc的情况下,达到提高效率和自动管理内存的目的。而且它也特别适合于“多次申请,一次释放”的情况.所以它甚至是鼓励你忽略使用destroy(从它的例子就可以看出来)。

destroy函数并没有提高复杂度,因为内部链表始终处于有序状态(由于使用order_malloc,order_free),所以不论是逐个释放,还是成批释放,它的复杂度都是O(N)

  回复  更多评论
  

# re: 【原创】系统设计之 内存管理(三) 2008-02-21 09:08 cppexplore
另:boost/pool下的6个hpp文件我是挨个读过了。detail下的都很简单,5个hpp,singleton.hpp有效行数就10几行,没看,想当然了下。mutex和guard在读singleton_pool.hpp的时候看了下linux下的mutex,顺便还测试了下,gcd_lcm的两个也很简单,没看,估计大约是2者求最小值的功能。
我一贯认为,明白原理,知道如何使用就好,深入具体细节就是浪费脑细胞,如果你要实现一个当然例外。  回复  更多评论
  

# re: 【原创】系统设计之 内存管理(三) 2008-02-21 10:29 cppexplore
@eXile
晕倒 在boost/pool/detail/pool_construct.inc里
只关注hpp去了
可以调用任意的构造函数
多谢指正!正文中现已标明。  回复  更多评论
  

# re: 【原创】系统设计之 内存管理(三) 2008-02-21 10:48 eXile
呵呵,不当之处,敬请原谅。
“多次申请,一次释放”的情况,比如说对于服务器而言,从收到一个包,到对这个包的处理完毕,则可视为内存的一个周期。不过有时候异步完成,再加上object_pool只能管理一种对象,又限制了这种使用。  回复  更多评论
  

# re: 【原创】系统设计之 内存管理(三)[未登录] 2008-02-21 11:06 cppexplore
@eXile
:)
“从收到一个包,到对这个包的处理完毕,则可视为内存的一个周期。”,这时候析构object_pool不恰当,因为以后还会收到包,内存还可循环被使用,这里还是应该调用destroy,而它的时间复杂度o(n),导致了真是不太适合使用。析构object_pool更不可取,时间复杂度不说,还有内存的再次申请,背离了内存池的初衷。  回复  更多评论
  

# re: 【原创】系统设计之 内存管理(三) 2008-02-21 11:12 eXile
这个倒不是问题,因为object_pool是可配置的, 它带有一个模板参数UserAlloc, 可以自定义  回复  更多评论
  

# re: 【原创】系统设计之 内存管理(三)[未登录] 2008-02-21 11:28 cppexplore
问题的关键不在UserAlloc,而是details::PODptr<size_type> 。除非你想在内存池之上实现这个内存池的UserAlloc(到底是先有鸡还是先有蛋......),即便这样,当前的object_pool析构最少也要付出o(n)的代价。  回复  更多评论
  

# re: 【原创】系统设计之 内存管理(三) 2008-02-21 11:50 eXile
呵呵,正是如此,UserAlloc可以使用一个全局的boost::pool来实现,所谓大pool套小pool, 不过如果object_pool设计时完全放弃destroy, 则可以取得更大的优化。  回复  更多评论
  

# re: 【原创】系统设计之 内存管理(三)[未登录] 2008-02-21 11:52 cppexplore
晕倒..........................  回复  更多评论
  

# re: 【原创】系统设计之 内存管理(三) 2008-02-21 12:07 jazz
好久之前开始用pool,不过一直不用object_pool当时就看到order_mallc的o(n)复杂度。当时一直没想明白 为什么用object_pool中为什么申请内存使用order_mallc来替代malloc。  回复  更多评论
  

# re: 【原创】系统设计之 内存管理(三) 2008-02-21 12:12 jazz
memory pool 本来是为提高性能,但是object_pool对于destroy-o(n)的性能怎么能够容忍?  回复  更多评论
  

# re: 【原创】系统设计之 内存管理(三) 2008-02-21 14:02 空明流转
我本来就是小块的分配释放,而且只需要单线程就够了,所以一开始的时候写了个自己的池,现在考虑用boost的环池替代。  回复  更多评论
  

# re: 【原创】系统设计之 内存管理(三) 2008-04-27 07:25 王贵阳
非常好,仔细研究。  回复  更多评论
  

# re: 【原创】技术系列之 内存管理(三) 2009-04-04 18:30 yshuise
object_pool设计成那样的根本原因,就是还可以作为内存池使用。不是真正的释放内存。所以,博主理解不对啊  回复  更多评论
  

# re: 【原创】技术系列之 内存管理(三)[未登录] 2009-04-04 19:37 cppexplore
@yshuise
你确信你看懂它的实现了?  回复  更多评论
  

# re: 【原创】技术系列之 内存管理(三) 2011-08-10 11:57 sa
既然是一个定长的pool,为何simple_segregated_storage还要拆分成那么多chunk干吗,直接把所有的obj通过一个linklist串在一起不就行了?  回复  更多评论
  

# re: 【原创】技术系列之 内存管理(三)[未登录] 2011-08-10 12:30 cppexplore
@sa
原因: 1) 和linklist相比更节省内存 2)和linklist相比,申请/归还内存更快  回复  更多评论
  

# re: 【原创】技术系列之 内存管理(三)[未登录] 2011-10-20 21:54 chipset
看了三篇文章。不知道该说啥好,单CPU,DLMalloc足矣,其它我都不看好,最不看好用完最后一次性释放的内存池,原因不解释。

多CPU,ptmalloc3,或glib里的内存管理器(以ptmalloc3为基础的改进),谷歌的TCMalloc,还有Free BSD的Jemalloc,以及Hoard。多CPU,碎片率低,好的线性加速比是关键,Jemalloc很强。TCMalloc看上去像个多CPU的自有列表内存管理器,加速比也不错,且有垃圾收集,但是启动速度跟启动Java虚拟机绝对有一拼...这些都是免费的。至于那些要钱的,我懒得一提...  回复  更多评论
  

# re: 【原创】技术系列之 内存管理(三) 2011-12-16 12:21 cppexplore
@chipset
站在"造轮子"角度 / "使用轮子" 角度 /"选轮子"角度 看到的东西各有些不同.  回复  更多评论
  

# re: 【原创】技术系列之 内存管理(三)[未登录] 2012-03-26 20:49 cexer
我觉得boost::pool的ordered_malloc和ordered_free函数是有必要的,它们是作为ordered pool的接口部分而存在的,使用场景不一样,和malloc和free是没有可比性。

boost::pool中有一个函数malloc_n用来申请几块连续的partition。一个block是以partition为粒度划分,以单向链表链接起来,多次malloc/free的动作之后,这些partition在链表中的逻辑顺序和它们在内存中的实际顺序极有可能是不一致的,此时要得到连续的partition,必须要遍历链表,找出在内存中实际连续的partition,并将它们在链表中的顺序也重新以连续的顺序链接,然后返回给调用者。

如果将链表的顺序与内存中实际顺序的不一致程度叫做它的无序度,显然这个遍历操作的耗费的时间,和链表的无序度是成正比的。如果这种malloc_n的操作很频繁,那么这种遍历链表重新链接的操作会很费时。

这种频繁申请多个连续partition的场景下正是使用ordered pool的地方,正如pool作者在代码写的注释那样:
// Note: if you're allocating/deallocating n a lot, you should
// be using an ordered pool.

ordered pool内部的所有partition链表无序度为0,即所有partition在内存中的实际顺序和它们在链表中的逻辑顺序完全一致,对于malloc_n这样的请求,可以更快满足。也因此boost::pool提供了ordered版本的操作接口,这组接口保证每次free之后链表的顺序不会乱。

不过,我个人觉得block以partition为粒度划分是没必要的,可以在收到请求的时候按实际需求划分,尽力使整个块保持连续,可以极大地避免产生碎片,降低ordered_malloc的ordered_free的时间。

当然ordered_xxx函数不应该和其它的函数混用,否则就是陪了夫人又折兵的买卖,即浪费了ordered_xxx的时间,又无法得到更快malloc_n的好处。
  回复  更多评论
  


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理