找回密码
 用户注册

QQ登录

只需一步,快速开始

查看: 16541|回复: 15

ACE_Message_Block使用研究

[复制链接]
发表于 2010-6-22 11:16:20 | 显示全部楼层 |阅读模式
最近在持续优化和改善PurenessScopeServer的性能,发现自己要做的工作还是较多的,这里很感谢各位朋友对这套系统的支持,不断的给我经验和各种测试,更有应用于实际工作中的实验。
前一段时间,freebird做了一个客户端,测试在Proactor模式下链接上万个链接的测试,发现服务器在处理大量并行链接进入的时候,比较卡。经过仔细的测试分析,发现问题出现在大量的new消耗了太多的时间,导致服务器处理较慢。于是去除了大量的new,使用对象池替换,同时对大量使用的ACE_Message_Block进行了一定的优化。目前测试的结果比较满意,CPU和IO都降低了很多,50个线程同时并发连接发送数据,每秒每个链接10个数据包,CPU大部分时间在0.0%左右。IO稳定在4K左右,等这个版本全部测试通过,我会公布给大家,这里强烈感谢freebird的测试以及开发的支持。当然还有sun无名指和其他的朋友。
先说大家经常用的ACE_Message_Block,这是ACE里面经常使用到的数据载体。因为需要大量的new和delete。所以这里的优化对系统性能有着很大的影响。
首先一般ACE_Message_Block的使用模式是:
ACE_NEW_NORETURN(pmb, ACE_Message_Block(u4PackeLen));  
声明一个指定长度为u4PackeLen的ACE_Message_Block对象。
需要释放ACE_Message_Block的时候,pmb->release();
这里需要注意的是,release()的时候,并不是所有的时候,都会释放内存。release()里面有一个引用计数,如果这个计数器为0 的时候才会释放内存。影响这个计数器的函数是duplicate()函数(浅拷贝),而Clone()(深拷贝)不会影响这个计数器,所以如果使用了duplicate()函数,一定要注意释放。否则会容易引起内存的泄露。
如果不在使用内存池的情况下,ACE_Message_Block的效能如何呢?我们以100万次new和delete释放为例。看看效能。
测试代码如下:
  1.         ACE_Time_Value tvBegin = ACE_OS::gettimeofday();
  2.         for(int i = 0; i < 1000000; i++)
  3.         {
  4.                 //没有使用内存池
  5.                 ACE_Message_Block* pBlock = new ACE_Message_Block(1024);    //创建一个1024字节的ACE_Message_Block对象
  6.                 pBlock->release();
  7.         }
  8.         ACE_Time_Value tvEnd = ACE_OS::gettimeofday();
  9.         printf("[Cost]Cost time is %d.\n", (tvEnd - tvBegin).msec());
复制代码
运行结果是:

[Cost]Cost time is 15078.

也就是花费了15秒左右。
在服务器的运用中,这样的时间效能是比较影响服务器性能的。
在我看来,其实从服务器角度而言,如果是基于协议的数据,大部分协议是定长的,也就是说,内存是固定长度的,如果能做到,Message_Block不真正释放,只是把内存保存起来等待下一次一样程度的内存申请,直接给Block使用,或许更具备效率。
于是,写一个内存池试试,封装一个ACE_Message_Block内存池的类。
类只需要两个接口:
ACE_Message_Block* Create(int u4Size);               //获得一个指定长度的ACE_Message_BlocK对象块
bool Close(ACE_Message_Block* pMessageBlock);   //使用完,归还一个ACE_Message_BlocK对象块

要使用ACE的内存池,需要做一些准备工作。
  1. //定义一个内存管理分配器
  2. typedef  ACE_Malloc<ACE_LOCAL_MEMORY_POOL, ACE_SYNCH_MUTEX> MUTEX_MALLOC;
  3. typedef ACE_Allocator_Adapter<MUTEX_MALLOC> Mutex_Allocator;
  4. Mutex_Allocator _msg_mb_allocator;
复制代码

