Modern C++ Design 笔记 第四章 Small-Object Allocation

在频繁的new/delete的时候容易衍生memory fragment, 我想设计这个Allocator的初衷就在这里。

初看到这个题目的时候不由的让人联想到的东西是std::allocator.

 

所以我们就从MS的STL中看看std::allocator的实现。

  1. template<class _Ty> inline
  2.     _Ty _FARQ *_Allocate(_SIZT _Count, _Ty _FARQ *)
  3.     {   // check for integer overflow
  4.     if (_Count <= 0)
  5.         _Count = 0;
  6.     else if (((_SIZT)(-1) / _Count) < sizeof (_Ty))
  7.         _THROW_NCEE(std::bad_alloc, NULL);

  8.         // allocate storage for _Count elements of type _Ty
  9.     return ((_Ty _FARQ *)::operator new(_Count * sizeof (_Ty)));
  10.     }

  11. void deallocate(pointer _Ptr, size_type)
  12.     {   // deallocate object at _Ptr, ignore size
  13.         ::operator delete(_Ptr);
  14.     }

  15. pointer allocate(size_type _Count)
  16.     {   // allocate array of _Count elements
  17.         return (_Allocate(_Count, (pointer)0));
  18.     }

可以看到这里遵循了STL的标准但是做到的就是把allocator和deallocate做了new/delete最基本的wrapper.

随后由于受到侯捷的<<STL源码剖析>>的“毒害”:),自然要看看SGI STL的策略的。SGI确实还是要高级不少的。具体的实现从__default_alloc_template这个类就可以看出一点端倪。


  1.   /* __n must be > 0      */
  2.   static void* allocate(size_t __n)
  3.   {
  4.     void* __ret = 0;

  5.     if (__n > (size_t) _MAX_BYTES) {
  6.       __ret = malloc_alloc::allocate(__n);
  7.     }
  8.     else {
  9.       _Obj* __STL_VOLATILE* __my_free_list
  10.           = _S_free_list + _S_freelist_index(__n);
  11.       // Acquire the lock here with a constructor call.
  12.       // This ensures that it is released in exit or during stack
  13.       // unwinding.
  14. #     ifndef _NOTHREADS
  15.       /*REFERENCED*/
  16.       _Lock __lock_instance;
  17. #     endif
  18.       _Obj* __RESTRICT __result = *__my_free_list;
  19.       if (__result == 0)
  20.         __ret = _S_refill(_S_round_up(__n));
  21.       else {
  22.         *__my_free_list = __result -> _M_free_list_link;
  23.         __ret = __result;
  24.       }
  25.     }

  26.     return __ret;
  27.   };

  28.   /* __p may not be 0 */
  29.   static void deallocate(void* __p, size_t __n)
  30.   {
  31.     if (__n > (size_t) _MAX_BYTES)
  32.       malloc_alloc::deallocate(__p, __n);
  33.     else {
  34.       _Obj* __STL_VOLATILE*  __my_free_list
  35.           = _S_free_list + _S_freelist_index(__n);
  36.       _Obj* __q = (_Obj*)__p;

  37.       // acquire lock
  38. #       ifndef _NOTHREADS
  39.       /*REFERENCED*/
  40.       _Lock __lock_instance;
  41. #       endif /* _NOTHREADS */
  42.       __q -> _M_free_list_link = *__my_free_list;
  43.       *__my_free_list = __q;
  44.       // lock is released here
  45.     }
  46.   }
