STL空间分配器源码分析(一)

1. 摘要

STL的空间分配器(allocator)定义于命名空间std内,主要为STL容器提供内存的分配和释放、对象的构造和析构的统一管理。空间分配器的实现细节,对于容器来说完全透明,容器不需关注内存分配和回收的策略细节如何。

STL allocator需实现如下4个标准接口

pointer allocate(size_type __n, const void*);     //内存分配
void deallocate(pointer __p, size_type __n);     //内存释放 
void construct(pointer __p, const _Tp& __val);    //构造
void destroy(pointer __p);              //析构

2. STL的几种空间分配器介绍

  • __new_allocator:C++标准中定义的分配器,仅对operator new和operator delete做简单封装;

  • malloc_allocator:C++标准中定义的分配器,仅对std::malloc和std::free做简单封装;

  • __mt_alloc:一种支持多线程的空间配置器(亦可单线程),可分配2的幂次方大小的内存块,该配置器可灵活调整,性能高(stl手册描述,个人未实测);

  • bitmap_allocator:一种使用位图来区分内存是否分配的配置器;

  • __pool_alloc:带有单锁内存池的器,即侯捷于《STL源码剖析》中介绍的SGI-STL空间配置器;

  • debug_allocator:该空间分配器主要用于调试,可包裹其他allocator,于用户层申请的内存大小的基础上扩容部分,附带调试信息;

  • throw_allocator:具有日志记录和异常生成控制的分配器;

补充说明:operator new和std::malloc都是仅申请内存,申请的内存不做初始化,但其仍存在如下区别:

  • operator new 可由用户重载,调用new关键字时将自动调用重载的operator new函数,而std::malloc不能重载;

  • operator new 有异常机制,在内存申请失败时会抛出异常,std::malloc申请失败只会返回NULL;

  • std::malloc 可和realloc结合使用,调整内存申请大小,operator new无类似的操作;

3. new_allocator

template<typename _Tp>
class __new_allocator
{
public:
  /* 重定义了几种类型别名 */
    typedef _Tp        value_type;
    typedef std::size_t     size_type;
    typedef std::ptrdiff_t  difference_type;
#if __cplusplus <= 201703L
    typedef _Tp*       pointer;
    typedef const _Tp* const_pointer;
    typedef _Tp&       reference;
    typedef const _Tp& const_reference;

    template<typename _Tp1>
    struct rebind
    { typedef __new_allocator<_Tp1> other; };
#endif

  /* 以下几个构造和析构函数,没有做特别实现 */
    _GLIBCXX20_CONSTEXPR
    __new_allocator() _GLIBCXX_USE_NOEXCEPT { }

    _GLIBCXX20_CONSTEXPR
    __new_allocator(const __new_allocator&) _GLIBCXX_USE_NOEXCEPT { }

    template<typename _Tp1>
    _GLIBCXX20_CONSTEXPR
    __new_allocator(const __new_allocator<_Tp1>&) _GLIBCXX_USE_NOEXCEPT { }

#if __cplusplus <= 201703L
    ~__new_allocator() _GLIBCXX_USE_NOEXCEPT { }

  /* 取参数地址 */
    pointer
    address(reference __x) const _GLIBCXX_NOEXCEPT
    { return std::__addressof(__x); }

    const_pointer
    address(const_reference __x) const _GLIBCXX_NOEXCEPT
    { return std::__addressof(__x); }
#endif

    _GLIBCXX_NODISCARD _Tp*
    allocate(size_type __n, const void* = static_cast<const void*>(0));

    void
    deallocate(_Tp* __p, size_type __n __attribute__ ((__unused__)));


#if __cplusplus <= 201703L
    size_type
    max_size() const _GLIBCXX_USE_NOEXCEPT
    { return _M_max_size(); }

#if __cplusplus >= 201103L
  /* C++11新特性,采用std::forward将参数完美转发,保留其右值属性 
  *  调用placement new,在已分配好的内存上构造对象,形参为std::forward转发的参数
  */
    template<typename _Up, typename... _Args>
    void
    construct(_Up* __p, _Args&&... __args)
    noexcept(std::is_nothrow_constructible<_Up, _Args...>::value)
    { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }

  /* 显示调用析构函数 */
    template<typename _Up>
    void
    destroy(_Up* __p)
    noexcept(std::is_nothrow_destructible<_Up>::value)
    { __p->~_Up(); }
#else
  /* 调用placement new,在已分配好的内存上构造对象 */
    void
    construct(pointer __p, const _Tp& __val)
    { ::new((void *)__p) _Tp(__val); }

  /* 显示调用析构函数 */
    void
    destroy(pointer __p) { __p->~_Tp(); }
#endif
#endif // ! C++20

  /* 以下重载了“=”和“!=”运算符,不做特殊实现 */
    template<typename _Up>
    friend _GLIBCXX20_CONSTEXPR bool
    operator==(const __new_allocator&, const __new_allocator<_Up>&)
    _GLIBCXX_NOTHROW
    { return true; }

#if __cpp_impl_three_way_comparison < 201907L
    template<typename _Up>
    friend _GLIBCXX20_CONSTEXPR bool
    operator!=(const __new_allocator&, const __new_allocator<_Up>&)
    _GLIBCXX_NOTHROW
    { return false; }
#endif

private:
    _GLIBCXX_CONSTEXPR size_type
    _M_max_size() const _GLIBCXX_USE_NOEXCEPT;
}