ACE_Malloc是一个内存池的模板类,它支持ACE_MMAP_MEMORY_POOL,ACE_LITE_MMAP_MEMORY_POOL,ACE_SBRK_MEMORY_POOL,ACE_SHARED_MEMORY_POOL,ACE_LOCAL_MEMORY_POOL,ACE_PAGEFILE_MEMORY_POOL。
这里ACE_LOCAL_MEMORY_POOL是继承于C++的new,剩下的几个大部分是服务于进程间数据共享。这里需要注意的是,ACE_MMAP_MEMORY_POOL可以将共享内存映射成文件形式,但是内存池如果一开始不声明足够,在使用过程中会导致内存的增长,进程间内存池增长,会导致内存池的基地址发生变化,导致原先指向内存池的地址发生错误。这里一定要小心。关于ACE进程级内存池类型的应用,这里是一个新的话题,等有时间专门开个帖子讨论。这里先说最简单的ACE_LOCAL_MEMORY_POOL。
  1. ACE_Message_Block* CMessageBlockManager::Create(uint32 u4Size)
  2. {
  3.         ACE_Guard<ACE_Recursive_Thread_Mutex> WGuard(m_ThreadWriteLock);
  4.         ACE_Message_Block* pmb = NULL;
  5.         ACE_NEW_MALLOC_NORETURN(pmb,
  6.                 ACE_static_cast(ACE_Message_Block*, _msg_mb_allocator.malloc(sizeof(ACE_Message_Block))),
  7.                 ACE_Message_Block(u4Size, // size
  8.                 ACE_Message_Block::MB_DATA, // type
  9.                 0,
  10.                 0,
  11.                 &_msg_mb_allocator, // allocator_strategy
  12.                 0, // locking strategy
  13.                 ACE_DEFAULT_MESSAGE_BLOCK_PRIORITY, // priority
  14.                 ACE_Time_Value::zero,
  15.                 ACE_Time_Value::max_time,
  16.                 &_msg_mb_allocator,
  17.                 &_msg_mb_allocator
  18.                 ));
  19.         return pmb;
  20. }
  21. bool CMessageBlockManager::Close(ACE_Message_Block* pMessageBlock)
  22. {
  23.         ACE_Guard<ACE_Recursive_Thread_Mutex> WGuard(m_ThreadWriteLock);
  24.         pMessageBlock->release();
  25.         return true;
  26. }
复制代码

其实代码一点都不复杂,只是稍微添加了一句话。
如果在多线程下调用
ACE_Guard<ACE_Recursive_Thread_Mutex> WGuard(m_ThreadWriteLock);这样的线程锁是必要的。
测试代码如下:
  1.         ACE_Time_Value tvBegin = ACE_OS::gettimeofday();
  2.         for(int i = 0; i < 1000000; i++)
  3.         {
  4.                 //使用内存池
  5.                 ACE_Message_Block* pBlock = NULL;
  6.                 pBlock = App_MessageBlockManager::instance()->Create(1000);
  7.                 if(pBlock == NULL)
  8.                 {
  9.                         printf("[Main]Get Data is NULL.\n");
  10.                         break;
  11.                 }
  12.                 pBlock->release();  //这里可以不用调用App_MessageBlockManager::instance()->Close(pBlock),这两句是等价的。
  13.         }
  14.         ACE_Time_Value tvEnd = ACE_OS::gettimeofday();
  15.         printf("[Cost]Cost time is %d.\n", (tvEnd - tvBegin).msec());
复制代码
运行结果是:

[Cost]Cost time is 2750.

比较两种使用,效果的提升是显而易见的。

在服务器应用中,越来越觉得,在大压力并发下,new和delete,以及memcpy是相当耗时的,相对逻辑代码上的优化而言,这种优化更加迫切。
呵呵,写出一点自己的感想,抛砖引玉。
发表于 2010-6-22 16:36:00 | 显示全部楼层
支持,,好文,,,类似在哪里论文中也见过此类做法。。。。。
发表于 2010-6-23 07:23:03 | 显示全部楼层
现在流行线程分配器,每个线程有个freelist,这样就不用mutex了。
发表于 2010-6-23 23:58:38 | 显示全部楼层

