STL源码剖析(一):空间配置器allocator

304 阅读7分钟

​ 源码下载地址:

github.com/steveLauwh/…

空间分配函数

首先需要了解内存分配的一个过程:程序调用=》allocator=》new=》malloc=》操作系统分配。

然后要理解三个有关于new的概念

  1. new operator:分配内存并初始化对象,不可被重载
  2. operator new:只分配内存,不初始化对象,可被重载,相当于C的malloc
  3. placement new:对分配好的内存进行初始化

在这里插入图片描述

Foo * p = new (ptr) Foo(value);  // ptr指向已经分配内存的起始位置

SGI配置器

SGI STL的配置器与标准规范不同,其名称为alloc而非allocator,不接受任何参数。所以在VC或CB需要这么指定配置器:

vector<int,std::allocator> iv;

而在GCC中,需这样指定:

vector<int,std::alloc> iv;

但其实SGI STL也支持allocator,只是效率不佳,仅仅对::operator new做了一层薄薄的封装。

一般来说,以下代码分为两阶段:(1)调用::operator new配置内存;(2)调用Foo::Foo()构造对象内容。

Foo* pf=new Foo;

STL alloc将这两阶段分开,内存配置交给alloc::allocate()负责,对象构造交给::construct()负责。

配置器定义于< memory>中,SGI 内含两个文件

<stl_alloc.h>:负责内存空间的配置和释放

<stl_construct.h>:负责对象内容的构造和析构

还有个重要的文件 <stl_uninitialized.h>:用来fill或copy大块内存数据

stl_construct.h

定义了全局函数construct() 和 destroy()

construct() 核心代码

    // 将初值 __value 设定到指针所指的空间上。
    template <class _T1, class _T2>
    inline void _Construct(_T1* __p, const _T2& __value) {
      new ((void*) __p) _T1(__value);   // placement new,调用 _T1::_T1(__value);
    }

destroy()核心代码

    // 第一个版本,接受一个指针,准备将该指针所指之物析构掉。
    template <class _Tp>
    inline void _Destroy(_Tp* __pointer) {
      __pointer->~_Tp();
    }

    // 第二个版本,接收两个迭代器。
    // 调用 __VALUE_TYPE() 获得迭代器所指对象的类别
    template <class _ForwardIterator>
    inline void _Destroy(_ForwardIterator __first, _ForwardIterator __last) {
      __destroy(__first, __last, __VALUE_TYPE(__first));
    }

其实destroy还有一个需要判断的地方:

 判断value_type时,如果这些元素有自己的析构函数,则这个析构函数叫做non-trivial destructor(构造函数、析构函数、拷贝构造函数、复制构造函数都可能是non-trivial)。

POD类型是C++的内建类型或传统C结构体类型,必有trivial ctor/dtor/copy/assignment四种函数。

若value_type没有non-trivail destructor,则说明这个对象的析构是无意义的,因而不需要一次又一次地调用那些无关痛痒的析构函数,直接调用malloc()、memcpy()等内存操作提高性能。

判断value_type是否有non-trival destrory的方法:

    // 利用__type_traits<T>判断一个对象的所述类,进而得知该类型的析构函数是否需要做什么。
    // 如果为true,则什么也不用做;否则,逐个调用第一个版本的析构函数
    template <class _ForwardIterator, class _Tp>
    inline void 
    __destroy(_ForwardIterator __first, _ForwardIterator __last, _Tp*)
    {
      typedef typename __type_traits<_Tp>::has_trivial_destructor
              _Trivial_destructor;
      __destroy_aux(__first, __last, _Trivial_destructor());
    }

 空间的配置和释放:alloc配置器

stl标准的空间配置为allocator,SGI也实现了这个版本的配置器,但由于其效率不高,SGI STL默认的配置器为alloc。

考虑到小型区块可能造成的内存破碎问题,SGI设计了双层级配置器:当配置区块超过128bytes时,便调用第一级配置器;如果小于128bytes,为降低额外开销overhead(稍后再讲),便采用复杂的memory pool整理方式。

为使得alloc符合STL规格,SGI将Alloc做了一层封装:

第一级配置器 __malloc_alloc_template

template <int __inst>
class __malloc_alloc_template {

private:
  
  // 以下函数将用来处理内存不足的情况
  static void* _S_oom_malloc(size_t);
  static void* _S_oom_realloc(void*, size_t);

#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
  static void (* __malloc_alloc_oom_handler)();
#endif

public:

  // 第一级配置器直接调用 malloc()
  static void* allocate(size_t __n)
  {
    void* __result = malloc(__n);
    // 以下无法满足需求时,改用 _S_oom_malloc()
    if (0 == __result) __result = _S_oom_malloc(__n);
    return __result;
  }

  // 第一级配置器直接调用 free()
  static void deallocate(void* __p, size_t /* __n */)
  {
    free(__p);
  }
  
  // 第一级配置器直接调用 realloc()
  static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
  {
    void* __result = realloc(__p, __new_sz);
    // 以下无法满足需求时,改用 _S_oom_realloc()
    if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
    return __result;
  }

  // 以下仿真 C++ 的 set_new_handler(),可以通过它指定自己的 out-of-memory handler
  // 为什么不使用 C++ new-handler 机制,因为第一级配置器并没有 ::operator new 来配置内存
  // 可能是历史原因
  static void (* __set_malloc_handler(void (*__f)()))()
  {
    void (* __old)() = __malloc_alloc_oom_handler;
    __malloc_alloc_oom_handler = __f;
    return(__old);
  }
};

当内存分配失败后,将通过out-of-memory handler尝试分配新内存,以_S_oom_malloc为例:

#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
// 初值为0,由客户自行设定
template <int __inst>
void (* __malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;
#endif

template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)
{
    void (* __my_malloc_handler)();
    void* __result;

    // 不断尝试释放、配置
    for (;;) {
        __my_malloc_handler = __malloc_alloc_oom_handler;
        if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }
        (*__my_malloc_handler)();  // 调用处理例程,企图释放内存
        __result = malloc(__n);   // 再次尝试配置内存
        if (__result) return(__result);
    }
}

C++ new handler机制:你可以要求系统在内存配置需求无法满足时,在抛出std::bad_alloc异常状态前,调用一个你所指定的函数,这个函数就称为new-handler。

第二级配置器 __default_alloc_template

malloc向系统申请内存时,需配置额外负担以管理内存,内存块越小,额外负担所占比例就越大。而在C++中,大部分内存都是小块的(如变量),如果小块内存也直接malloc,将造成大量的空间浪费。

​编辑

 在上图中,蓝色部分为实际数据,绿色部分为字节补齐部分,上下两端的红色部记录内存块的大小(这也是为什么,在free一块内存时,只需要指导这块内存的首地址即可)。红色的部分称为cookie,两个cookie各占4字节。至于为什么上下两端都有,暂时不知道。

因此,STL尽量减少malloc的次数,设置了如下的自由链表free-lists:

 free-list的节点结构为:

union _Obj {
     union _Obj* _M_free_list_link;  // 利用联合体特点
     char _M_client_data[1];    /* The client sees this.        */
};

free-list有三个值得注意的点:

  1. union的妙用,可以节省自由链表中指针的额外开销:在当前节点未分配前,节点中的值为下一个节点的地址;当节点分配后,free_list头指针指向它的下一个节点,而它内部的值被替换为用户的data值。=》从这也看出,自由链表从头节点分配效率更高。
  2. 区块按照8字节对齐:8、16、24、...、128bytes
  3. 配置器不仅负责区块的分配器,也负责区块的回收,也就是把一个区块挂到对应的free-list

如果自由链表中没有可分配的节点了,将从内存池获取内存并分配节点:

  // 配置一大块空间,可容纳nobjs个大小为 size的区块
  // 如果内存池空间也不够了,nobjs可能会降低
  static char* _S_chunk_alloc(size_t __size, int& __nobjs);

  // Chunk allocation state.
  static char* _S_start_free;  // 内存池起始位置。只在 _S_chunk_alloc() 中变化
  static char* _S_end_free;    // 内存池结束位置。只在 _S_chunk_alloc() 中变化
  static size_t _S_heap_size;

 同样,二级配置器也需要符合STL标准接口,需要封装函数 allocate():

  static void* allocate(size_t __n)
  {
    void* __ret = 0;

    // 如果需求区块大于 128 bytes,就转调用第一级配置
    if (__n > (size_t) _MAX_BYTES) {
      __ret = malloc_alloc::allocate(__n);
    }
    else {
      // 根据申请空间的大小寻找相应的空闲链表(16个空闲链表中的一个)
      _Obj* __STL_VOLATILE* __my_free_list
          = _S_free_list + _S_freelist_index(__n);
      _Obj* __RESTRICT __result = *__my_free_list;
      // 空闲链表没有可用数据块,就将区块大小先调整至 8 倍数边界,然后调用 _S_refill() 重新填充
      if (__result == 0)
        __ret = _S_refill(_S_round_up(__n));
      else {
        // 如果空闲链表中有空闲数据块,则取出一个,并把空闲链表的指针指向下一个数据块  
        *__my_free_list = __result -> _M_free_list_link;
        __ret = __result;
      }
    }

    return __ret;
  };

 回收区块:deallocate(),其中关于线程安全的代码未粘贴

  // 空间释放函数 deallocate()
  static void deallocate(void* __p, size_t __n)
  {
    // 大于 128 bytes,就调用第一级配置器的释放
    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;
      // 调整空闲链表,回收数据块
      __q -> _M_free_list_link = *__my_free_list;   
      *__my_free_list = __q;
    }
  }

