c++ stl allocator(具备次配置力sub-allocation)


在这里插入图片描述

1.1 alloc(具备次配置力sub-allocation的sgi空间配置器)

sgi stl的每一个容器都已经指定其缺省的空间配置器为alloc。例如:

template<class T,class Alloc = alloc>
class vector
{ 
    ... 
};

1.2 sgi特殊的空间配置器std::alloc

内存配置操作由alloc::allocate()负责;
内存释放操作由alloc::deallocate()负责;
对象构造操作由::construct()负责;
对象析构操作由::destroy()负责。
在这里插入图片描述

1.3 构造和析构基本工具construct()和destroy()

#include <new.h>
1.3.1 construct()
template <class T1,class T2>
inline void construct(T1* p,const T2& value)
{
    new(p) T1(value); // placement new;调用T1::T1(value);
}

construct()接受一个指针p和一个初值value,该函数的用途就是将初值设定在指针所指的空间上。c++的placement new运算子可用来完成这一任务。

1.3.2 destroy()
template <class T>
inline void destroy(T* pointer)
{
    pointer->~T(); // 调用 dtor ~T();
}

template <class ForwardIterator>
inline void destroy(ForwardIterator frist,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(first,last,trivial_destructor());
}

(1)如果元素的数值类别(value type)有non-trivial destructor…

template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator,ForwardIterator,__false_type)
{
    for(;first < last;++first)
        destroy(&*first);
}

(2)如果元素的数值类别(value type)有trivial destructor…

template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator,ForwardIterator,__true_type)
{

}

destroy()有两个版本,第一版本接受一个指针,准备将该指针所指之物析构掉。这很简单,直接调用该对象的析构函数即可。第二版本接受first和last两个迭代器,准备将[first,last)范围内的所有对象析构掉。我们不知道这个范围有多大,万一很大,而每个对象的析构函数都无关痛痒(trivial destructor),那么一次次调用这些无关痛痒的析构函数,对效率是一种伤害。因此,这里首先利用value_type()获得迭代器所指对象的型别再利用__type_traits判断该型别的构造函数是否无关痛痒。若是(__true_type),则什么也不做就结束;若否(__false_type),这才以循环方式巡访整个范围,并在循环中每经历一个对象就调用一个版本的destroy()。

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

对象构造前的空间配置和对象析构后的空间释放,由<stl_alloc.h>负责,sgi对此对的设计哲学如下:
  ·向system heap要求空间。
  ·考虑多线程(multi-threads)状态。
  ·考虑内存不足时的应变措施。
  ·考虑过多“小型区块”可能造成的内存碎片(fragment)问题。

sgi正是以malloc()和free()完成内存的配置与释放。考虑到小型区块所可能造成的内存破碎问题,sgi设计了双层级配置器,第一级直接使用了malloc()和free(),第二级配置器则视情况采用不同的策略:当配置区块超过128bytes时,视之为“足够大”、便调用第一级配置器;当配置区块小于128bytes时,视之为“过小”,为了降低额外负担,便采用复杂的memory pool整理方式,而不再求助于第一级配置器。
在这里插入图片描述 其中__malloc_alloc_template就是第一级配置器,__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) Alloc::deallocate(p,n * sizeof(T)); 
	}
	static void deallocate(T* p)
	{ 
	    Alloc::deallocate(p,sizeof(T)); 
	}
};

其内部四个成员函数其实都是单纯的转调用,调用传递给配置器(可能是第一级也可能是第二级)的函数成员。这个接口使配置器的配置单位从bytes转为个别元素的大小(sizeof(T))。sgi stl容器全部都使用这个simple_alloc接口,例如:
在这里插入图片描述在这里插入图片描述在这里插入图片描述

1.5 第一级配置器 __malloc_alloc_template剖析

// 注意,无“template型别参数”。

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

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

	// 可以通过它指定你自己的out-of-memory handler
    static void (* set_malloc_handler(void (*f) ())) ()
	{
        void (* old) () = __malloc_alloc_oom_handler;
        __malloc_alloc_oom_handler = f;
        return (old);				
	}

// 初值为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);
	}
}
typedef __malloc_alloc_template<0> malloc_alloc;

第一级配置器以malloc(),free(),realloc()等c函数执行实际的内存配置、释放、重配置操作,并实现出类似C++ new-handler的机制。sgi第一级配置器的allocate()和reallocate()都是在调用malloc()和realloc()不成功后,改调用oom_malloc()和oom_realloc()。后两者都有内循环,不断调用“内存不足处理例程”,期望在某次调用之后,获得足够的内存而圆满完成任务。但如果“内存不足处理例程”并未被客端设定,oom_malloc()和oom_realloc()便老实不客气地调__THROW_BAD_ALLOC,丢出bad_alloc异常信息,或利用exit(1)硬生生中止程序。