回复 #1 freeeyes 的帖子

在服务器应用中,越来越觉得,在大压力并发下,new和delete,以及memcpy是相当耗时的,相对逻辑代码上的优化而言,这种优化更加迫切。
呵呵,写出一点自己的感想,抛砖引玉。

像一些开源的项目如CEGUI, OGRE也存在这样的问题, 大量的new和delete耗费了时间, 游戏项目应用这两个组合的都会碰到,并对其优化改造。
我在游戏项目逻辑层面的数据管理上倾向于把数据放在STL 的用vector、llist 或 map管理,而逻辑只是对这些数据进行维护(修改,添加,删除操作),逻辑代码里没有new delete,避免了错误,降低复杂性,避开了C++的对象,继续抛砖引玉
 楼主| 发表于 2010-6-24 13:41:29 | 显示全部楼层
其实对于性能和实用性而言,STL是不错的。不过,在游戏服务器开发中,更多的时候为了追查数据的BUG,而不用STL的容器(因为它的内存预分配会干扰当前数据内存的占用量的判断)。一般都是自己写的链表和容器替代STL,性能上的损失换来的是数据内存的可控性。
这一点,我觉得见人见智吧。
发表于 2010-6-24 15:48:01 | 显示全部楼层
为什么直接不使用ACE_Cached_Allocator呢?

我网络收发的使用引入内存池之后,在大并发量下(接近18000活跃连接),
每连接每秒1024字节echo情况下测试,内存基本无明显变化,但是吞吐量有质的飞跃。

另外ACE_Message_Block的构造函数定义了三个内存分配器,
这三个内存分配器使用很有讲究的。
ACE_Allocator *allocator_strategy,   真正数据的分配器
ACE_Allocator *data_block_allocator, ACE_Data_Block的分配器
ACE_Allocator *message_block_allocator ACE_Message_Block自身的分配器

因此个人经验最好不要在这里使用相同的分配器。
data_block_allocator,message_block_allocator 分别使用
与ACE_Data_Block,ACE_Message_Block对象尺寸大小一致的内存池。
typedef ACE_Cached_Allocator<ACE_Message_Block,ACE_SYNCH_MUTEX> MblkAlloc;
allocator_strategy真正数据的分配器由于数据不固定,


因此根据自己应用简化处理,假设目前应用绝大多数情况网络传输字节小于1024
可以使用1024字节固定大小的内存池,小于1024字节直接使用内存池分配,
typedef char MEMORY_DATA[1024];
typedef ACE_Cached_Allocator<MEMORY_DATA,ACE_SYNCH_MUTEX> DataAlloc;
如果实际网络接收数据大于1024的情况下这里不使用内存分配策略,
即直接使用系统new直接分配。

[ 本帖最后由 modern 于 2010-6-24 15:54 编辑 ]
 楼主| 发表于 2010-6-25 18:21:04 | 显示全部楼层
发表于 2010-6-29 23:14:28 | 显示全部楼层
顶一个,现在正在处理这里的问题
 楼主| 发表于 2010-7-2 13:25:17 | 显示全部楼层
To modern:
最近网站不是很稳定,很多文章写了一半就白费了,我很想和你探讨一下ACE_Cached_Allocator这个模板的本质用法,呵呵,期待你的回复。
我以前看过ACE_Cached_Allocator的一些说明,在实际操作中,我发现ACE_Cached_Allocator似乎很容易引起Message_Block的崩溃。最近研究了这个几天,并和ACE_Malloc&lt;ACE_LOCAL_MEMORY_POOL, ACE_SYNCH_MUTEX&gt;做了一下比较。
ACE_Cached_Allocator模板在初始化的时候,需要两个参数。
  1. typedef char BUFFCOUNT[1024];
  2. m_pbuff_allocator = new ACE_Cached_Allocator<BUFFCOUNT,ACE_SYNCH_MUTEX>(10);
