STL之空間配置器(Allocator)


本文以gcc_9.3.0爲例

一、介紹

配置器的基本目的是爲給定類型提供內存資源以及提供在內存不再需要時將其歸還的地方。標準庫的內存管理被封裝在一個類模板中——allocator。

1.1、設計

C++標準對於該領域只是給出了一些指示,實現這些要求的最簡單的方式是調用operator newoperator delete運算符。相比於有緩存的分配和可以重複利用之前分配的內存,這種方法可能會比較慢。但是這種方法可以在多種硬件和操作系統中正確地工作。__gnu_cxx::new_allocator實現了簡單的operator newoperator delete語義,而__gun_cxx::malloc_allocator使用C語言的函數std::mallocstd::free實現了相同的功能。

另一種方法是在配置器類中使用智能來緩存分配。 這種額外的機制可以採用多種形式:位圖索引,以2的指數級增長的存儲桶的索引,或者更簡單的固定大小緩存池。緩存可以在程序中的容器間共享。而且,也不總是調用operator new和operator delete來傳遞內存,這可以帶來速度上的優勢。使用這些技術的配置器是__gnu_cxx::bitmap_allocator__gun_cxx::mt_alloc,和__gun_cxx::pool_allocator1

1.2、使用

STL默認使用std::allocator配置器,而該配置器繼承自__gnu_cxx::new_allocator

vector< int > int_vec;

我們可以指定配置器:

vector<int,__gnu_cxx::malloc_allocator<int>> malloc_intvec;

1.3、擴展的配置器

  • 1、new_allocator:簡單地包裝::operator new 和::operator delete
  • 2、malloc_allocator:簡單地包裝malloc和free
  • 3、debug_allocator:在一個任意的配置器A外增加了一層包裝。它將大小略有增加的請求傳遞給A,並使用額外的內存來存儲尺寸大小信息。當將指針傳遞給deallocate()時,將檢查存儲的大小,並使用assert()確保它們匹配。
  • 4、throw_allocator:包含內存跟蹤和標記功能
  • 5、__pool_alloc:高性能的單池配置器。可重用內存在類型相同實例之間共享。當列表(free list)被用完時,它將通過::operator new 獲取新內存。如果客戶容器請求的塊超過某個閾值,則將繞過緩衝池,將分配請求直接傳遞給::operator new。
  • 6、__mt__alloc:高性能的固定尺寸的配置器,分配呈指數級增長。
  • 7、bitmap_allocator:一種高性能的配置器,使用位圖來跟蹤已經使用和未使用的內存位置。

二、C++內存操作

2.1、C++ 中的 new 和 delete

當我們使用一條new表達式時:

string *sp = new string("a value");
string *arr = new string[10];

實際執行了三步操作:

  • 第一步,new表達式調用operator new(或者operator new[ ])標準庫函數。該函數分配一塊足夠大的、原始的、未命名的內存空間以便存儲特定類型的對象(或者對象的數組)。
  • 第二步,編譯器運行相應的構造函數以構造這些對象,併爲其傳入初始值。
  • 第三步,對象被分配了空間並構造完成,返回一個指向該對象的指針。

當我們使用一條delete表達式刪除一個動態分配的對象時:

delete sp;
delete [] arr;

實際執行了兩步操作:

  • 第一步,對sp所指向的對象或者arr所指的數組中的元素執行對應的析構函數。
  • 第二步,編譯器調用名爲operator delete(或者operator delete[ ])的標準庫函數釋放內存空間。2

2.2、STL內存配置

STL將new和delete的實現均分爲兩個階段:

  • 內存配置由allocator::allocate()負責,內存釋放由allocator::deallocate()負責
  • 對象構造由_Construct()負責,對象析構由_Destroy()負責

allocate和deallocate在不同的配置器中有不同的實現,_Construct和_Destroy則在stl_construct.h中實現。

三、對象構造和析構

3.1、對象構造

調用placement new

/**
* Constructs an object in existing memory by invoking an allocated
* object's constructor with an initializer.
*/
#if __cplusplus >= 201103L
template <typename _T1, typename... _Args>
inline void
_Construct(_T1 *__p, _Args &&... __args)
{
    ::new (static_cast<void *>(__p)) _T1(std::forward<_Args>(__args)...);
}
#else
template <typename _T1, typename _T2>
inline void
_Construct(_T1 *__p, const _T2 &__value)
{
    // _GLIBCXX_RESOLVE_LIB_DEFECTS
    // 402. wrong new expression in [some_]allocator::construct
    ::new (static_cast<void *>(__p)) _T1(__value);
}
#endif

3.2、對象析構

3.2.1 析構單個對象

/**
* Destroy the object pointed to by a pointer type.
*/
template <typename _Tp>
inline void
_Destroy(_Tp *__pointer)
{
    __pointer->~_Tp();
}

