|
楼主 |
发表于 2010-7-2 13:25:17
|
显示全部楼层
To modern:
最近网站不是很稳定,很多文章写了一半就白费了,我很想和你探讨一下ACE_Cached_Allocator这个模板的本质用法,呵呵,期待你的回复。
我以前看过ACE_Cached_Allocator的一些说明,在实际操作中,我发现ACE_Cached_Allocator似乎很容易引起Message_Block的崩溃。最近研究了这个几天,并和ACE_Malloc<ACE_LOCAL_MEMORY_POOL, ACE_SYNCH_MUTEX>做了一下比较。
ACE_Cached_Allocator模板在初始化的时候,需要两个参数。
- typedef char BUFFCOUNT[1024];
- 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)中,如此定义的。
- size_t chunk_size = sizeof (T);
- chunk_size = ACE_MALLOC_ROUNDUP (chunk_size, ACE_MALLOC_ALIGN);
- ACE_NEW (this->pool_,
- 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并不会像一些比较先进的开源池那样,自动粘合连续内存和自动压缩小块内存。
因为在构造的时候,代码会有如下动作。
- for (size_t c = 0;
- c < n_chunks;
- c++)
- {
- void* placement = this->pool_ + c * chunk_size;
- this->free_list_.add (new (placement) ACE_Cached_Mem_Pool_Node<T>);
- }
复制代码
这也就是说,在内存池中,会按照T的大小生成等同于n_chunks的指向T对象指针的一个list。至于这样的分配,是为了在当用户申请大小小于或者等同于这个内存大小的时候,直接在list里面找到空余的T指针分配给它。
再来看看,在ACE_Cached_Allocator<T, ACE_LOCK>::malloc (size_t nbytes)里面,系统干了什么。
- if (nbytes > sizeof (T))
- return 0;
- // addr() call is really not absolutely necessary because of the way
- // ACE_Cached_Mem_Pool_Node's internal structure arranged.
- 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构造的时候,到底做了一些什么动作。
- ACE_Message_Block::ACE_Message_Block (size_t size,
- ACE_Message_Type msg_type,
- ACE_Message_Block *msg_cont,
- const char *msg_data,
- ACE_Allocator *allocator_strategy,
- ACE_Lock *locking_strategy,
- unsigned long priority,
- const ACE_Time_Value &execution_time,
- const ACE_Time_Value &deadline_time,
- ACE_Allocator *data_block_allocator,
- ACE_Allocator *message_block_allocator)
- {
- if (this->init_i (size,
- msg_type,
- msg_cont,
- msg_data,
- allocator_strategy,
- locking_strategy,
- msg_data ? ACE_Message_Block:ONT_DELETE : 0,
- priority,
- execution_time,
- deadline_time,
- 0, // data block
- data_block_allocator,
- message_block_allocator) == -1)
- }
复制代码 init_i()这个函数里面有很多复杂的语句和宏,其实剥离那些没用的,可以总结一个详细流程,流程如下。
- this->rd_ptr_ = 0;
- this->wr_ptr_ = 0;
- this->priority_ = priority;
- ....
- if (db == 0)
- {
- if (data_block_allocator == 0)
- ......
-
- //关键代码看这里
- ACE_NEW_MALLOC_RETURN (db,
- static_cast<ACE_Data_Block *> (
- data_block_allocator->malloc (sizeof (ACE_Data_Block))),
- ACE_Data_Block (size,
- msg_type,
- msg_data,
- allocator_strategy,
- locking_strategy,
- flags,
- data_block_allocator),
- -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的构造如下:(省去无用的代码)
- if (msg_data == 0)
- {
- ACE_ALLOCATOR (this->base_,
- (char *) this->allocator_strategy_->malloc (size));
- }
复制代码
这里说明,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()函数中的
- if (db != 0 && db->size () < size)
- {
- db->ACE_Data_Block::~ACE_Data_Block(); // placement destructor ...
- data_block_allocator->free (db); // free ...
- errno = ENOMEM;
- return -1;
- }
复制代码
这里要注意了,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()的野指针。
问题来了,一般的程序员,都喜欢这样的写法。
- 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);
- if(pmb != NULL)
- {
- .....
- }
复制代码
此时此刻,实际上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函数再去构造新的内存。
那么,如果使用
- //定义一个内存管理分配器
- typedef ACE_Malloc<ACE_LOCAL_MEMORY_POOL, ACE_SYNCH_MUTEX> MUTEX_MALLOC;
- typedef ACE_Allocator_Adapter<MUTEX_MALLOC> Mutex_Allocator;
- 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)
这里面的遍历,实际是最关键的代码。实现了内存块在链表中的流动。
- ACE_SEH_TRY
- {
- // Begin the search starting at the place in the freelist where the
- // last block was found.
- prevp = this->cb_ptr_->freep_;
- currp = prevp->next_block_;
- }
复制代码
看来链表和内存池的类型是完全分离的,这样设计的好处是,内存分配机制和内存块遍历机制完全隔离。
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 编辑 ] |
|