复制代码

这里的意思是,申请一个个数为10个的并制定大小的BUFFCOUNT内存块(比如这里制定的是1024为一个块),这里,也就是池里的空间是10*1024个字节。一次性的new出来,并放入char*中。
具体ACE_Cached_Allocator在构造函数(Malloc_T.cpp)中,如此定义的。
  1.   size_t chunk_size = sizeof (T);
  2.   chunk_size = ACE_MALLOC_ROUNDUP (chunk_size, ACE_MALLOC_ALIGN);
  3.   ACE_NEW (this->pool_,
  4.            char[n_chunks * chunk_size]);
复制代码
可见n_chunks 对应的是(10),而模板对象是sizeof(T);其实如此说来,完全可以把
m_pbuff_allocator = new ACE_Cached_Allocator<BUFFCOUNT,ACE_SYNCH_MUTEX>(10);
改成
m_pbuff_allocator = new ACE_Cached_Allocator<char,ACE_SYNCH_MUTEX>(10 * 1024);  <--方法1
或者
typedef char BUFFCOUNT[10*1024];
m_pbuff_allocator = new ACE_Cached_Allocator<BUFFCOUNT, ACE_SYNCH_MUTEX>(1);  <--方法2
替换。
但是实际上这样做是错误的,因为ACE_Cached_Allocator并不会像一些比较先进的开源池那样,自动粘合连续内存和自动压缩小块内存。
因为在构造的时候,代码会有如下动作。
  1.   for (size_t c = 0;
  2.        c < n_chunks;
  3.        c++)
  4.     {
  5.       void* placement = this->pool_ + c * chunk_size;
  6.       this->free_list_.add (new (placement) ACE_Cached_Mem_Pool_Node<T>);
  7.     }
复制代码

这也就是说,在内存池中,会按照T的大小生成等同于n_chunks的指向T对象指针的一个list。至于这样的分配,是为了在当用户申请大小小于或者等同于这个内存大小的时候,直接在list里面找到空余的T指针分配给它。
再来看看,在ACE_Cached_Allocator<T, ACE_LOCK>::malloc (size_t nbytes)里面,系统干了什么。
  1.   if (nbytes > sizeof (T))
  2.     return 0;
  3.   // addr() call is really not absolutely necessary because of the way
  4.   // ACE_Cached_Mem_Pool_Node's internal structure arranged.
  5.   return this->free_list_.remove ()->addr ();
复制代码

我感觉这里有一个陷阱,也就是说当Cache发现申请的内存块大于sizeof (T)的时候,return 0,它不会寻找连续的内存块粘合起来,返回给外面一个指向T*的指针。所以,当你申请内存块大于你的sizeof(T)上限,那么你将会从Cache得到一个NULL。虽然这样的做法,可以固定内存块的大小,但是如果程序中存在大量小于1024的内存申请,将会极大的浪费内存。因为,就算你要1个字节,也会占用一个1024字节(以本代码为例)的BUFFCOUNT[1024];
其实,我认为,在一定环境下,使用ACE_Cached_Allocator对象是非常合适的。比如,我需要申请一个对象池(比如一些Class),在频繁的申请和释放中,ACE_Cached_Allocator绝对是一个一流的解决方案,它可以省去很多不必要的new和delete,从而大幅提升程序的性能。
这里就要牵扯出ACE_Message_Block这个类了,这个类比较特殊,特殊在哪里?我觉得特殊在于它的ACE_Data_Block* db。
因为,在实际大量应用中,如果你能指定ACE_Message_Block的块大小,并不需要变化,ACE_Cached_Allocator绝对是不二的选择。
但是实际上,却会出现更多的情况,比如,ACE_Message_Block的申请大小是变化的,我可能需要1个字节的,也可能需要1000个字节的ACE_Message_Block。这里就出现了一个我认为ACE_Message_Block有待改进的地方。下面我会详述这个问题在哪里。(我觉得ACE的开发者也注意到了这个问题,以至于留了一行输出和注释,但是不知道为什么这么多的版本尚未改进这里。)
首先,我们来看看,ACE_Message_Block构造的时候,到底做了一些什么动作。
  1. ACE_Message_Block::ACE_Message_Block (size_t size,
  2.                                       ACE_Message_Type msg_type,
  3.                                       ACE_Message_Block *msg_cont,
  4.                                       const char *msg_data,
  5.                                       ACE_Allocator *allocator_strategy,
  6.                                       ACE_Lock *locking_strategy,
  7.                                       unsigned long priority,
  8.                                       const ACE_Time_Value &execution_time,
  9.                                       const ACE_Time_Value &deadline_time,
  10.                                       ACE_Allocator *data_block_allocator,
  11.                                       ACE_Allocator *message_block_allocator)
  12. {
  13.   if (this->init_i (size,
  14.                     msg_type,
  15.                     msg_cont,
  16.                     msg_data,
  17.                     allocator_strategy,
  18.                     locking_strategy,
  19.                     msg_data ? ACE_Message_Block:ONT_DELETE : 0,
  20.                     priority,
  21.                     execution_time,
  22.                     deadline_time,
  23.                     0, // data block
  24.                     data_block_allocator,
  25.                     message_block_allocator) == -1)
  26. }