free lists的填充

在allocate时,如果没有可用区块时,就调用refill(),为free list重新填充空间。新的空间取自内存池(经由chunk_alloc()完成),取得20个新节点,如果内存池空间也不足,获得的节点数可能小于20。以下为refill()源码:

template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
    int __nobjs = 20;
    // 调用 _S_chunk_alloc(),缺省取 20 个区块作为 free list 的新节点
    char* __chunk = _S_chunk_alloc(__n, __nobjs);
    _Obj* __STL_VOLATILE* __my_free_list;
    _Obj* __result;
    _Obj* __current_obj;
    _Obj* __next_obj;
    int __i;

    // 如果只获得一个数据块,那么这个数据块就直接分给调用者,空闲链表中不会增加新节点
    if (1 == __nobjs) return(__chunk);
    // 否则根据申请数据块的大小找到相应空闲链表 
    __my_free_list = _S_free_list + _S_freelist_index(__n);   

    /* 在chunk空间建立free list */
      __result = (_Obj*)__chunk;
      *__my_free_list = __next_obj = (_Obj*)(__chunk + __n);  
      // 从1开始,第0个数据块给调用者,地址访问即chunk~chunk + n - 1  
      for (__i = 1; ; __i++) {    // 头插法
        __current_obj = __next_obj;
        __next_obj = (_Obj*)((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);
}

内存池取内存的过程chunk_alloc() :

// 从内存池中取空间
template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size, 
                                                            int& __nobjs)
{
    char* __result;
    size_t __total_bytes = __size * __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 >= __size) {  // 内存池剩余空间不能满足申请,提供一个以上的区块
        __nobjs = (int)(__bytes_left/__size);
        __total_bytes = __size * __nobjs;
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return(__result);
    } else {                             // 内存池剩余空间连一个区块的大小都无法提供                      
        size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
	    // 内存池的剩余空间分给合适的空闲链表
        if (__bytes_left > 0) {
            _Obj* __STL_VOLATILE* __my_free_list =
                        _S_free_list + _S_freelist_index(__bytes_left);

            ((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
            *__my_free_list = (_Obj*)_S_start_free;
        }
        // 配置 heap 空间,用来补充内存池
        _S_start_free = (char*)malloc(__bytes_to_get); 
        if (0 == _S_start_free) {  // heap 空间不足,malloc() 失败
            size_t __i;
            _Obj* __STL_VOLATILE* __my_free_list;
	         _Obj* __p;
            for (__i = __size;
                 __i <= (size_t) _MAX_BYTES;
                 __i += (size_t) _ALIGN) {
                 __my_free_list = _S_free_list + _S_freelist_index(__i);
                 __p = *__my_free_list;
                 if (0 != __p) {
                    *__my_free_list = __p -> _M_free_list_link;
                    _S_start_free = (char*)__p;
                    _S_end_free = _S_start_free + __i;
                    return(_S_chunk_alloc(__size, __nobjs));
                }
            }
	    _S_end_free = 0;
            // 调用第一级配置器
            _S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);  
        }
        _S_heap_size += __bytes_to_get;
        _S_end_free = _S_start_free + __bytes_to_get;
        return(_S_chunk_alloc(__size, __nobjs));  // 递归调用自己
    }
}

  • 内存池空间完全满足容量,直接分配

  • 内存池空间不足,但能分配一个以上的区块,则先给分配出来给调用方用着先

  • 如果一个区块都分配不了了,就准备扩充内存池了,在此之前,需将内存池中较小的零头区块分配给合适的free list。(96bytes的没有,可能有16bytes的)

  • 调用malloc补充内存池

    • 假如malloc这里配置了40+n(附加量)个96bytes的区块,一个交给调用方,19个交给free list,另外20+n(附加量)个交给内存池。
  • 如果heap空间不足,malloc失败,那就从大块的free list找有没有空闲的节点,有的话就把这个区块释放了,拿给小区块的free list用用。

  • 如果free list中的大区快也都用完了,只能借助第一级配置器(借用里面的out-of-memory机制,看有没有哪个程序可以释放一部分资源出来)

