在频繁的new/delete的时候容易衍生memory fragment, 我想设计这个Allocator的初衷就在这里。
初看到这个题目的时候不由的让人联想到的东西是std::allocator.
所以我们就从MS的STL中看看std::allocator的实现。
- template<class _Ty> inline
- _Ty _FARQ *_Allocate(_SIZT _Count, _Ty _FARQ *)
- {
- if (_Count <= 0)
- _Count = 0;
- else if (((_SIZT)(-1) / _Count) < sizeof (_Ty))
- _THROW_NCEE(std::bad_alloc, NULL);
-
- return ((_Ty _FARQ *)::operator new(_Count * sizeof (_Ty)));
- }
- void deallocate(pointer _Ptr, size_type)
- {
- ::operator delete(_Ptr);
- }
- pointer allocate(size_type _Count)
- {
- return (_Allocate(_Count, (pointer)0));
- }
可以看到这里遵循了STL的标准但是做到的就是把allocator和deallocate做了new/delete最基本的wrapper.
随后由于受到侯捷的<<STL源码剖析>>的“毒害”:),自然要看看SGI STL的策略的。SGI确实还是要高级不少的。具体的实现从__default_alloc_template这个类就可以看出一点端倪。
-
- static void* allocate(size_t __n)
- {
- void* __ret = 0;
- if (__n > (size_t) _MAX_BYTES) {
- __ret = malloc_alloc::allocate(__n);
- }
- else {
- _Obj* __STL_VOLATILE* __my_free_list
- = _S_free_list + _S_freelist_index(__n);
-
-
-
- # ifndef _NOTHREADS
-
- _Lock __lock_instance;
- # endif
- _Obj* __RESTRICT __result = *__my_free_list;
- if (__result == 0)
- __ret = _S_refill(_S_round_up(__n));
- else {
- *__my_free_list = __result -> _M_free_list_link;
- __ret = __result;
- }
- }
- return __ret;
- };
-
- static void deallocate(void* __p, size_t __n)
- {
- if (__n > (size_t) _MAX_BYTES)
- malloc_alloc::deallocate(__p, __n);
- else {
- _Obj* __STL_VOLATILE* __my_free_list
- = _S_free_list + _S_freelist_index(__n);
- _Obj* __q = (_Obj*)__p;
-
- # ifndef _NOTHREADS
-
- _Lock __lock_instance;
- # endif /* _NOTHREADS */
- __q -> _M_free_list_link = *__my_free_list;
- *__my_free_list = __q;
-
- }
- }
这里虽然仅仅贴出了allocator和deallocator的代码,但是也可以看出一点端倪了。首先可以肯定的是这里不再简单的是new/delete了, 这里加入了一些特殊的逻辑来管理内存,大致上就是FreeListLink辅之以一个memeory pool.每次的特定大小的内存分配都可以到FreeList对应的node里面去找。这个list的node对应的就是小于等于128byte的各个8倍数。实际上就是8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128。如果list node中存在就直接拿好了, 如果没有就
_S_refill一下,这里
_S_refill的实现大致上就是从Memory Pool中去读取了。这里就不展开了:)
说了这么多废话,现在回到这个我们的Loki lib中的small-object allocator,这个allocator的特点在什么地方呢。中国人看问题的方式可能和老外不同,至少和这本书介绍的方式不同,我们喜欢所谓透过现象看本质的流程。在层次上这里的模型大概分成Chunk, FixedAllocator, SmallObjAllocator和SmallObject。 在最上层的使用者就是SmallObject,让我们撇开复杂的模型中和内存策略无关的singleton包装,直接看到SmallObjAllocator中allocate和deallocate的实现。
- void * SmallObjAllocator::Allocate( std::size_t numBytes, bool doThrow )
- {
- if ( numBytes > GetMaxObjectSize() )
- return DefaultAllocator( numBytes, doThrow );
- assert( NULL != pool_ );
- if ( 0 == numBytes ) numBytes = 1;
- const std::size_t index = GetOffset( numBytes, GetAlignment() ) - 1;
- const std::size_t allocCount = GetOffset( GetMaxObjectSize(), GetAlignment() );
- (void) allocCount;
- assert( index < allocCount );
- FixedAllocator & allocator = pool_[ index ];
- assert( allocator.BlockSize() >= numBytes );
- assert( allocator.BlockSize() < numBytes + GetAlignment() );
- void * place = allocator.Allocate();
- if ( ( NULL == place ) && TrimExcessMemory() )
- place = allocator.Allocate();
- if ( ( NULL == place ) && doThrow )
- {
- #ifdef _MSC_VER
- throw std::bad_alloc( "could not allocate small object" );
- #else
-
-
- throw std::bad_alloc();
- #endif
- }
- return place;
- }
- void SmallObjAllocator::Deallocate( void * p, std::size_t numBytes )
- {
- if ( NULL == p ) return;
- if ( numBytes > GetMaxObjectSize() )
- {
- DefaultDeallocator( p );
- return;
- }
- assert( NULL != pool_ );
- if ( 0 == numBytes ) numBytes = 1;
- const std::size_t index = GetOffset( numBytes, GetAlignment() ) - 1;
- const std::size_t allocCount = GetOffset( GetMaxObjectSize(), GetAlignment() );
- (void) allocCount;
- assert( index < allocCount );
- FixedAllocator & allocator = pool_[ index ];
- assert( allocator.BlockSize() >= numBytes );
- assert( allocator.BlockSize() < numBytes + GetAlignment() );
- const bool found = allocator.Deallocate( p, NULL );
- (void) found;
- assert( found );
- }
首先可以看到的是这里的分配都是依靠
pool_来实现的, 这里的
pool_最初的定义如书中的描述是std::vector<FixedAllocator> pool_;但是现在已经退化成了
FixedAllocator* pool_;其实从现在的实现来看却是没有vector的必要。能够发现的是,同样2种实现都可以到达常数时间找到对应allocator。(其实这点很重,在SGI的freelist也有类似的接口做到了这一点)。找到之后的分配就完全靠他了。
同时我们可以看看这个pool的实例化的过程:
- SmallObjAllocator::SmallObjAllocator( std::size_t pageSize,
- std::size_t maxObjectSize, std::size_t objectAlignSize ) :
- pool_( NULL ),
- maxSmallObjectSize_( maxObjectSize ),
- objectAlignSize_( objectAlignSize )
- {
- #ifdef DO_EXTRA_LOKI_TESTS
- std::cout << "SmallObjAllocator " << this << std::endl;
- #endif
- assert( 0 != objectAlignSize );
- const std::size_t allocCount = GetOffset( maxObjectSize, objectAlignSize );
- pool_ = new FixedAllocator[ allocCount ];
- for ( std::size_t i = 0; i < allocCount; ++i )
- pool_[ i ].Initialize( ( i+1 ) * objectAlignSize, pageSize );
- }
这里的Initialize并不会把实际的内存都分配好,而只是设定下alignment, 和maxSize, 这里的maxSize就像SGI STL中的那个128, 就是我这个small-object allocator会分配的内存大小最多就是128bytes.当然这里的分配器就比SGI STL稍稍灵活了一点。
总结一点,这里的SmallObjAllocator同样地 ,首先要看需要分配的内存的大小有多大, 确定下来size之后常数时间去pool_[ i ]中,实际上就是去FixedAllocator去获取。可以看到的是和SGI STL的模型非常类似:)现在我们来看看FixedAllocator是如何来操作具体的allocate/deallocate的。
- void * FixedAllocator::Allocate( void )
- {
-
- assert( ( NULL == emptyChunk_ ) || ( emptyChunk_->HasAvailable( numBlocks_ ) ) );
- assert( CountEmptyChunks() < 2 );
- if ( ( NULL == allocChunk_ ) || allocChunk_->IsFilled() )
- {
- if ( NULL != emptyChunk_ )
- {
- allocChunk_ = emptyChunk_;
- emptyChunk_ = NULL;
- }
- else
- {
- for ( ChunkIter i( chunks_.begin() ); ; ++i )
- {
- if ( chunks_.end() == i )
- {
- if ( !MakeNewChunk() )
- return NULL;
- break;
- }
- if ( !i->IsFilled() )
- {
- allocChunk_ = &*i;
- break;
- }
- }
- }
- }
- else if ( allocChunk_ == emptyChunk_)
-
-
-
- emptyChunk_ = NULL;
- assert( allocChunk_ != NULL );
- assert( !allocChunk_->IsFilled() );
- void * place = allocChunk_->Allocate( blockSize_ );
-
- assert( ( NULL == emptyChunk_ ) || ( emptyChunk_->HasAvailable( numBlocks_ ) ) );
- assert( CountEmptyChunks() < 2 );
- return place;
- }
- bool FixedAllocator::Deallocate( void * p, Chunk * hint )
- {
- assert(!chunks_.empty());
- assert(&chunks_.front() <= deallocChunk_);
- assert(&chunks_.back() >= deallocChunk_);
- assert( &chunks_.front() <= allocChunk_ );
- assert( &chunks_.back() >= allocChunk_ );
- assert( CountEmptyChunks() < 2 );
- Chunk * foundChunk = ( NULL == hint ) ? VicinityFind( p ) : hint;
- if ( NULL == foundChunk )
- return false;
- assert( foundChunk->HasBlock( p, numBlocks_ * blockSize_ ) );
- #ifdef LOKI_CHECK_FOR_CORRUPTION
- if ( foundChunk->IsCorrupt( numBlocks_, blockSize_, true ) )
- {
- assert( false );
- return false;
- }
- if ( foundChunk->IsBlockAvailable( p, numBlocks_, blockSize_ ) )
- {
- assert( false );
- return false;
- }
- #endif
- deallocChunk_ = foundChunk;
- DoDeallocate(p);
- assert( CountEmptyChunks() < 2 );
- return true;
- }
进一步这里可以看到很多所谓Chunk的身影了,这里具体的alloc的任务就是Chunk来完成的,不同于
FixedAllocator的是这里的chunks_被定义成了std::vector< Chunk >,因为这里的长度就会发生变化了。因为实际上一个确定了blockSize的Chunk的实际大小是固定的,所以就需要很多个Chunk来完成特定量的分配。 (但是数组是不在这个讨论范围内的)。比如说你需要257个8个byte的内存块, 问题来了Chunk的特性决定了他的块的数量最大是256,所以,只能能有2个Chunk来完成,也就是这里有个Chunk数组的原因。
这里的Chunk的实现类似月SGI STL中的Union OBJ,同样的为了避免额外的标记内存的指针或者数字,SGI STL中采取了Union的方式来共用内存,而Chunk则直接使用了分配内存中的一个byte(或者说一个char)来标记分配的情况。还是看图最直观:

Chunk中维护了一段内存,pData_一个index和一个count,使用这些东西就可以满足要求了。很显然的是每次分配一个block,就把firstAvailableBlock_++,然后blocksAvailable_--,firstAvailableBlock_加上pData就可以常数时间找到空闲的内存了。又是常数时间, 呵呵。而且这些内存不会有额外的浪费分配。现在的体会只有用impressive可以形容啦。