复制代码
init_i()这个函数里面有很多复杂的语句和宏,其实剥离那些没用的,可以总结一个详细流程,流程如下。

  1. this->rd_ptr_ = 0;
  2.   this->wr_ptr_ = 0;
  3.   this->priority_ = priority;
  4. ....
  5.     if (db == 0)
  6.     {
  7.       if (data_block_allocator == 0)
  8.          ......
  9.      
  10.       //关键代码看这里
  11.       ACE_NEW_MALLOC_RETURN (db,
  12.                              static_cast<ACE_Data_Block *> (
  13.                                data_block_allocator->malloc (sizeof (ACE_Data_Block))),
  14.                              ACE_Data_Block (size,
  15.                                              msg_type,
  16.                                              msg_data,
  17.                                              allocator_strategy,
  18.                                              locking_strategy,
  19.                                              flags,
  20.                                              data_block_allocator),
  21.                              -1);   
复制代码
         

上面的代码不用说了,初始化一些变量,和读写指针位置。
下面的代码意思是,当ACE_Message_Block发现你指定了data_block_allocator分配器,它就会使用这个分配器,分配ACE_Data_Block* db对象中的char*内存块详细的地址位置。并赋值给ACE_Data_Block* db对象中的内存指针(char *base_,具体代码在ACE_Data_Block()的构造函数中)。
以上加黑的代码再进去,就是ACE_Data_Block对象的构造函数。
ACE_Data_Block的构造如下:(省去无用的代码)
  1. if (msg_data == 0)
  2.     {
  3.       ACE_ALLOCATOR (this->base_,
  4.                      (char *) this->allocator_strategy_->malloc (size));
  5.     }
复制代码

这里说明,ACE_Data_Block使用了分配器,分配了一块内存。并给予this->base_指针赋值。
这里,如果出现以下情况, (ACE_Cached_Allocator对象中的malloc()函数)
if (nbytes > sizeof (T))
    return 0;
也就是分配器中返回一个NULL,导致ACE_ALLOCATOR分配失败,这时候会怎样?
这时候程序会直接跳出构造,回到init_i函数中
ACE_Message_Block::init_i()函数中的
  1.       if (db != 0 && db->size () < size)
  2.         {
  3.           db->ACE_Data_Block::~ACE_Data_Block();  // placement destructor ...
  4.           data_block_allocator->free (db); // free ...
  5.           errno = ENOMEM;
  6.           return -1;
  7.         }
复制代码

这里要注意了,db是一个ACE_Data_Block*,这时候,ACE_Data_Block构造分配内存失败,但是此时此刻ACE_Data_Block指针是有效的,紧接着跟了一句
db->ACE_Data_Block::~ACE_Data_Block();  // placement destructor ...
好了,如果 db->size () < size,也就是内存没有分配出指定的大小,db会析构(这里为什么不用delete db或者free db?我怀疑可能是因为db的构造可以通过重载内存分配器获得,所以这里不能简单的使用以上的方法,必须再内存分配器的基础上重载)
这时候,执行完 db->ACE_Data_Block::~ACE_Data_Block();和data_block_allocator->free (db); 但是指针已经失效了,但是我不明白为什么ACE的开发者不跟一句 db = NULL;这就是我说的极易为以下几步的代码执行造成很大隐患的本质原因。因为此时,db如果不赋值为NULL会造成一个不折不扣的野指针。
好了,继续看下去。
return -1;
它返回了。
回到init_i()函数,这时候因为返回值是-1,所以会调用
  ACE_ERROR ((LM_ERROR, ACE_TEXT ("ACE_Message_Block")));
好了,此时此刻,我们看一下,内存里面到底有什么。
你成功的new了一个ACE_Message_Block()对象,一个指向ACE_Message_Block的指针,其中还包含一个指向ACE_Message_Block()->ACE_Data_Block()的野指针。
问题来了,一般的程序员,都喜欢这样的写法。

      
  1. pmb = new ACE_Message_Block(u4Size, ACE_Message_Block::MB_DATA, 0, 0, m_pbuff_allocator, 0, ACE_DEFAULT_MESSAGE_BLOCK_PRIORITY, ACE_Time_Value::zero, ACE_Time_Value::max_time, m_pdata_allocator, m_pmsgallocator);
  2.         if(pmb != NULL)
  3.         {
  4.                    .....
  5.         }
复制代码

此时此刻,实际上pmb生成是失败的,但是你却获得了一个指向ACE_Message_Block的指针,同时,更要命的是,你还获得了一个ACE_Message_Block指向ACE_Data_Block()的野指针。
好了,如果你继续在后面追加一个pmb->release();
行了,你就等着看内存崩溃的画面吧。
因为release()会释放这个内部指针db,然而,db已经是不存在的了。
我觉得,这里未必是ACE的BUG,或许,在某些编写习惯中,会导致自己的代码莫名其妙的崩溃。不过我觉得,这里确实很有一个ACE陷阱的疑点。
所以,综上所述,我个人觉得,对于ACE_Message_Block对象,使用ACE_Cached_Allocator是有风险的。尤其是在使用不定大小的ACE_Message_Block大小时,这个定时炸弹会被放大,甚至有机会爆炸。
所以,我个人而言,不支持在使用不定大小的Message_Block内存池中,使用ACE_Cached_Allocator,而我更倾向于使用
ACE_Malloc<ACE_LOCAL_MEMORY_POOL, ACE_SYNCH_MUTEX>
好了,说了这么多,我说说为什么ACE_Malloc感觉会比ACE_Cached_Allocator好。
ACE_Malloc是一个复杂的模板(至今都没有完全看懂它的嵌套方法,模板学的不深,不过大致意思还算明白),他会根据ACE_LOCAL_MEMORY_POOL去寻找ACE_Local_Memory_Pool这个类去初始化内存池,然后会调用ACE_Malloc_T里面的去实现malloc方法,如果内存不够了,会调用ACE_Local_Memory_Poo的acquire函数再去构造新的内存。
那么,如果使用
  1. //定义一个内存管理分配器
  2. typedef  ACE_Malloc<ACE_LOCAL_MEMORY_POOL, ACE_SYNCH_MUTEX> MUTEX_MALLOC;
  3. typedef ACE_Allocator_Adapter<MUTEX_MALLOC> Mutex_Allocator;
  4. Mutex_Allocator _msg_mb_allocator;
复制代码
在使用内存池的时候,前面都一样,只是在malloc的时候,会调用(malloc_t.cpp)
template <ACE_MEM_POOL_1, class ACE_LOCK, class ACE_CB> void *
ACE_Malloc_T<ACE_MEM_POOL_2, ACE_LOCK, ACE_CB>::shared_malloc (size_t nbytes)
这里nbytes就是你要申请的内存大小。
这里,牵扯到两个对象
  MALLOC_HEADER *prevp = 0;
  MALLOC_HEADER *currp = 0;
MALLOC_HEADER是什么呢?第一眼看下去,我的直觉是一个内存块穿起来的链表。
typedef typename ACE_CB::ACE_Malloc_Header MALLOC_HEADER;
于是去找ACE_Malloc_Header 这个类在干什么。查找出它的代码位于malloc.*里面,仔细的看了一下,证明了我的判断。
好了,回来看继续看这个函数
while (1)
这里面的遍历,实际是最关键的代码。实现了内存块在链表中的流动。

  1. ACE_SEH_TRY
  2.     {
  3.       // Begin the search starting at the place in the freelist where the
  4.       // last block was found.
  5.       prevp = this->cb_ptr_->freep_;
  6.       currp = prevp->next_block_;
  7.     }
复制代码

看来链表和内存池的类型是完全分离的,这样设计的好处是,内存分配机制和内存块遍历机制完全隔离。
while(1)里面的代码看似很复杂,实际只要耐心,还是能看懂他们在干什么的。
size_t const nunits = (nbytes + sizeof (MALLOC_HEADER) - 1) / sizeof (MALLOC_HEADER) + 1;
有点类似于桶+内存链的模式。
大致意思就是,先在已经分配的内存自由链中,寻找大于等于申请内存大小的空间,如果不存在则调用ACE_Local_Memory_Pool类的相应方法创造出内存块并赋值入自由链表中。
针对不同大小的内存块,如果链表过长,遍历起来会影响运行速度,这里ACE开发者显然想到了,并做了优化。
所以我个人认为,用一个或者三个ACE_Malloc()对象,其实并不是有很大差距。
为了验证我的这个想法。稍微修改了一下我的代码

        m_pmsgallocator   = new Mutex_Allocator();
        m_pdata_allocator = new Mutex_Allocator();
        m_pbuff_allocator = new Mutex_Allocator();

测试结果
[10000 Cost]10000 size = 10 Cost time is 31.
而同理的cache是
[10000 Cost]10000 size = 10 Cost time is 15.
看来cache还是快些的。
不过,我之所以使用ACE_Malloc<ACE_LOCAL_MEMORY_POOL, ACE_SYNCH_MUTEX>为的就是尽量避开我上面所述的陷阱。Cache有两个问题点,第一个是申请的单块内存必须小于规定的大小,第二,当链表用尽,同样会引发炸弹,而不会cache重新分配。而ACE_Malloc<ACE_LOCAL_MEMORY_POOL, ACE_SYNCH_MUTEX>的动态内存管理刚好弥补了这一块,如果内存第一次申请的过大,找不到会new,如果自由链表用尽,会从内存池再去创建。如果内存池创建失败了,那么同样会引起这个问题,但是这样的几率是非常小的,相对cache而言。
以上的测试,希望能抛砖引玉,借着这个机会把ace的内存机制和Message_block行为分析透彻一些。

[ 本帖最后由 freeeyes 于 2010-7-2 15:38 编辑 ]
发表于 2010-7-5 16:02:01 | 显示全部楼层
答复freeeyes
周五请假,周末一直好忙,抱歉今天才看到你的帖子。
刚看你的答复的时候,确实有点晕,好长,一定写得很辛苦,不过我还是坚持看完了。
首先,对于ACE_Cached_Allocator与ACE_Message_block的分析是比较中肯的,你提到的很多问题我之前也注意到了。
1.ACE_Cached_Allocator设计即定长的内存池,这在其class注释的开头就已经明确指出了,
定长内存池肯定有其功能上的不足,但是好处在于实现简单,对于特定应用来说实现简单反而会有更好的性能。
因此关于申请超过尺寸的内存而产生的一些列的问题,我认为这不是ACE的问题,
毕竟人家写的很清楚了,我的内存池就支持这样的一个工作集嘛。

2.由于链表的节点耗尽导致内存分配失败的情况,进而进入你提及的ACE_Message_Block逻辑,倒是更让让我十分担心的。
ACE_Cached_Allocator持有的free_list_默认使用的是ACE_PURE_FREE_LIST模式,
即如果内存节点耗尽,则返回空指针,在你说的情况下是非常恐怖的。
另free_list_可以使用考虑ACE_FREE_LIST_WITH_POOL模式进行初始化,
在剩余节点达到低水平位的时候,再分配指定大小数量的内存块出来。
不过极端情况下,如果这么一大坨内存分配不出来,结果就杯具了。
这种情况在服务器整体架构设计的不好的时候,必定会出现,但这也不能算ACE的错。

3.关于内存空间的浪费问题,由于我当时主要修复的是在进行网络数据收发频繁的内存分配问题,
我到不觉得这是太大的问题,因为正常的情况下,只要服务器的处理线程不是死锁
或者阻塞在某些调用上导致大量的排队,处理完一条情况前面申请的内存会很快被回收的,
如果出现了这种情况,大都是架构设计的问题,是另一个层面需要解决的事情了,
即使不引人内存池,如果出现这样的情况,服务器也早晚会死,死的早点反而会让问题早暴露早解决。
解决特定的应用刚刚好~

4.顺便提一下,印象里ACE_Message_Block与ACE_Data_Block大小竟然足足40个字节,如果不使用内存池,每收发一个数据包,
都要额外的分配与释放这些内存,在并发量上去之后,是相当杯具的。

其次,关于ACE_Malloc_T,我之前经常使用结合ACE_MMAP_MEMORY_POOL的进程间共享内存,平台移植性还算不错
到没有太注意使用ACE_LOCAL_MEMORY_POOL的内存池应用,对这套体系倒是蛮熟,
以前只是孤立的看ACE_LOCAL_MEMORY_POOL的源代码,没太搞懂其意图。
刚才大致结合ACE_Malloc_T的share_malloc函数看一下ACE_LOCAL_MEMORY_POOL的源代码才恍然大悟,
真正核心的东西原来在share_malloc这里。

我的观点是具体的性能肯定是ACE_Cached_Allocator会更优一些,毕竟只做了一次简单的链表遍历。
而ACE_LOCAL_MEMORY_POOL的伸缩性会更好一些,至于是否会带来额外的问题有时间仔细看一下代码,对比测试一下。
因此对于你提的第一点问题,我认为并不是ACE的错,两种内存池的应用场景不一致罢了,需要根据自己的需求做取舍。
第二点问题,ACE_Cached_Allocator也是可以动态扩展的,至于内存耗尽之后申请不到大坨的内存,
两者都无法避免,但这也不是ACE的错,主要还是看具体架构设计。

题外话,显然想仅通过上一个内存池就把所有服务器问题都解决肯定是不可能的,
最近已经在主代码分支里一定程度的引入了boost,确实让生活变得好过了一些。
有的时候我也比较羡慕人家原生提供GC的语言,但是现状如此,貌似是否引入GC,C++0x也在吵。
我认为用ACE的内存池至少解决特定问题还是不错的。
对于如何在服务器提供有效的全局的内存与资源管理,以及任务调度也是我最近要解决的问题,
希望共同探讨一下,开阔一下思路。

最后,对于ACE代码里的总总注释已经想到了问题甚至解决方案,但是长期没有解决,
我们也分有析过,由于ACE是D博士与他的一些朋友带着他的一堆学生搞的,
所以很多人毕业了也就不再管了,因此代码质量也是一阵一阵的,
搞笑的是好像是08年吧,ACE的svn目录被D博士的当时一学生,全都给清了。

[ 本帖最后由 modern 于 2010-7-5 16:26 编辑 ]
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|手机版|小黑屋|ACE Developer ( 京ICP备06055248号 )

GMT+8, 2024-4-29 11:16 , Processed in 0.016991 second(s), 6 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表