3.2.2 析構指定範圍內的對象

3.2.2.1、_Destroy(_ForwardIterator __first, _ForwardIterator __last)
template <bool>
struct _Destroy_aux
{
    template <typename _ForwardIterator>
    static void
    __destroy(_ForwardIterator __first, _ForwardIterator __last)
    {
        for (; __first != __last; ++__first)
            std::_Destroy(std::__addressof(*__first));
    }
};

template <>
struct _Destroy_aux<true>
{
    template <typename _ForwardIterator>
    static void
        __destroy(_ForwardIterator, _ForwardIterator) {}
};

/**
* Destroy a range of objects.  If the value_type of the object has
* a trivial destructor, the compiler should optimize all of this
* away, otherwise the objects' destructors must be invoked.
*/
template <typename _ForwardIterator>
inline void
_Destroy(_ForwardIterator __first, _ForwardIterator __last)
{
    typedef typename iterator_traits<_ForwardIterator>::value_type
        _Value_type;
#if __cplusplus >= 201103L
    // A deleted destructor is trivial, this ensures we reject such types:
    static_assert(is_destructible<_Value_type>::value,
                  "value type is destructible");
#endif
    std::_Destroy_aux<__has_trivial_destructor(_Value_type)>::
        __destroy(__first, __last);
}
3.2.2.2、_Destroy_n(_ForwardIterator __first, _Size __count)
template <bool>
struct _Destroy_n_aux
{
    template <typename _ForwardIterator, typename _Size>
    static _ForwardIterator
    __destroy_n(_ForwardIterator __first, _Size __count)
    {
        for (; __count > 0; (void)++__first, --__count)
            std::_Destroy(std::__addressof(*__first));
        return __first;
    }
};

template <>
struct _Destroy_n_aux<true>
{
    template <typename _ForwardIterator, typename _Size>
    static _ForwardIterator
    __destroy_n(_ForwardIterator __first, _Size __count)
    {
        std::advance(__first, __count);
        return __first;
    }
};

/**
   * Destroy a range of objects.  If the value_type of the object has
   * a trivial destructor, the compiler should optimize all of this
   * away, otherwise the objects' destructors must be invoked.
   */
template <typename _ForwardIterator, typename _Size>
inline _ForwardIterator
_Destroy_n(_ForwardIterator __first, _Size __count)
{
    typedef typename iterator_traits<_ForwardIterator>::value_type
        _Value_type;
#if __cplusplus >= 201103L
    // A deleted destructor is trivial, this ensures we reject such types:
    static_assert(is_destructible<_Value_type>::value,
                  "value type is destructible");
#endif
    return std::_Destroy_n_aux<__has_trivial_destructor(_Value_type)>::
        __destroy_n(__first, __count);
}
3.2.2.3、_Destroy(_ForwardIterator __first, _ForwardIterator __last, _Allocator &__alloc)
/**
   * Destroy a range of objects using the supplied allocator.  For
   * nondefault allocators we do not optimize away invocation of 
   * destroy() even if _Tp has a trivial destructor.
   */

template <typename _ForwardIterator, typename _Allocator>
void _Destroy(_ForwardIterator __first, _ForwardIterator __last,
              _Allocator &__alloc)
{
    typedef __gnu_cxx::__alloc_traits<_Allocator> __traits;
    for (; __first != __last; ++__first)
        __traits::destroy(__alloc, std::__addressof(*__first));
}
3.2.2.4、_Destroy(_ForwardIterator __first, _ForwardIterator __last, allocator<_Tp> &)
template <typename _ForwardIterator, typename _Tp>
inline void
_Destroy(_ForwardIterator __first, _ForwardIterator __last,
         allocator<_Tp> &)
{
    _Destroy(__first, __last);
}

3.3 _Construct和_Destroy示意

_Construct
::new(static_cast(__p)) _T1
Destroy
單個對象
_Destroy(_Tp* __pointer)
__pointer->~_Tp()
對象範圍
_Destroy(_ForwardIterator __first, _ForwardIterator __last)
_Destroy_n(_ForwardIterator __first, _Size __count)
_Destroy(_ForwardIterator __first, _ForwardIterator __last, _Allocator& __alloc)
_Destroy(_ForwardIterator __first, _ForwardIterator __last, allocator<_Tp>&)
std::_Destroy_aux::__destroy(__first, __last)
std::_Destroy_n_aux::__destroy_n
__traits::destroy
_Destroy(__first, __last)

四、具體配置器示例

4.1、new_allocator

文件位置:libstdc++ -v3\include\ext\new_allocator.h
簡要介紹:只是將operator new和operator delete做了簡單的包裝

allocate的實現如下:

pointer
allocate(size_type __n, const void * = static_cast<const void *>(0))
{
    if (__n > this->max_size())
        std::__throw_bad_alloc();

#if __cpp_aligned_new
    if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
    {
        std::align_val_t __al = std::align_val_t(alignof(_Tp));
        return static_cast<_Tp *>(::operator new(__n * sizeof(_Tp), __al));
    }
#endif
    return static_cast<_Tp *>(::operator new(__n * sizeof(_Tp)));
}

deallocate的實現如下:

void deallocate(pointer __p, size_type)
{
#if __cpp_aligned_new
    if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
    {
        ::operator delete(__p, std::align_val_t(alignof(_Tp)));
        return;
    }
#endif
    ::operator delete(__p);
}

4.2、__pool_alloc

文件位置:libstdc++ -v3\include\ext\pool_allocator.h
簡要介紹:若請求的區塊大小超過128bytes時,直接調用operator new處理。當區塊小於128bytes時,則以內存池管理:每次配置一大塊內存,並維護對應的自由鏈表(free_list)。下次,若再有相同大小的內存需求,就直接從free_list中分配。如果歸還區塊,則由配置器回收到free_list中。爲了方便管理,配置器會將任何區塊的內存需求量上調至8的倍數,並維護16個free_lists。

template<typename _Tp>
    class __pool_alloc : private __pool_alloc_base

在__pool_alloc_base中維護一個內存池

    class __pool_alloc_base
    {
    protected:

      enum { _S_align = 8 };
      enum { _S_max_bytes = 128 };
      enum { _S_free_list_size = (size_t)_S_max_bytes / (size_t)_S_align };
      
      union _Obj
      {
	      union _Obj* _M_free_list_link;
	      char        _M_client_data[1];    // The client sees this.
      };
      
      static _Obj* volatile         _S_free_list[_S_free_list_size];

      // Chunk allocation state.
      static char*                  _S_start_free;
      static char*                  _S_end_free;
      static size_t                 _S_heap_size;     
      
      size_t
      _M_round_up(size_t __bytes)
      { return ((__bytes + (size_t)_S_align - 1) & ~((size_t)_S_align - 1)); }
      
      _GLIBCXX_CONST _Obj* volatile*
      _M_get_free_list(size_t __bytes) throw ();
    
      __mutex&
      _M_get_mutex() throw ();

      // Returns an object of size __n, and optionally adds to size __n
      // free list.
      void*
      _M_refill(size_t __n);
      
      // Allocates a chunk for nobjs of size size.  nobjs may be reduced
      // if it is inconvenient to allocate the requested number.
      char*
      _M_allocate_chunk(size_t __n, int& __nobjs);
    };

4.2.1、allocate()

template <typename _Tp>
_Tp *__pool_alloc<_Tp>::allocate(size_type __n, const void *)
{
    pointer __ret = 0;
    if (__builtin_expect(__n != 0, true))
    {
        if (__n > this->max_size())
            std::__throw_bad_alloc();

        const size_t __bytes = __n * sizeof(_Tp);

#if __cpp_aligned_new
        if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
        {
            std::align_val_t __al = std::align_val_t(alignof(_Tp));
            return static_cast<_Tp *>(::operator new(__bytes, __al));
        }
#endif

        // If there is a race through here, assume answer from getenv
        // will resolve in same direction.  Inspired by techniques
        // to efficiently support threading found in basic_string.h.
        if (_S_force_new == 0)
        {
            if (std::getenv("GLIBCXX_FORCE_NEW"))
                __atomic_add_dispatch(&_S_force_new, 1);
            else
                __atomic_add_dispatch(&_S_force_new, -1);
        }
        //大於128bytes,直接調用operator new
        if (__bytes > size_t(_S_max_bytes) || _S_force_new > 0)
            __ret = static_cast<_Tp *>(::operator new(__bytes));
        else
        {
            _Obj *volatile *__free_list = _M_get_free_list(__bytes);

            __scoped_lock sentry(_M_get_mutex());
            _Obj *__restrict__ __result = *__free_list;
            if (__builtin_expect(__result == 0, 0))
                //沒有找到合適的__free_list,重新填充
                __ret = static_cast<_Tp *>(_M_refill(_M_round_up(__bytes)));
            else
            {
                *__free_list = __result->_M_free_list_link;
                __ret = reinterpret_cast<_Tp *>(__result);
            }
            if (__ret == 0)
                std::__throw_bad_alloc();
        }
    }
    return __ret;
}

4.2.1.1、 __M_refill()

