《STL源码剖析》笔记-空间配置器

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/WizardtoH/article/details/81946956

上一篇:《STL源码剖析》笔记-STL概论和版本介绍

单从运用STL来说,空间配置器是了解不到的,因为它隐藏在了容器的背后默默工作。整个STL操作对象都存放在容器中,而容器需要空间用来存放数据。那么为什么不叫内存配置器?因为空间不一定是内存,也可以是磁盘或者其他存储介质。不过,以下介绍SGI(Silicno Graphics Computer Systems,Inc) STL提供的配置器,配置的对象是内存。

空间配置器的标准接口

// 一系列类型定义,在后续会进行介绍
allocator::value_type
allocator::pointer
allocator::const_pointer
allocator::reference
allocator::const_reference
allocator::size_type
allocator::difference_type
allocator::rebind // class rebind<U>拥有唯一成员other;是一个typedef,代表allocator<U>

// 默认构造函数和析构函数,因为没有数据成员,所以不需要初始化,但是必须被定义
allocator::allocator()
allocator::allocator(const allocator&)
template <class U> allocator::allocator(const allocator<U>&)
allocator::~allocator()

// 初始化、地址相关函数
pointer allocator::allocate(size_type n, const void*=0)
size_type allocator::max_size() const
pointer allocator::address(reference x) const
const_pointer allocator::address(const_reference x) const
void deallocate(pointer p, size_type n)

// 构建函数
void allocator::construct(pointer p, const T& x)
void allocator::destory(pointer p)

SGI特殊的空间配置器

SGI STL中的空间配置器与规范不同,名称为alloc而不是allocater,且不接受任何参数。

std::vector<int, std::allocator<int>> vec vec;  // VC编译器
std::vector<int, std::alloc> vec;               // GCC编译器

不过对于使用来说影响并不大,因为使用是通常都是使用默认的空间配置器,不需要自行指定。SGI STL也定义了符合部分标准的、名为allocator的配置器,但是因为效率不佳不建议使用。

C++中new和delete其实都包含两步操作,new包含了申请内存、调用构造函数,delete包含了调用析构函数、释放内存。而
allocator为了更加精密分工,区分开了两个步骤:allocate和deallocate负责内存申请和释放,construct和destory负责构造和析构。

构造和析构

    template <class T1, class T2>  
    inline void construct(T1* p, const T2& value) {  
        // placement new,在已经分配的内存上进行构造
        new (p) T1(value);  
    }  

    // 以下是 destroy() 第一版本,接受一个指针,仅仅调用析构函数 
    template <class T>  
    inline void destroy(T* pointer) {  
        pointer->~T();  
    }  

    // 以下是 destroy() 第二版本,接受两个迭代器。此函数设法找出元素的数值型别,  
    // 进而利用 __type_traits<> 求取最适当措施  
    template <class ForwardIterator>  
    inline void destroy(ForwardIterator first, ForwardIterator last) {  
      __destroy(first, last, value_type(first));  
    }  

   // 判断元素的数值型别(value type)是否有 trivial destructor  
    template <class ForwardIterator, class T>  
    inline void __destroy(ForwardIterator first, ForwardIterator last, T*) {  
      typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;  
      __destroy_aux(first, last, trivial_destructor());  
    }  

    // 如果元素的数值型别(value type)有 non-trivial destructor  
    template <class ForwardIterator>  
    inline void  
    __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) {  
      for ( ; first < last; ++first)  
        destroy(&*first);  
    }  

    // 如果元素的数值型别(value type)有 trivial destructor  
    template <class ForwardIterator>   
    inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {}  

    // 以下是 destroy() 第二版本针对迭代器为 char * 和 wchar_t * 的特化版  
    inline void destroy(char*, char*) {}  
    inline void destroy(wchar_t*, wchar_t*) {} 