1.6 第二级配置器 __default_alloc_template剖析

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

sgi第二级配置器的做法是,如果区块够大,超过128bytes时,就移交第一级配置器处理。当区块小于128bytes时,则以内存池(memory pool)管理,此法又称为次层配置(sub-allocation):每次配置一大块内存,并维护对应之自由链表(free-list)。下次若再有相同大小的内存需求,就直接从free-lists中拨出。如何客端释还小额区块,就由配置器回收到free-lists中–是的,配置器除了负责配置,也负责回收。为了方便管理,sgi第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数(例如客端要求30bytes,就自动调整为32bytes),并维护16个free-lists,各自管理大小分别为
8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128bytes的小额区块。free-lists的节点结构如下:
在这里插入图片描述在这里插入图片描述

enum {__ALIGN = 8};					        // 小型区块的上调边界
enum {__MAX_BYTES = 128};	                // 小型区块的上限
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};  // free-lists个数
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));
    }
private:
    // free-lists的节点构造
    union obj
    {
	    union obj* free_list_link;
	    char client_date[1];
	}
private:
    // 16个free-lists
    static obj* volatile free_list[__NFREELISTS];
    // 以下函数根据区块大小,决定使用第n号free-list。n从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);
    static char* start_free;  // 内存池起始位置。
    static char* end_free;    // 内存池结束位置。
    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);
};
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};
1.6.1空间配置函数allocate()

身为一个配置器,__default_alloc_template拥有配置器的标准接口函数allocate()。此函数首先判断区块大小,大于128bytes就调用第一级配置器,小于128bytes就检查对应的free list。如果free-list之内有可用的区块,就直接拿出来用,如果没有可用区块,就将区块大小上调至8倍数边界,然后调用refill(),准备为free-list重新填充空间。refill()将于稍后介绍。

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中适当的一个
    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);
}

在这里插入图片描述

1.6.2 空间释放函数deallocate()

身为一个配置器,__default_alloc_template拥有配置器标准接口函数deallocate()。该函数首先判断区块大小,大于128bytes就调用第一级配置器,小于128bytes就找出对应的free list,将区块回收。

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

在这里插入图片描述

1.6.3 重新填充free lists

回头讨论先前说过的allocate()。当它发现free list中没有可用区块了时,就调用refill(),准备为free list重新填充空间。新的空间将取自内存池(chunk_alloc()完成)。缺省取得20个新节点(新区块),但万一内存池空间不足,获得的节点数可能小于20:
// 返回一个大小为n的对象,并且有时候会为适当的free list增加节点
// 假设n已经适当上调至8的倍数

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
    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指向新配置的空间
    *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);
        if(nobjs - 1 == i)
        {
            current_obj->free_list_link = 0;
            break;	
        }
        else
        {
            current_obj->free_list_link = next_obj;
        }	
    }
    
    return (result);
}
1.6.4 内存池(memory pool)

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

上述的chunk_alloc()函数以end_free - start_free来判断内存池的水量。(1)如果水量充足,就直接调出20个区块返回给free list。(2)如果水量不足以提供20个区块,但还足够供应一个以上的区块,就拔出这不足20个区块的空间出去。这时候其pass by reference的nobjs参数将被修改为实际能够供应的区块数。如果内存池连一个区块空间都无法供应,对客端显然无法交待,(3)此时需利用malloc从heap中配置内存,为内存池注入活水源头以应付需求。新水量的大小为需求量的两倍,再加上一个随着配置次而数增加而愈大的附加量。(4)万一山穷水尽,整个system heap空间都不够了,malloc()行动失败,chunk_alloc()就四处寻找有无“尚有未用区块,且区块够大”之free lists。就找到了一块交出,(5)找不到就调用第一级配置器。第一级配置器其实也是使用malloc()来配置内存,但它有out-of-memory处理机制,或许有机会释放其他的内存拿来此处使用。如果可以,就成功,否则发出bad_alloc异常。
在这里插入图片描述sgi容器通常以这种方式来使用配置器:

template <class T,class Alloc = alloc>
class vector
{
public:
    typedef T value_type;
    ... ...
protected:
    // 专属之空间配置器,每次配置一个元素大小
    typedef simple_alloc<value_type,Alloc> data_allocator;
    ... ...
};

其中第二个template参数所接受的缺省参数alloc,可以是第一级配置器,也可以是第二级配置器。不过,sgi stl已经把它设为配置器第二级。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值