STL源码——SGI 空间配置器

本文主要参考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-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, };

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

allocate函数如下:

//n must be > 0
static 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-list
	result = (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);  //每一个区块大小为n
		if (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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值