STL二级配置器

具备次配置力的SGI空间配置器

SGI STL的配置器与众不同,也与标准规范不同,其名称是alloc而非allocator,而且不接受任何参数。换句话说,如果你要在程序中明白采用SGI配置器,则不能采用标准写法

vector<int, std::allocator<int> > iv;

必须这么写:

vector<int, std::alloc> iv;

SGI STL allocator未能符合标准规格,这个事实通常不会给我们带来困扰,因为通常我们使用缺省的空间配置器,很少需要自行指定配置器名称,而SGI STL的每一个容器都已经指定其缺省的空间配置器alloc

template <class T, class Alloc = alloc>		//缺省使用alloc为配置器
class vector{ ... };

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

class Foo { ... };
Foo* pf = new Foo;		//配置内存,然后构造对象
delete pf;				//将对象析构,然后释放内存

new内含两阶段操作:(1)调用::operator new配置内存;(2)调用Foo::Foo()构造对象内容。

delete内含两阶段操作:(1)调用Foo::~Foo()将对象析构,(2)调用::operator delete释放内存

STL标准规格告诉我们,配置器定义于<memory>中,SGI<memory>内含以下两个文件:

#include <stl_alloc.h>		//负责内存空间的配置和释放
#include <stl_construct.h>	//负责对象内容的构造和析构

在这里插入图片描述

构造和析构基本工具:construct()和destory()

下面是<stl_construct.h>的部分内容

#include <new.h>	//使用placement new

template <class T1, class T2>
inline void construct(T1* p, const T2& value){
    new (p) T1(value);	//placement new 调用T1::T1(value)
}						//将初值设定到指针所指的空间上

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

//以下是destory()的第二版本,接受两个迭代器。此函数设法找出元素的数值型别。
template<class ForwardIterator>
inline void destory(ForwardIterator first, ForwardItertor last){
    __destory(first, last, value_type(first));	//value_type(first)获得迭代器所指对象的型别
}

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

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

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

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

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

对象构造前的空间配置和对象析构后的空间释放,由<stl_alloc.h>负责

C++内存配置基本操作是::operator new(),内存释放基本操作是::operator delete()。这两个全局函数相当于C的malloc()free()函数。SGImalloc()free()完成内存的配置和释放。

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

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

其中__malloc_alloc_template就是第一级配置器,__default_alloc_template就是二级配置器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)); }
};

内部4个成员函数其实都是在单纯的转调用,调用传递给配置器(可能使第一级也可能是第二级)的成员函数。这个接口使配置器的单位从bytes转化为个别元素的大小(sizeof(T))SGI STL容器全都使用这个simple_alloc接口。

template <class T, class Alloc=alloc>	//缺省使用alloc为配置器
class vector{
protected:
    typedef simple_alloc<value_type, Alloc> data_allocator;
    void deallocate(){
        if(...)
            data_allocator::deallocate(start, end_of_storage - start);
    }
};

在这里插入图片描述

在这里插入图片描述


第一级配置器__malloc_alloc_template剖析

#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

//以下是第一级配置器
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)
    {
        free(p);	//第一级配置器直接使用free()
    }
    
    static void* reallocate(void* p, size_t, size_t new_sz)
    {
        void* result = realloc(p, new_sz);	//第一级配置器直接使用realloc()
        //以下无法满足需求时,改用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);
    }
};

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(n);			//再次尝试配置内存
        if(result)
            return(result);
    }
}

第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置,释放,重配置操作,并实现出类似C++ new-handler的机制。是的,它不能直接运用C++ new_handler机制,因为它并没有使用::operator new来配置内存。

所谓C++ new handler机制就是,你可以要求系统在内存配置需求无法被满足时,调用一个你所指定的函数。换句话说,一旦::operator new无法完成任务,在丢出std::bad_malloc异常状态之前,会先调用由客户端指定的处理例程。该处理例程通常被称为new-handlernew-handler解决内存不足的做法有特定的模式。

请注意,SGI第一级配置器的allocate()realloc()都是在调用malloc()realloc()不成功后,改调用oom_malloc()oom_realloc(),后两者都有内循环,不断调用“内存不足处理例程”,期望在某次调用之后,获得足够的内存而圆满完成任务。但如果“内存不足处理例程”并未被客户端设定,oom_malloc()oom_realloc()便调用__THROW_BAD_ALLOC,丢出bad_alloc异常信息,或利用exit(1)中止程序