内存池的实际操作:

tip:

对于容器,如vector,默认的配置器为alloc(也可以自己指定为allocator),而alloc配置器默认使用二级配置器。

 

 空间配置讲完了,那如何在分配的空间上进行数据操作呢?前面讲过了construct()和destroy(),另外还有三个填充类函数,其实际定义于<stl_uninitialized>:

一、uninitialized_copy:区间拷贝

// 内存的配置与对象的构造行为分离开来。
template <class _InputIter, class _ForwardIter>
inline _ForwardIter
  uninitialized_copy(_InputIter __first, _InputIter __last,
                     _ForwardIter __result)
{
  return __uninitialized_copy(__first, __last, __result,
                              __VALUE_TYPE(__result));
}

简单来说就是首先得到__result的 value_type(如是是否POD类型,判断是否含有non-trivial 函数),进而判断是调用construct() 还是 直接调用memmove()或copy() ;然后,就是把 [__first,__last)这个区间上的数据,挨个复制到目标容器上。

可以看出__result 这个迭代器有两个功能:指示目标容器的起点;指示迭代器所指元素的类型。

以下是这个函数的进一步转调:

// 这个函数用来过渡的,这种技法在类型推导中比较常见
template <class _InputIter, class _ForwardIter, class _Tp>
inline _ForwardIter
__uninitialized_copy(_InputIter __first, _InputIter __last,
                     _ForwardIter __result, _Tp*)
{
  typedef typename __type_traits<_Tp>::is_POD_type _Is_POD;
  return __uninitialized_copy_aux(__first, __last, __result, _Is_POD());
}

// 如果是trival construct,直接内存拷贝
template <class _InputIter, class _ForwardIter>
inline _ForwardIter 
__uninitialized_copy_aux(_InputIter __first, _InputIter __last,
                         _ForwardIter __result,
                         __true_type)
{
  return copy(__first, __last, __result);
}

// 如果是non-trival construct,挨个复制
template <class _InputIter, class _ForwardIter>
_ForwardIter 
__uninitialized_copy_aux(_InputIter __first, _InputIter __last,
                         _ForwardIter __result,
                         __false_type)
{
  _ForwardIter __cur = __result;
  __STL_TRY {
    for ( ; __first != __last; ++__first, ++__cur)
      _Construct(&*__cur, *__first);
    return __cur;
  }
  __STL_UNWIND(_Destroy(__result, __cur));
}

针对char* 和 wchar_t* 两种类型,用memmove(直接移动内存内容)来执行复制行为,以下是两种类型的特化版本:

inline char* uninitialized_copy(const char* __first, const char* __last,
                                char* __result) {
  memmove(__result, __first, __last - __first);
  return __result + (__last - __first);
}

inline wchar_t* 
uninitialized_copy(const wchar_t* __first, const wchar_t* __last,
                   wchar_t* __result)
{
  memmove(__result, __first, sizeof(wchar_t) * (__last - __first));
  return __result + (__last - __first);
}

二、uninitialized_fill:区间单值填充

template <class _ForwardIter, class _Tp>
inline void uninitialized_fill(_ForwardIter __first,
                               _ForwardIter __last, 
                               const _Tp& __x)
{
  __uninitialized_fill(__first, __last, __x, __VALUE_TYPE(__first));
}

可以看出,就是将__x的值复制到[__first,__last),复制的技法和uninitialized_copy类似。注意这里的__first和__last是目标迭代器。

三、uninitialized_fill_n

template <class _ForwardIter, class _Size, class _Tp>
inline _ForwardIter 
uninitialized_fill_n(_ForwardIter __first, _Size __n, const _Tp& __x)
{
  return __uninitialized_fill_n(__first, __n, __x, __VALUE_TYPE(__first));
}

和上一个函数很相近,只不过是把区间终点换成区间长度了。

总结:以上三个函数,其实就是结合转调实现泛化和特化的一个过程。

个人感觉:char* 类型的特化,有点像打补丁,应该归属 __true_type那个版本中去,然后再特化,不调用copy()而调用memmove(),这样更好理解一些。