STL标准规定空间配置器必须拥有construct和destory两个函数。上述construct函数的作用是将初值设定到指针所指的空间上,实现上使用了placement new(详见https://blog.csdn.net/d_guco/article/details/54019495)。destory有两个版本,第一个版本接受一个指针,直接调用析构函数进行析构。第二个版本接收两个迭代器,会将它们之间所有的对象进行析构,因为范围可能很大,所以会先判断类型是否为trivial(trivial:不重要的,详见https://blog.csdn.net/WizardtoH/article/details/80767740),如果是不重要则不需要析构也不影响,因此不做析构操作,如果不是trivial类型,则对每个对象进行释放。如何判断类型是否为trivial类型,在后续会介绍。

空间的配置和释放,std::alloc

SGI STL的空间配置和释放,有以下的考虑:

  • 向system heap申请空间。
  • 考虑多线程。
  • 考虑内存不足的情况。
  • 考虑内存碎片的问题。

下面介绍的内容都不考虑多线程的情况,简化问题的复杂度。

C++中申请和释放内容分别为new和delete,对应C语言中的malloc和free,SGI STL中使用了malloc和free。为了解决内存碎片的问题,SGI STL设计了双层配置器,第一级直接使用malloc和free,第二级按照不同情况采用不同策略:

  • 当配置区块超过128bytes时,视之为“足够大”,便调用第一级配置器。
  • 当配置区块小于128bytes时,视之为“过小”,为了降低额外负担,便采用复杂的memory pool整理方式(后面会介绍),而不再求助于第一级配置器。

整个设计究竟只开放第一级配置器,或者是同时开放第二级配置器,取决于 __USE_MALLOC是否被定义。通常情况下未被定义,因此大多数情况只开放了一级配置器。无论 alloc 被定义成第一级还是第二级配置器, SGI 都将 alloc 进行了上层封装,类似于一个转接器,使其配置器的接口符合 STL 规范:

template<class _Tp, class _Alloc>
class simple_alloc {
public:
    static _Tp* allocate(size_t __n)
      { return 0 == __n ? 0 : (_Tp*) _Alloc::allocate(__n * sizeof (_Tp)); }

    static _Tp* allocate(void)
      { return (_Tp*) _Alloc::allocate(sizeof (_Tp)); }

    static void deallocate(_Tp* __p, size_t __n)
      { if (0 != __n) _Alloc::deallocate(__p, __n * sizeof (_Tp)); }

    static void deallocate(_Tp* __p)
      { _Alloc::deallocate(__p, sizeof (_Tp)); }
};

内部的四个成员函数只是单纯的传递给配置器的成员函数,可能是第一级也有可能是第二级,SGI STL 的所有容器全部用该接口,比如我们常用的vector的定义:

template<class T, class Alloc = alloc>        //默认缺省alloc为空间配置器
class vector
{
protected:

    // 使用了simple_alloc,每次配置一个元素大小
    typedef simple_alloc<value_type, Alloc> data_allocator;

    void deallocate()
    {
        if(...)
        {
            data_allocator::deallocate(start, end_of_storage - start);
        }
    }
    //...

};

第一级配置器

#if 0   
#   include <new>   
#   define  __THROW_BAD_ALLOC throw bad_alloc   
#elif !defined(__THROW_BAD_ALLOC)   
#   include <iostream.h>   
#   define  __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1)   
#endif   

// malloc-based allocator. 通常比稍后介绍的 default alloc 速度慢,   
//一般而言是 thread-safe,并且对于空间的运用比较高效(efficient)。   
//以下是第一级配置器。   
//注意,无「template 型别参数」。至于「非型别参数」inst,完全没派上用场。  
template <int inst>     
class __malloc_alloc_template {   

private:   
//以下都是函式指标,所代表的函式将用来处理内存不足的情况。   
// oom : out of memory.   
static void *oom_malloc(size_t);   
static void *oom_realloc(void *, size_t);   
static void (* __malloc_alloc_oom_handler)();   

public:   

static void * allocate(size_t n)   
{   
    void  *result =malloc(n);//第一级配置器直接使用 malloc()   
    // 以下,无法满足需求时,改用 oom_malloc()   
    if (0 == result) result = oom_malloc(n);   
    return  result;   
}   
static void deallocate(void *p, size_t /* n */)   
{   
free(p); //第一级配置器直接使用 free()   
}   

static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)   
{   
    void  *  result  =realloc(p, new_sz);//第一级配置器直接使用 rea  
    // 以下,无法满足需求时,改用 oom_realloc()   
    if (0 == result) result = oom_realloc(p, new_sz);   
    return  result;   
}   

//以下模拟 C++的 set_new_handler(). 换句话说,你可以透过它,   
//指定你自己的 out-of-memory handler   
static void (* set_malloc_handler(void (*f)()))()   
{   
    void  (*  old)()  =  __malloc_alloc_oom_handler;   
__malloc_alloc_oom_handler = f;   
    return(old);   
}   
};   

// malloc_alloc out-of-memory handling   
//初值为 0。有待客端设定。   
template <int inst>   
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;   

template <int inst>   
void * __malloc_alloc_template<inst>::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);   
    }   
}   

template <int inst>   
void * __malloc_alloc_template<inst>::oom_realloc(void *p, 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 = realloc(p, n);//再次尝试配置内存。   
        if  (result)  return(result);   
    }   
}   