这里虽然仅仅贴出了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的实现。


  1. // SmallObjAllocator::Allocate ------------------------------------------------

  2. void * SmallObjAllocator::Allocate( std::size_t numBytes, bool doThrow )
  3. {
  4.     if ( numBytes > GetMaxObjectSize() )
  5.         return DefaultAllocator( numBytes, doThrow );

  6.     assert( NULL != pool_ );
  7.     if ( 0 == numBytes ) numBytes = 1;
  8.     const std::size_t index = GetOffset( numBytes, GetAlignment() ) - 1;
  9.     const std::size_t allocCount = GetOffset( GetMaxObjectSize(), GetAlignment() );
  10.     (void) allocCount;
  11.     assert( index < allocCount );

  12.     FixedAllocator & allocator = pool_[ index ];
  13.     assert( allocator.BlockSize() >= numBytes );
  14.     assert( allocator.BlockSize() < numBytes + GetAlignment() );
  15.     void * place = allocator.Allocate();

  16.     if ( ( NULL == place ) && TrimExcessMemory() )
  17.         place = allocator.Allocate();

  18.     if ( ( NULL == place ) && doThrow )
  19.     {
  20. #ifdef _MSC_VER
  21.         throw std::bad_alloc( "could not allocate small object" );
  22. #else
  23.         // GCC did not like a literal string passed to std::bad_alloc.
  24.         // so just throw the default-constructed exception.
  25.         throw std::bad_alloc();
  26. #endif
  27.     }
  28.     return place;
  29. }

  30. // SmallObjAllocator::Deallocate ----------------------------------------------

  31. void SmallObjAllocator::Deallocate( void * p, std::size_t numBytes )
  32. {
  33.     if ( NULL == p ) return;
  34.     if ( numBytes > GetMaxObjectSize() )
  35.     {
  36.         DefaultDeallocator( p );
  37.         return;
  38.     }
  39.     assert( NULL != pool_ );
  40.     if ( 0 == numBytes ) numBytes = 1;
  41.     const std::size_t index = GetOffset( numBytes, GetAlignment() ) - 1;
  42.     const std::size_t allocCount = GetOffset( GetMaxObjectSize(), GetAlignment() );
  43.     (void) allocCount;
  44.     assert( index < allocCount );
  45.     FixedAllocator & allocator = pool_[ index ];
  46.     assert( allocator.BlockSize() >= numBytes );
  47.     assert( allocator.BlockSize() < numBytes + GetAlignment() );
  48.     const bool found = allocator.Deallocate( p, NULL );
  49.     (void) found;
  50.     assert( found );
  51. }
首先可以看到的是这里的分配都是依靠 pool_来实现的, 这里的 pool_最初的定义如书中的描述是std::vector<FixedAllocator> pool_;但是现在已经退化成了 FixedAllocator* pool_;其实从现在的实现来看却是没有vector的必要。能够发现的是,同样2种实现都可以到达常数时间找到对应allocator。(其实这点很重,在SGI的freelist也有类似的接口做到了这一点)。找到之后的分配就完全靠他了。

同时我们可以看看这个pool的实例化的过程:

  1. SmallObjAllocator::SmallObjAllocator( std::size_t pageSize,
  2.     std::size_t maxObjectSize, std::size_t objectAlignSize ) :
  3.     pool_( NULL ),
  4.     maxSmallObjectSize_( maxObjectSize ),
  5.     objectAlignSize_( objectAlignSize )
  6. {
  7. #ifdef DO_EXTRA_LOKI_TESTS
  8.     std::cout << "SmallObjAllocator " << this << std::endl;
  9. #endif
  10.     assert( 0 != objectAlignSize );
  11.     const std::size_t allocCount = GetOffset( maxObjectSize, objectAlignSize );
  12.     pool_ = new FixedAllocator[ allocCount ];
  13.     for ( std::size_t i = 0; i < allocCount; ++i )
  14.         pool_[ i ].Initialize( ( i+1 ) * objectAlignSize, pageSize );
  15. }

这里的Initialize并不会把实际的内存都分配好,而只是设定下alignment, 和maxSize, 这里的maxSize就像SGI STL中的那个128, 就是我这个small-object allocator会分配的内存大小最多就是128bytes.当然这里的分配器就比SGI STL稍稍灵活了一点。