// Returns an object of size __n, and optionally adds to "size
// __n"'s free list.  We assume that __n is properly aligned.  We
// hold the allocation lock.
void *
__pool_alloc_base::_M_refill(size_t __n)
{
    int __nobjs = 20;//默認取20個區塊
    char *__chunk = _M_allocate_chunk(__n, __nobjs);
    _Obj *volatile *__free_list;
    _Obj *__result;
    _Obj *__current_obj;
    _Obj *__next_obj;

    if (__nobjs == 1)
        return __chunk;
    __free_list = _M_get_free_list(__n);

    // Build free list in chunk.
    __result = (_Obj *)(void *)__chunk;
    *__free_list = __next_obj = (_Obj *)(void *)(__chunk + __n);
    for (int __i = 1;; __i++)
    {
        __current_obj = __next_obj;
        __next_obj = (_Obj *)(void *)((char *)__next_obj + __n);
        if (__nobjs - 1 == __i)
        {
            __current_obj->_M_free_list_link = 0;
            break;
        }
        else
            __current_obj->_M_free_list_link = __next_obj;
    }
    return __result;
}

4.2.1.2、 _M_allocate_chunk()

// Allocate memory in large chunks in order to avoid fragmenting the
// heap too much.  Assume that __n is properly aligned.  We hold the
// allocation lock.
char *
__pool_alloc_base::_M_allocate_chunk(size_t __n, int &__nobjs)
{
    char *__result;
    size_t __total_bytes = __n * __nobjs;
    size_t __bytes_left = _S_end_free - _S_start_free;

    if (__bytes_left >= __total_bytes)
    {//內存池剩餘空間完全滿足需要
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return __result;
    }
    else if (__bytes_left >= __n)
    {//內存池剩餘空間不能完全滿足需要,但足夠分配一個及以上的區塊
        __nobjs = (int)(__bytes_left / __n);
        __total_bytes = __n * __nobjs;
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return __result;
    }
    else
    {//內存池剩餘空間連一個區塊大小都不能滿足
        // Try to make use of the left-over piece.
        if (__bytes_left > 0)
        {//將剩餘的空間分配到合適的_S_free_list中
            _Obj *volatile *__free_list = _M_get_free_list(__bytes_left);
            ((_Obj *)(void *)_S_start_free)->_M_free_list_link = *__free_list;
            *__free_list = (_Obj *)(void *)_S_start_free;
        }

        size_t __bytes_to_get = (2 * __total_bytes + _M_round_up(_S_heap_size >> 4));
        __try
        {
            _S_start_free = static_cast<char *>(::operator new(__bytes_to_get));
        }
        __catch(const std::bad_alloc &)
        {//heap剩餘空間不足,::operate new失敗
        //1、從_S_free_list中找出有剩餘內存的且大於等於__n的最小的鏈
        //2、從中分配__n大小的內存,同時修改_S_start_free和_S_end_free
        //3、遞歸調用_M_allocate_chunk時,將多餘的內存放入適合的_S_free_list中
            // Try to make do with what we have.  That can't hurt.  We
            // do not try smaller requests, since that tends to result
            // in disaster on multi-process machines.
            size_t __i = __n;
            for (; __i <= (size_t)_S_max_bytes; __i += (size_t)_S_align)
            {
                _Obj *volatile *__free_list = _M_get_free_list(__i);
                _Obj *__p = *__free_list;
                if (__p != 0)
                {
                    *__free_list = __p->_M_free_list_link;
                    _S_start_free = (char *)__p;
                    _S_end_free = _S_start_free + __i;
                    return _M_allocate_chunk(__n, __nobjs);
                    // Any leftover piece will eventually make it to the
                    // right free list.
                }
            }
            // What we have wasn't enough.  Rethrow.
            _S_start_free = _S_end_free = 0; // We have no chunk.
            __throw_exception_again;
        }
        //::operator new成功,修改_S_heap_size、_S_end_free
        //遞歸調用_M_allocate_chunk,完成分配
        _S_heap_size += __bytes_to_get;
        _S_end_free = _S_start_free + __bytes_to_get;
        return _M_allocate_chunk(__n, __nobjs);
    }
}

4.2.1.3、 allocate()流程圖

在這裏插入圖片描述

4.2.2、 deallocate()

template <typename _Tp>
void __pool_alloc<_Tp>::deallocate(pointer __p, size_type __n)
{
    if (__builtin_expect(__n != 0 && __p != 0, true))
    {
#if __cpp_aligned_new
        if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
        {
            ::operator delete(__p, std::align_val_t(alignof(_Tp)));
            return;
        }
#endif
        const size_t __bytes = __n * sizeof(_Tp);
        if (__bytes > static_cast<size_t>(_S_max_bytes) || _S_force_new > 0)
            ::operator delete(__p);
        else
        {
            _Obj *volatile *__free_list = _M_get_free_list(__bytes);
            _Obj *__q = reinterpret_cast<_Obj *>(__p);

            __scoped_lock sentry(_M_get_mutex());
            __q->_M_free_list_link = *__free_list;
            *__free_list = __q;
        }
    }
}

  1. https://gcc.gnu.org/onlinedocs/libstdc++/manual/memory.html#std.util.memory.allocator ↩︎

  2. C++ primer,中文版,第5版 ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章