//注意,以下直接将参数 inst指定为 0。   
typedef __malloc_alloc_template<0> malloc_alloc;   

以上代码可以看到,第一级配置器实现了类似于new-handler的机制,因为它本身不是使用new和delete来进行空间配置的,所以无法使用C++中的new-handler。可以看到allocote和reallocote都是先直接调用C函数,失败之后才调用oom_alloc和oom_realloc反复尝试,希望能够在某次尝试成功。当然,前提是必须使用set_malloc_handler设置处理函数,否则直接抛出异常。需要注意的是,设计set_malloc_handler处理是客端的责任。关于设计new-handler(内存不足处理例程)的具体做法可参考《Effective C++(第二版)》条款7(详见https://blog.csdn.net/wangqiulin123456/article/details/8253279)。

第二级配置器

第二级配置器比第一级多了一些机制,防止连续小内存空间配置造成的内存碎片问题。SGI STL第二级配器的做法是,当配置的区块大于128bytes时,交给第一级配置器处理;区块小于等于128bytes时,就以内存池的方式进行管理:每次配置一大块内存,以内存链表的方式进行管理。为了方便管理,所有区块的内存都会被调整至8的倍数,并维护16个free-lists,分别为8bytes~128bytes,每个free-lists中都存在相同大小的一些区块。

free-lists节点定义如下:

union obj
{
    union obj* free_list_link;
    char client_data[1];
};

这个结构可以看做是从一个内存块中4个字节大小(32位的情况下),当这个内存块空闲时,它存储了下个空闲块,当这个内存块交付给用户时,它存储的是用户的数据。使用两个成员的原因应该是便于在两种情况下进行切换(猜测),因为如果只使用一个成员就需要进行强制转换,并且使用union也不会占用更多空间。

以下是二级配置器的实现部分:

enum {__ALIGN = 8};  //小型区块的上调边界
enum {__MAX_BYTES = 128};   //小型区块的上界
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};   //free-list个数

 // 无template型参数,且第二个参数没有用上
 // 第一个用于多线程,暂不讨论
template <bool threads, int inst>
class __default_alloc_template {

private:
    /*将bytes上调至8的倍数
    用二进制理解,byte整除align时尾部为0,结果仍为byte;否则尾部肯定有1存在,加上
    align - 1之后定会导致第i位(2^i = align)的进位,再进行&操作即可得到8的倍数
    */
    static size_t ROUND_UP(size_t bytes) {
        return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
    }
private:
    union obj {   //free-list的节点
        union obj * free_list_link;
        char client_data[1];    /* The client sees this.     */
    };

private:
    //16个free-lists
    static obj * __VOLATILE free_list[__NFREELISTS]; 
    //根据区块大小,找到合适的free-list,返回其下标(从0起算)
    static  size_t FREELIST_INDEX(size_t bytes) {
        return (((bytes) + __ALIGN-1)/__ALIGN - 1);
  }

  //返回一个大小为n的对象,并可能编入大小为n的区块到相应的free-list
  static void *refill(size_t n);
  //配置一大块空间,可容纳nobjs个大小为“size”的区块
  //如果配置nobjs个区块有所不便,nobjs可能会降低
  static char *chunk_alloc(size_t size, int &nobjs);

  //Chunk allocation state
  static char *start_free;
  static char *end_free;
  static size_t heap_size;

public:
    // 下面会介绍
    static void * allocate(size_t n); 
    static void * deallocatr(void *p, size_t n);
    static void * reallocate(void *p, size_t old_sz, size_t new_sz);
};

//以下是static data member的定义与初值设定
template <bool threads, int inst>
char * __default_alloc_template<threads, inst>::start_free = 0;

template <bool threads, int inst>
char * __default__alloc_template<threads, inst>::end_free = 0;

template <bool threads, int inst>
size_t __default_alloc_template<threads, inst>::heap_size = 0;

template <bool threads, int inst>
__default_alloc_template<threads, inst>::obj * volatile
__default_alloc_template<threads, inst>::free_list[__NFREELISTS] = 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };

__default_alloc_template的allocate函数

 static void * allocate(size_t n)
  {
    obj * __VOLATILE * my_free_list;
    obj * __RESTRICT result;

    if (n > (size_t) __MAX_BYTES) {
        return(malloc_alloc::allocate(n));
    }

    //如果所开辟的区块的大小小于128bytes
    my_free_list = free_list + FREELIST_INDEX(n);
    result = *my_free_list;
    if (result == 0) {
        // 没有找到可用的free list,重新配置
        void *r = refill(ROUND_UP(n));     // refill会在后续介绍
        return r;
    }
    //如果此时free_list上有空间,则拨出一块空间给对象使用
    *my_free_list = result -> free_list_link;
    return (result);
  };