第二级配置器__default_alloc_template剖析

第二级配置器多了一些机制,避免太多小额区块造成内存的碎片。小额区块带来的其实不仅仅是内存碎片,配置时的额外负担也是一个大问题。

SGI第二级配置器的做法是:如果区块够大,超过128bytes时,就移交第一级配置器处理。当区块小于128bytes时,则以内存池(memory pool)管理,此法又称为次级配置:每次配置一大块内存,并维护对应的自由链表。下次若再有相同大小的内存需求,就直接从free-list中拨出。如果客户端释放小额区块,就由配置器回收到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节点结构

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

在这里插入图片描述

下面是二级配置器的部分实现内容

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:
	union obj{
        union obj* free_list_link;
        char client_data[1];
    };
    
private:
    //16个free-list
    static obj* volatile free_list[__NFREELISTS];
    //以下函数根据区块大小,决定使用第n号free-list,n从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;	//内存池起始位置,只在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 thread, int inst>
char *__default_alloc_template<threads, inst>::start_free = 0;

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

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

template<bool thread, 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()

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

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

在这里插入图片描述

空间释放函数deallocate()

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

//p不可以是0
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 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;
}

内存池

从内存池中取空间给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){
    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);
        //以下试着让内存池中的残余零头还有利用价值
        //把内存池中的残余零头加到free list中
        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;
        }
        
        //配置heap空间,用来补充内存池
        start_free = (char*)malloc(bytes_to_get);
        if(0 == start_free){
            //heap空间不足,malloc失败
            int i;
            obj* volatile* my_free_list, *p;
            //试着检视我们手上拥有的东西。这不会造成伤害,我们不打算尝试配置
            //较小的区块,因为那在多进程机器上容易导致灾难
            //以下搜索适当的free list
            //所谓适当是指“尚有未用区块,且区块够大”之free list
            for(i = size; i <= __MAX_BYTES; i += __ALIGN){
                my_free_list = free_list + FREELIST_INDEX(i);
                p = *my_free_list;
                if(p != 0){		//free list内还有未用的区块
                    //调整free list以释放出未用区块
                    *my_free_list = p->free_list_link;
                    start_free = (char*)p;
                    end_free = start_free + i;
                    //递归调用自己,为了修正nobjs
                    return (chunk_alloc(size, nobjs));
                    //注意,任何残余零头终将被编入适当的free list中备用
                }
            }
            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;
        //递归调用自己,为了修正nobjs
        return (chunk_alloc(size, nobjs));
    }
}

上述的chunk_alloc()函数以end_free - start_free来判断内存池的水量。如果水量充足,就直接调出20个区块返回给free list。如果水量不足以提供20个区块,但还足够供应一个以上的区块,就拨出这不足20个区块的空间出去。这时候其pass by referencenobjs参数将被修改为实际能够供应的区块数。如果内存池连一个区块空间都无法供应,对客户端显然无法交代,此时需要利用mallocheap中配置内存,为内存池注入源头活水以应付需求。新水量的大小为需求量的两倍,再加上一个随配置次数增加而愈来愈大的附加量。

万一山穷水尽,整个system heap空间都不够了(以至无法为内存池提供源头活水),malloc()行动失败,chunk_alloc()就四处寻找有无“尚有未用区块,且区块够大”的free lists。找到了就挖一块交出,找不到就调用第一级配置器。第一级配置器也是使用malloc()来配置内存,但它有out-of-memory处理机制(类似new-handler机制),或许有机会释放其他的内存拿来此处使用。如果可以,就成功,否则发出bad_alloc异常。

在这里插入图片描述

SGI通常以这种方式来使用配置器:

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

其中第二个template参数所接受的缺省参数alloc,可以是第一级配置器,也可以是第二级配置器。

system heap空间都不够了(以至无法为内存池提供源头活水),malloc()行动失败,chunk_alloc()就四处寻找有无“尚有未用区块,且区块够大”的free lists。找到了就挖一块交出,找不到就调用第一级配置器。第一级配置器也是使用malloc()来配置内存,但它有out-of-memory处理机制(类似new-handler机制),或许有机会释放其他的内存拿来此处使用。如果可以,就成功,否则发出bad_alloc异常。

SGI通常以这种方式来使用配置器:

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

其中第二个template参数所接受的缺省参数alloc,可以是第一级配置器,也可以是第二级配置器。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值