以上代码裁剪了源码的部分实现,加入个人注释理解。空间配置器的重点在于内存的分配和释放,对象的构造和析构,new_allocator对此仅用operator new、operator delete、placement new做简单封装。

上述代码中rebind 的定义是stl中的一个特点,所有的空间配置器都实现了类似如下的定义

template<typename _Tp1>
struct rebind
{ typedef __new_allocator<_Tp1> other; };

其作用在于,实现对不同类型采用同一内存分配策略的需求。

空间配置器作为容器的模板参数,容器只知其形参名而不知其具体的内存配置策略如何,当容器需要对另一类型采用同样的内存配置策略时,此时就可以采用rebind,获取到其所需另一类型的,符合同一内存分配策略的空间配置器

std::allcoator<T>::rebind<U>::other 等价于std::allcoator<U>。

3.1 allocate的实现

template<typename _Tp1>
struct rebind
{ typedef __new_allocator<_Tp1> other; };

_GLIBCXX_NODISCARD _Tp*
allocate(size_type __n, const void* = static_cast<const void*>(0))
{
#if __cplusplus >= 201103L
    /* 静态断言,编译期间检查类型大小 */
     static_assert(sizeof(_Tp) != 0, "cannot allocate incomplete types");
#endif

    /* GCC 提供的分支预测 */
    if (__builtin_expect(__n > this->_M_max_size(), false))
    {
        if (__n > (std::size_t(-1) / sizeof(_Tp)))
          std::__throw_bad_array_new_length();
        std::__throw_bad_alloc();
    }

#if __cpp_aligned_new
    /* 当类型对齐后的大小大于系统默认内存对齐大小,采用 
    * void* operator new ( std::size_t count, std::align_val_t al) 作为内存申请的接口
    */
    if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
    {
        std::align_val_t __al = std::align_val_t(alignof(_Tp));
        return static_cast<_Tp*>(_GLIBCXX_OPERATOR_NEW(__n * sizeof(_Tp),
                               __al));
    }
#endif
    return static_cast<_Tp*>(_GLIBCXX_OPERATOR_NEW(__n * sizeof(_Tp)));
}
  • __builtin_expect(exp, x) 是GCC的一个内建函数,用于分支预测,提高性能(处理if else分支时,前面分支的汇编指令会先装载,后面分支的指令需要通过JMP指令才能访问,JMP访问比前者更耗时间,大量的JMP访问会有性能开销,因此,采用分支预测,CPU提前装载执行概率更高的指令,提高性能)。
      __builtin_expect(exp, x)期望的表达式exp==x,当x=0时,if分支执行的可能性小,否则else分支执行的可能性小。函数的范围值为exp。

  • STDCPP_DEFAULT_NEW_ALIGNMENT 是 operator new 操作对齐值的阈值,超过这个值,operator new将无法保证分配的内存满足对齐要求,此时可用 void* operator new ( std::size_t count, std::align_val_t al) 作为内存申请的接口,该接口为C++17实现。接口将强行使用指定的参数作为内存对齐的大小分配内存。

  • alignof(_Tp)运算符用于计算类型的内存对齐大小。std::align_val_t 为枚举类型,定义如:enum class align_val_t: size_t {}; 域化枚举,并指定类型为size_t。

3.2 deallocate的实现

void
deallocate(_Tp* __p, size_type __n __attribute__ ((__unused__)))
{
#if __cpp_sized_deallocation
# define _GLIBCXX_SIZED_DEALLOC(p, n) (p), (n) * sizeof(_Tp)
#else
# define _GLIBCXX_SIZED_DEALLOC(p, n) (p)
#endif

#if __cpp_aligned_new
    if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
    {
        _GLIBCXX_OPERATOR_DELETE(_GLIBCXX_SIZED_DEALLOC(__p, __n),
                 std::align_val_t(alignof(_Tp)));
        return;
    }
#endif
    _GLIBCXX_OPERATOR_DELETE(_GLIBCXX_SIZED_DEALLOC(__p, __n));
}

3.3 其中几个宏的定义

#if __has_builtin(__builtin_operator_new) >= 201802L
# define _GLIBCXX_OPERATOR_NEW __builtin_operator_new
# define _GLIBCXX_OPERATOR_DELETE __builtin_operator_delete
#else
# define _GLIBCXX_OPERATOR_NEW ::operator new
# define _GLIBCXX_OPERATOR_DELETE ::operator delete
#endif

3.4 _M_max_size的实现

_GLIBCXX_CONSTEXPR size_type
_M_max_size() const _GLIBCXX_USE_NOEXCEPT
{
#if __PTRDIFF_MAX__ < __SIZE_MAX__
    return std::size_t(__PTRDIFF_MAX__) / sizeof(_Tp);
#else
    return std::size_t(-1) / sizeof(_Tp);
#endif
}

4. malloc_allocator

malloc_allocator的实现与new allocator实现类似,区别在于调用的接口不同,malloc_allocator封装的接口为std::malloc和std::free。此空间配置器不做源码分析。

本系列章节所分析的源码基于gcc-master。

posted @ 2022-03-28 23:07  流翎  阅读(407)  评论(0编辑  收藏  举报