这里写图片描述

__default_alloc_template的deallocate函数

static void deallocate(void *p, size_t n)
{
    obj *q = (obj *)p;
    obj * __VOLATILE * my_free_list;

    if (n > (size_t) __MAX_BYTES) {
        malloc_alloc::deallocate(p, n);
        return;
    }

    //如果是小内存块的释放,则还是先找到所释放内存块在free_list[]中的位置
    my_free_list = free_list + FREELIST_INDEX(n);
    q -> free_list_link = *my_free_list;

    // 调整free_list上的首地址,即返还给free_list中的这个节点
    *my_free_list = q;
}

这里写图片描述

refill函数重新填充

对于第二级空间配置器从free-lists中获取空间和释放空间的流程已经做了介绍,其中获取空间的过程中会出现所需大小的区块已经被获取完了,这时候需要重新填充这个区块,也就是调用refill函数。

template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
    // 默认填充20个(n字节上调至8的整数倍)的内存块
    int nobjs = 20;

    // 这个函数的作用是尝试取得nobjs个(n字节上调至8的整数倍)的内存块作为free_list的新节点
    // 这里需要注意取得的不一定是20个区块,如果内存池的空间不够,它所获得的区块数目可能小于20个
    char * chunk = chunk_alloc(n, nobjs);
    obj * __VOLATILE * my_free_list;
    obj * result;
    obj * current_obj, * next_obj;
    int i;

    // 如果申请到的区块数目为1,则直接返还给对象使用
    if (1 == nobjs) return(chunk);

    // 如果不为1则找到区块在free_list[]中所对应位置
    my_free_list = free_list + FREELIST_INDEX(n);

    //从头拨出1个申请好的区块在下面返还给对象,把剩余的区块全部链在free_list[FREELIST_INDEX]下面
    result = (obj *)chunk;
    *my_free_list = next_obj = (obj *)(chunk + n);
    for (i = 1; ; i++) {
        current_obj = next_obj;

        //chunck_alloc返回的空间类型为char*
        next_obj = (obj *)((char *)next_obj + n);
        //如果已经链上的节点的个数等于从内存池申请的节点的个数-1,终止循环
        if (nobjs - 1 == i) {
            current_obj -> free_list_link = 0;
            break;
        } else {
            current_obj -> free_list_link = next_obj;
        }
    }

    return(result);
}

内存池

refill函数中使用到的chunk_alloc,就是从内存池中获取空间,以下为具体实现:

template <bool threads, int inst>
char*
__default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs)
{
    char * result;
    size_t total_bytes = size * nobjs;
    size_t bytes_left = end_free - start_free;  // 内存池剩余大小

    if (bytes_left >= total_bytes) {
        // 剩余大小满足申请需求
        result = start_free;
        start_free += total_bytes;
        return(result);
    }
    else if (bytes_left >= size) {
        // 剩余大小不满足申请需求,但是能够供应一个区块以上的大小,也就是refill里说明的获得的区块数目可能小于20个
        nobjs = bytes_left/size;
        total_bytes = size * nobjs;
        result = start_free;
        start_free += total_bytes;
        return(result);
    }
    else {
        // 剩余大小连一个区块都不满足
        size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);

        // 尝试内存池中剩余的小空间分配给合适的free-lists
        if (bytes_left > 0) {
            obj * __VOLATILE * my_free_list = free_list + FREELIST_INDEX(bytes_left);
            ((obj *)start_free) -> free_list_link = *my_free_list;
            *my_free_list = (obj *)start_free;
        }

        // 用malloc给内存池分配空间
        start_free = (char *)malloc(bytes_to_get);
        if (0 == start_free) {
            // 分配失败
            int i;
            obj * __VOLATILE * my_free_list, *p;

            // 在free_lists中查找没有使用过的内存块,并且它足够大
            for (i = size; i <= __MAX_BYTES; i += __ALIGN) {
                my_free_list = free_list + FREELIST_INDEX(i);
                p = *my_free_list;

                // 把free-lists中的内存编入内存池
                if (0 != p) {
                    *my_free_list = p -> free_list_link;
                    start_free = (char *)p;
                    end_free = start_free + i;
                    return(chunk_alloc(size, nobjs));  // 递归调用,剩余的的零头会被重新编入合适的free-lists
                }
            }

        // 如果free_list中也没有内存块了
        end_free = 0;

        // 试着调用一级空间配置器,可能会抛出异常或者申请到内存
        start_free = (char *)malloc_alloc::allocate(bytes_to_get);
        }

        heap_size += bytes_to_get;
        end_free = start_free + bytes_to_get;
        return (chunk_alloc(size, nobjs));
    }
}