总结一点,这里的SmallObjAllocator同样地 ,首先要看需要分配的内存的大小有多大, 确定下来size之后常数时间去pool_[ i ]中,实际上就是去FixedAllocator去获取。可以看到的是和SGI STL的模型非常类似:)现在我们来看看FixedAllocator是如何来操作具体的allocate/deallocate的。

  1. // FixedAllocator::Allocate ---------------------------------------------------

  2. void * FixedAllocator::Allocate( void )
  3. {
  4.     // prove either emptyChunk_ points nowhere, or points to a truly empty Chunk.
  5.     assert( ( NULL == emptyChunk_ ) || ( emptyChunk_->HasAvailable( numBlocks_ ) ) );
  6.     assert( CountEmptyChunks() < 2 );

  7.     if ( ( NULL == allocChunk_ ) || allocChunk_->IsFilled() )
  8.     {
  9.         if ( NULL != emptyChunk_ )
  10.         {
  11.             allocChunk_ = emptyChunk_;
  12.             emptyChunk_ = NULL;
  13.         }
  14.         else
  15.         {
  16.             for ( ChunkIter i( chunks_.begin() ); ; ++i )
  17.             {
  18.                 if ( chunks_.end() == i )
  19.                 {
  20.                     if ( !MakeNewChunk() )
  21.                         return NULL;
  22.                     break;
  23.                 }
  24.                 if ( !i->IsFilled() )
  25.                 {
  26.                     allocChunk_ = &*i;
  27.                     break;
  28.                 }
  29.             }
  30.         }
  31.     }
  32.     else if ( allocChunk_ == emptyChunk_)
  33.         // detach emptyChunk_ from allocChunk_, because after 
  34.         // calling allocChunk_->Allocate(blockSize_); the chunk 
  35.         // is no longer empty.
  36.         emptyChunk_ = NULL;

  37.     assert( allocChunk_ != NULL );
  38.     assert( !allocChunk_->IsFilled() );
  39.     void * place = allocChunk_->Allocate( blockSize_ );

  40.     // prove either emptyChunk_ points nowhere, or points to a truly empty Chunk.
  41.     assert( ( NULL == emptyChunk_ ) || ( emptyChunk_->HasAvailable( numBlocks_ ) ) );
  42.     assert( CountEmptyChunks() < 2 );

  43.     return place;
  44. }

  45. // FixedAllocator::Deallocate -------------------------------------------------

  46. bool FixedAllocator::Deallocate( void * p, Chunk * hint )
  47. {
  48.     assert(!chunks_.empty());
  49.     assert(&chunks_.front() <= deallocChunk_);
  50.     assert(&chunks_.back() >= deallocChunk_);
  51.     assert( &chunks_.front() <= allocChunk_ );
  52.     assert( &chunks_.back() >= allocChunk_ );
  53.     assert( CountEmptyChunks() < 2 );

  54.     Chunk * foundChunk = ( NULL == hint ) ? VicinityFind( p ) : hint;
  55.     if ( NULL == foundChunk )
  56.         return false;

  57.     assert( foundChunk->HasBlock( p, numBlocks_ * blockSize_ ) );
  58. #ifdef LOKI_CHECK_FOR_CORRUPTION
  59.     if ( foundChunk->IsCorrupt( numBlocks_, blockSize_, true ) )
  60.     {
  61.         assert( false );
  62.         return false;
  63.     }
  64.     if ( foundChunk->IsBlockAvailable( p, numBlocks_, blockSize_ ) )
  65.     {
  66.         assert( false );
  67.         return false;
  68.     }
  69. #endif
  70.     deallocChunk_ = foundChunk;
  71.     DoDeallocate(p);
  72.     assert( CountEmptyChunks() < 2 );

  73.     return true;
  74. }
进一步这里可以看到很多所谓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)来标记分配的情况。还是看图最直观:

Modern C++ Design 笔记 第四章 Small-Object Allocation_第1张图片

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









你可能感兴趣的:(Modern C++ Design 笔记 第四章 Small-Object Allocation)