STL源码——SGI 空间配置器

来源:互联网 发布:《人工智能ai》 编辑:程序博客网 时间:2024/05/15 23:45

本文主要参考STL源码剖析,但书中对某些地方写的不是很详细,所以根据个人的理解增加了一些细节的说明,便于回顾。

由于小型区块分配时可能造成内存破碎问题,SGI设计了两级配置器,第一级配置器直接使用malloc和free,第二级配置器则视情况采取不同的策略:当配置的区块超过128Bytes时,调用第一级配置器;当配置区块小于128Bytes时,采用复杂的内存池整理方式,而不再求助于第一级配置器。使用第一级配置器还是同时开放第二级配置器,取决于__USE_MALLOC是否被定义。

#ifdef __USE_MALLOC...typedef __malloc_alloc_template<0> malloc_alloc;typedef malloc_alloc alloc;   //令alloc为第一级配置器#else...//令alloc为第二级配置器typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS,0> alloc;#endif

其中__malloc_alloc就是第一级配置器,__default_alloc_template就是第二级配置器

无论alloc被定义为何种配置器,SGI再为之包装一个接口如下,使配置器的接口能符合STL规格:

template<class T, class Alloc>class simple_alloc{public:static T *allocate(size_t n){return {0 == n ? 0 : (T*)Alloc::allocate(n * sizeof(T));}static T *allocate(void){return (T*)Alloc::allocate(sizeof(T));}static void deallocate(T *p, size_t n){if(0 != n) Allocate::deallocate(p, n * sizeof(T));}static void deallocate(T *p){Alloc::deallocate(p, sizeof(T));}}

可以看出,其内部四个成员函数都是单纯的函数调用。SGI STL容器全都使用这个simple_alloc接口(缺省使用alloc为配置器)。

一二级配置器的关系如下(图摘自STL源码剖析)



接口包装及实际运用方式如下(图摘自STL源码剖析):


第二级配置器的设计思想是:每次配置一大块连续内存,并维护其对应的自由链表(free-list,大小相同的区块串接在一起),下次若内存需求,先从free-list中找到对应大小的区块所在的链表,然后直接从该链表拨出一个区块给客户端使用。客户端释放小额区块时,就由配置器回收到free-lists中。为了方便管理,SGI第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数(实际区块 >= 内存需求),并维护16个free-lists,各自管理大小分别为8, 16, 24, 32, 40, 48, 56, 64, 72,  80,88,96,104,112,120,128 字节的小额区块。每个free-lists是一系列大小相同的区块串成的链表,便于分配和回收。free-lists的节点结构如下:

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

插曲:书上对节点如此设计的原因解释如下:不造成内存的浪费(存储额外的链表指针)。

STL源码中使用联合union来设计,并且第二个字段设置为client_data[1],是使用了柔性数组。从第一个字段看,obj可被视为一个指针,指向另一个obj,从第二个地段看,obj可被视为一个大小不定的内存区块(柔性数组),数组长度视分配的内存而定。

柔性数组简单介绍如下:

结构中最后一个元素允许是未知大小的数组(长度为0或者1),这个数组就是柔性数组。但结构中的柔性数组前面必须至少一个其他成员,柔性数组不占用结构体的内存。包含柔数组成员的结构用malloc函数进行内存的动态分配,且分配的内存应该大于结构的大小以适应柔性数组的预期大小,如下一个例子:

Struct Packet{int len;char data[1]; //使用[1]比使用[0]兼容性好};

对于编译器而言,数组名仅仅是一个符号,它不会占用任何空间,它在结构体中,只是代表了一个偏移量。当使用packet存储数据时,使用

char *tmp = (char*)malloc(sizeof(Packet)+1024) 

申请一块连续的内存空间,这块内存空间的长度是Packet的大小加上1024数据的大小。包中的数据存放在data中。

回到正题,这里用柔性数组,主要是用来表示16种不同大小的内存区块(前面提到过的,8,16,24……),在源码中根本没有用到client_data,而obj是在内存配置器内部定义的,用户更是用不上。或许这就是设计者对代码精炼的追求吧。使用union联合体的内存使用方式如下:(union大小为4)


所以使用起来正如书中那样:


第二级配置器部分实现内容如下:

enum {__ALIGN = 8};  //小型区块的上调边界enum {__MAX_BYTES = 128};   //小型区块的上界enum {__NFREELISTS = __MAX_BYTES/__ALIGN};   //free-list个数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-listsstatic 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, };

弄清结点结构之后,先说一下allocate的基本流程,有了大概的了解之后,再进入源码分析。allocate首先判断所需区块的大小,大于128Bytes就调用第一级配置器,小于128Bytes就检查对应的free-list,如果free-list之内有可用的区块,就直接拿来用,否则就将区块大小调至8的倍数,调用refill函数为free-list重新填充空间。

allocate函数如下:

//n must be > 0static void * allocate(size_t n){    obj * __VOLATILE * my_free_list;    obj * __RESTRICT result;    //大于128就调用第一级配置器    if (n > (size_t) __MAX_BYTES) {        return(malloc_alloc::allocate(n));    }    //寻找16个free-lists中适当的一个    my_free_list = free_list + FREELIST_INDEX(n);    result = *my_free_list;    if (result == 0) {    //没找到可用的free-list,准备重新填充free-list        void *r = refill(ROUND_UP(n));        return r;    }    //调整free-list,指向拨出区块的下一个区块    *my_free_list = result -> free_list_link;    return (result);};

refill调用chunk_alloc获取连续的内存空间,然后将这块连续的内存空间编排入相应的free-list中(缺省情况下取得20个区块,若内存池空间不够,获得区块数可能小于20),最后返回这块内存空间的首址。而chunk_alloc负责从内存池中取空间给free-list使用,由于只有这里涉及到了内存池容量的变化,故内存池的起始、结束位置只在chunk_alloc中发生变化。

refill函数如下:

//返回一个大小为n的对象,并且有时候会适当的free-list增加节点//假设n已经适当上调至8的倍数template <bool threads, int inst>void* __default_alloc_template<threads, inst>::refill(size_t n){    int nobjs = 20;    //尝试获得nobjs个区块作为free-list的新节点    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);    //在chunk这段连续内存内建立free-listresult = (obj *)chunk;   //这一块准备返回给客户端//将free-list指向新配置的连续内存空间//allocate中my_free-list为0才进入本函数,故无需存储现在的*my_free-list,直接覆盖即可*my_free_list = next_obj = (obj *)(chunk + n);//将free-list的各节点串接起来     for (i = 1; ; i++) {current_obj = next_obj;next_obj = (obj *)((char *)next_obj + n);  //每一个区块大小为nif (nobjs - 1 == i) {  //最后一块    current_obj -> free_list_link = 0;    break;} else {    current_obj -> free_list_link = next_obj;}}    return(result);}

chunk_alloc取空间的原则如下:尽量从内存池中取,内存池不够了,才使用free-list中的可用区块。具体分三种情况:

①若当前内存池剩余空间完全满足需求,直接从内存池中拨出去,调整内存池起址即可;

②内存池剩余空间不能完全满足,但足以应对一个(含)以上的区块,一个给客户端使用,剩余的编入free-list;③内存池连一个区块的大小都无法提供,由于内存池分配时大小为8的倍数,每次拨出也是8的倍数,故剩余空间也是8的倍数,可以编入一个区块到相应大小的free-list中。此时内存池全部容量已用完。接下来使用heap分配新的内存(由于内存池中的内存要保持连续,否则按区块大小编排free-list也无从谈起,故在使用heap分配内存之前,内存池中的内存要保证全部用完)。

i.若堆空间也不足了,那么从size起,在每一个free-list中寻找可用区块,直到找到可用区块,将该区块归还给内存池,再调用一次chunk_alloc(这次调用一定进入情况①或者②),从而修改调整内存池、nobjs。若free-lists中都没有一个可用区块,则调用第一级配置器,看out-of-memory机制是否有对策。

ii.否则,直接使用堆分配的内存,此时内存池已有足够的空间,再调用一次chunk_alloc,调整nobjs。

chunk_alloc函数代码如下:

//size此时已适当上调至8的倍数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;   //8的倍数    size_t bytes_left = end_free - start_free;  //8的倍数    if (bytes_left >= total_bytes) {  //情况1    //内存池剩余空间完全满足需求量        result = start_free;        start_free += total_bytes;        return(result);    } else if (bytes_left >= size) {  //情况2    //虽不足以完全满足,但足够供应一个(含)以上的区块    //从start_free开始一共total_bytes分配出去,其中前size个bytes给客户端,剩余的给free-list        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);        // 以下尝试将内存池中的残余零头分配完        if (bytes_left > 0) {            obj * __VOLATILE * my_free_list =                        free_list + FREELIST_INDEX(bytes_left); //找到大小相同区块所在的free-list            ((obj *)start_free) -> free_list_link = *my_free_list;  //将内存池剩余空间编入free-list中            *my_free_list = (obj *)start_free;        }        //此时内存池的空间已用完        //配置heap空间,用来补充内存池        start_free = (char *)malloc(bytes_to_get);        if (0 == start_free) {        //heap空间不足,malloc失败            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;                if (0 != p) {   //free-list尚有可用区块                //调整free-list以释出可用区块                    *my_free_list = p -> free_list_link;                    start_free = (char *)p;   //将改区块归还到内存池                    end_free = start_free + i;                    //再次从内存池中索要连续空间来满足客户端需求                    return(chunk_alloc(size, nobjs));  //由于此时i >= size,故此次只会进入情况1/2                }            }    end_free = 0;//没有可用区块归还到内存池,内存池仍为空    //调用第一级配置器,看out-of-memory机制是否能改善            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));    }}

以上就是SGI 空间配置器的内存分配机制。

0 0
原创粉丝点击