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版 ↩︎

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