SGI-STL学习笔记之allocator

    技术2022-05-19  22

    空间配置器的标准接口:

    allocator::value_type allocator::pointer allocator::const_pointer allocator::reference allocator::const_refrence allocator::size_type allocator::difference_type pointer allocator::allocate(size_type n,const void*=0) //配置空间,足以存储n个T对象,第二个参数是个提示。实现上可能会力利用它来增进区域性,或完全忽略之 void allocator::deallocate(pointer p,size_type n) //归还先前配置的空间 size_type allocator::max_size()const //返回可成功配置的最大量 void allocator::construct(pointer p,const T& x) //等同于new((void*)p)T(x) void allocator::destroy(pointer p) //等同于p->~T()

     

    SGI空间配置器

     

    SGI的配置器与众不同,其名称为alloc而非allocator,而且不接受任何参数

    标准的写法:

    vector<int ,std::allocator> iv;

    SGI的写法:

    vector<int ,std::alloc> iv;

    SGI STL的每一个容器都已经指定其缺省的空间配置器为alloc

    SGI特殊的空间配置器,std::alloc

    一般而言,我们习惯使用的C++内存配置操作和释放操作为:

    class Foo{…};

    Foo* pf=new Foo;  //配置空间,构造对象

    delete pf;                          //析构对象,释放内存

    其中,new算式包含两个阶段:(1)调用::operator new 配置内存;(2)调用Foo::Foo()构造对象。delete算式类似。

    为了精密分工,STL allocator将两个阶段操作区分开来。内存的配置操作由alloc::allocate()负责,内存释放操作由alloc::deallocate()负责;对象的构造操作由::construct()负责,对象的析构操作由::destroy()负责

    配置器定于与<memory>中,SGI<memory>内包含以下两个文件:

    #include <stl_alloc.h>         //负责内存空间的配置与释放

    #include <stl_construct.h>     //负责对象内容的构造与析构

     

     

    1.SGI STL配置器相关函数的文件分布

    构造与析构工具construct()destroy()

    两函数均被设计为全局函数。

    其中,construct()接收一个指针p和一个初值value,该函数的用途是将初值设定到指针所指的空间上,C++placement new算子可完成此任务

    destroy()有两个版本,第一个版本接受一个指针,将该指针所指对象析构。第二个版本,接受一个范围。

    空间的配置与释放std::alloc

    考虑到小型区域块可能造成的内存破碎问题,SGI STL 设计了双层级配置器。第一级配置器直接使用malloc() free(),第二级配置器则视情况采用不同的策略:当配置区域大小超过128 bytes时,视之为足够大,便调用第一级配置器;当配置区域小于128 bytes时,视之为过小,为了降低额外的负担,便采用复杂的memory pool整理方式,而不再求助于第一级配置器。

    __malloc_alloc_template就是第一级 配 置 器 , __default_alloc_template 就是第级配置器。再次注意,alloc并不接受任何template型别参数。

    无论alloc被定义为第级或第二级配置器,SGI 还为它再包装个接口:simple_allocSGI STL 容器全部使用这个simple_alloc接口。

     

     

    第二级配置器__default_alloc_template剖析

    第二级配置器多了一些机制,避免太多的小额区块造成的内存碎片。小额区块带来的其实不仅仅是内存碎片,配置时的系统开销也是一大问题,系统开销永远无法避免,毕竟系统要靠这些空间管理内存,如下图所示,区块愈小,系统开销占据的比例就愈大,愈显得浪费。

    次层配置(sub-allocation):每次配置一大块内存,并维护对应之自由链表(free-list),下次若再有相同大小的内存需求,就直接从free-list中拔出。如果客户端释还小额区块,就有配置器回收到free-list。为了方便管理,SGI第二级配置器会主动将任何小额区块的内存需求量上调为8的倍数,并维护16free-list,各自管理大小分别为,8, 16, 24, 32, 40, 48, 56, 64, 72,

    80, 88, 96, 104, 112, 120, 128 bytes 的小额区块。。free-lists 的节点结构如

    union obj { union obj * free_list_link; char client_data[1]; //The client sees this. };

    注意,上述 obj 所用的是 union,由于 union 之故,从其第一字段观之,obj可被视为一个指标,指向相同形式的另 obj。从其第字段观之,obj 可被视为一个指标,指向实际区块。一物二用的结果是,不会为了维护串行所必须的指针而造成内存的另种浪费。

    代码剖析   1、类模板声明:                                                               

    enum {__ALIGN = 8}; // 最小区块大小

    enum {__MAX_BYTES = 128}; // 最大区块大小

    enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; // free-lists 个数

    //级配置器。

    u  template <bool threads, int inst>

    class __default_alloc_template {

    private:

    // ROUND_UP() bytes调至 8 的倍数。

    Ø  static size_t ROUND_UP(size_t bytes) {

    return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));

    }

    /*~(__ALIGN - 1)得到后三位为0的位串

    *(bytes) + __ALIGN-1利用四舍五入的相同的实现方法,实现8”,然后与使用&操作舍去不足*8的部分,例如:

    *(18+7)& ~(__ALIGN - 1)---->(10010+00111)&(111000)-----> (1 1001)&(11000) ----->11000=24

    */

    private:

    Ø  union obj {   // free-lists 的节点构造

    union obj * free_list_link;

    char client_data[1]; /* The client sees this. */

    };

    private:

    //注意:free_list类型为obj类型的指针,所以其将指向每一个free-list的首地址

    Ø  static obj * volatile free_list[__NFREELISTS]; // 16 free-lists

    //根据区块大小,决定使用第 n free-listn 1 起算。

    Ø  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; // 记忆池起始位置。只在 chunk_alloc()变化

    Ø  static char *end_free; // 记忆池结束位置。只在 chunk_alloc()变化

    Ø  static size_t heap_size; //已分配堆的总大小

    public:

    Ø  static void * allocate(size_t n) { /* 详述于后 */ }

    Ø  static void deallocate(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, };

    代码剖析  2、空间配置函数allocate()的实现

    // n must be > 0

    static void * allocate(size_t n)

    {

    obj * volatile * my_free_list;

    obj * result;

    // 大于 128 就呼叫第级配置器

    if (n > (size_t) __MAX_BYTES)

    return(malloc_alloc::allocate(n));

    }

    // 寻找 16 free lists 区块大小为8n的链表

    my_free_list = free_list + FREELIST_INDEX(n);//free lists中获取链表首位置

    result = *my_free_list;         //取得该链表第一区块的首地址,

    if (result == 0) {// 没找到可用的 free list,准备重新填充 free list

    void *r = refill(ROUND_UP(n));

    return r;

    }

    // 调整 free list将该free list的首地址更新为第二区块的首地址

    *my_free_list = result -> free_list_link;

    return (result);

    };

     

    代码剖析  2、空间释放函数deallocate()的实现

    //备注:区块可以直接释还的主要原因是:空间配置释放与对象构造析构的分离,在调用此函数之前对象已经析构,所以可以释还

    // p 指向要释还的区块的指针

    static void deallocate(void *p, size_t n)

    {

    obj *q = (obj *)p;

    obj * volatile * my_free_list;

    // 大于 128 就呼叫第级配置器

    if (n > (size_t) __MAX_BYTES) {

    malloc_alloc::deallocate(p, n);

    return;

    }

    // 寻找对应的 free list

    my_free_list = free_list + FREELIST_INDEX(n);

    // 调整 free list,回收区块

    q -> free_list_link = *my_free_list; //将旧的第一区块的地址赋值给释还的区块

    *my_free_list = q;  //释还区块将首地址赋值给相应的free-list并成为该free-list新第一区块

    }

    代码剖析  3、重新填充函数free lists

    allocate()中如果发现所需区块大小的free-list中没有区块可用,则会调用refill()为该free-list重新填充空间。所需内存来自memory pool(chunk_alloc() 完成)

    // 传回个大小为 n 的对象,并且有时候会为适当的 free list 增加节点.

    // 假设 n 已经适当调至 8 的倍数。

    //注意:n为每个对象的大小

    template <bool threads, int inst>

    void* __default_alloc_template<threads, inst>::refill(size_t n)

    {

    int nobjs = 20;

    // 呼叫 chunk_alloc(),尝试取得 nobjs 个区块做为 free list 的新节点。

    // 注意参数 nobjs pass by reference,函数调用后nobjs将传回实际取得的区块个数

    char * chunk = chunk_alloc(n, nobjs);

    obj * volatile * my_free_list;

    obj * result;

    obj * current_obj, * next_obj;

    int  i;

    // 如果只获得个区块,这个区块就拨给呼叫者用,free list 无新节点。

    if (1 == nobjs) return(chunk);

    // 否则准备调整 free list,纳入新节点。

    my_free_list = free_list + FREELIST_INDEX(n);/*my_free_list将获取到区块大小为8nfree-list首地址*/

    // chunk 空间内建立 free list

    result = (obj *)chunk; // 块准备传回给客端

    //导引 free list 指向新配置的空间(取自记忆池)

    *my_free_list = next_obj = (obj *)(chunk + n);

    // free list 的各节点串接起来。

    for (i = 1; ; i++) { // 1 开始,因为第 0 个将传回给客端

    current_obj = next_obj;

    next_obj = (obj *)((char *)next_obj + n);//

    if (nobjs - 1 == i) {

    current_obj -> free_list_link = 0;

    break;

    } else {

    current_obj -> free_list_link = next_obj;

    }

    }

    return(result);

    }

    代码剖析  4、内存池(memory pool)的实现

    从内存池取空间给 free list 使用,是 chunk_alloc() 的工作:

    // 假设 size 已经适当调至 8 的倍数。

    // 注意参数 nobjs pass by reference

    template <bool threads, int inst>

    char*

    __default_alloc_template<threads, inst>::

    chunk_alloc(size_t size, int& nobjs)

    {

    Ø  //1.在剩余空间中寻找适当的内存

    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) {

    // 记忆池剩余空间不能完全满足需求量,但足够供应个(含)以的区块。

    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);

    Ø  //2.找不到合适的内存,处理内存池中的残余零头

    // 试着让内存池的残余零头还有利用价值。

    if (bytes_left > 0) {// 内存池内还有些零头,先配给适当的 free list

    // 首先寻找适当的 free list

    obj * volatile * my_free_list =free_list + FREELIST_INDEX(bytes_left);

    // 调整 free list,将记忆池的残余空间编入。

    ((obj *)start_free) -> free_list_link = *my_free_list;

    *my_free_list = (obj *)start_free;

    }

    Ø  //3.heap中寻找空间补充内存池

    // 配置 heap 空间,用来补充内存池。

    start_free = (char *)malloc(bytes_to_get);

    if (0 == start_free) {

    // heap 空间不足,malloc() 失败。

    int i;

    obj * volatile * my_free_list, *p;

    Ø  /* 4.配置heap空间失败,尝试从区块比要求的请求区块大的free-list中查找是否尚有未用区块*/

    for (i = size; i <= __MAX_BYTES; i += __ALIGN) {

    my_free_list = free_list + FREELIST_INDEX(i);

    p = *my_free_list;

    if (0 != p) { // free list 内尚有未用区块。

    // 调整 free list 以释出未用区块

    *my_free_list = p -> free_list_link;

    Ø  //5.找到内存块,调整start_freeend_free指针,递归处理找到的内存补充free-list

    start_free = (char *)p; 

    end_free = start_free + i;

    // 递归呼叫自己,为了修正 nobjs

    return(chunk_alloc(size, nobjs));

    // 注意,因为2.任何残余零头终将被编入适当的 free-list备用。

    }

    }

    Ø  // 6.如果出现意外(山穷水尽,到处都没内存可用了)

    end_free = 0;

    // 呼叫第级配置器,看看 out-of-memory 机制能否尽点力

    start_free = (char *)malloc_alloc::allocate(bytes_to_get);

    // 这会导致掷出异常(exception),或内存不足的情况获得改善。

    }

    Ø  /*对应3.配置heap成功,修改heap_size大小,end_free指针,递归调用处理内存,补充free-list*/

    heap_size += bytes_to_get;

    end_free = start_free + bytes_to_get;

    // 递归呼叫自己,为了修正 nobjs

    return(chunk_alloc(size, nobjs));

    }

    }

     

    Andy编辑于2011-03-05 15:23:41


    最新回复(0)