内存池的处理主要有一下一些情况:

  • 内存池空间足够,直接分配空间。
  • 内存池空间不满足所有需求,但是能够分配一个区块以上的大小,分配能分配的最大空间。
  • 内存池空间连一个区块都无法满足,尝试使用malloc进行申请,扩展内存池大小然后分配。
  • malloc申请失败,则尝试用第一级配置器,因为其中有针对申请空间失败的处理(new-handler机制),可能能够释放其他的内存来使用,如果失败会抛出异常。

这里写图片描述

内存基础处理工具

STL定义了五个全局函数,其中construct和destroy在上文已经介绍。另外的3个分别是uninitialized_copy、uninitialized_fill、uninitialized_fill_n。

// uninitialized_copy函数模板
template <class InputIterator, class ForwardIterator>
inline ForwardIterator
uninitialized_copy(InputIterator first, InputIterator last,
        ForwardIterator result)
{
    // value_type是个模板函数,萃取迭代器指向类型,用该类型生成临时对象,
    return __uninitialized_copy(first, last, result, value_type(result));

}

uninitialized_copy的作用是把[first, last)之间的所有对象进行复制,并放入result当中。当我们自己实现容器的时候,就可以使用uninitialized_copy,因为容器的全区间构造函数通常使用两个步骤:配置内存区块、使用uninitialized_copy构造。C++标准规定uninitialized_copy具有“commit or rollback”语意,就是说要么构造出所有元素,要么就回滚不构造任何元素。

// uninitialized_fill函数模板
template <class ForwardIterator, class T>
inline void uninitialized_fill(ForwardIterator first, ForwardIterator last,
        const T& x)
{
    __uninitialized_fill(first, last, x, value_type(first));
}

uninitialized_fill能够使我们将内存配置与对象的建构行为分离开。如果[first, last)范围内每个迭代器都指向未初始化的内存,那么uninitialized_fill()会在该范围内产生x的复制品。全部初始化为x。与uninitialized_copy不同,uninitialized_copy是以一段迭代器标记的区块内的元素去初始化自己的未初始化元素,这里是全部初始化为同一个指定的值x。

 // uninitialized_fill_n函数模板
37 template <class ForwardIterator, class Size, class T>
38 inline ForwardIterator uninitialized_fill_n(ForwardIterator first,Size n,
39         const T& x)
40 {
41     return __uninitialized_fill_n(first, n, value_type(first));
42 }

uninitialized_fill_n和uninitialized_fill类似,不过范围是从first到first+n。

以上的三个函数,实际运行时都会先判断类型是否为POD类型(详见https://blog.csdn.net/WizardtoH/article/details/80767740),如果是POD类型就采用最有效的方法进行填充初值,否则采用最保守的方法。以下用uninitialized_fill_n为例:

template <class ForwardIterator, class Size, class T>
inline ForwardIterator
__uninitialized_fill_n_aux(ForwardIterator first, Size n,
        const T& x, __true_type)
{
    // pod类型直接批量填充
    return fill_n(first, n, x);
}

template <class ForwardIterator, class Size, class T>
ForwardIterator
__uninitialized_fill_n_aux(ForwardIterator first, Size n,
        const T& x, __false_type)
{
    // 非POD类型一个一个填充,并捕捉异常
    ForwardIterator cur = first;
    __STL_TRY
    {
        for (; n > 0; --n, ++cur)
            construct(&*cur, x);
        return cur;
    }
    __STL_UNWIND(destroy(first, cur));
}

template <class ForwardIterator, class Size, class T, class T1>
inline ForwardIterator __uninitialized_fill_n(ForwardIterator first,
        Size n, const T& x, T1*)
{
    typedef typename __type_traits<T1>::is_POD_type is_POD;
    return __uninitialized_fill_n_aux(first, n, x, is_POD());
}

template <class ForwardIterator, class Size, class T>
inline ForwardIterator uninitialized_fill_n(ForwardIterator first,Size n,
        const T& x)
{
    return __uninitialized_fill_n(first, n, value_type(first));
}

下一篇:《STL源码剖析》笔记-迭代器iterators

猜你喜欢

转载自blog.csdn.net/WizardtoH/article/details/81946956