SGI STL 之空间配置器

一 .SGI STL之空间配置器

  身为C++标准库最重要的组成部分,STL(标准模板库)不仅是一个可复用组件库,而且是一个包罗万象算法与数据结构的软件框架.STL实现版本多样,而SGI版本不论在技术层次,源代码组织,源代码可读性上,均有卓越的表现.
  以STL的运用角度而言,空间配置器是最不需要介绍的东西,它总是隐藏在一切组件的背后,默默工作.但从STL的实现角度而言,空间配置器又是极其重要的,因为整个STL的操作对象都放在容器之内,而容器一定需要配置空间以置放资料.
    并且阅读和剖析名家代码是提高编程水平的捷径.源码之前,了无秘密.大师们的缜密思维,经验结晶,技术思路,独到风格,都原原本本体现在源码之中.在你仔细推敲之中,迷惑不解之时,恍然大悟之际,你的经验,思维,视野,知识乃至技术品味都会获得快速的成长…


二.双层级配置器简介

  SGI设计了双层级配置器,第一级配置器直接使用malloc()和free(),第二级配置器则是情况采用不同的策略:当配置区块超过128bytes时,视之为"过小",为了降低额外负担,便采用复杂的内存池(memory pool)整理方式,而不再求助于第一级配置器. 一二级配置器的关系,及实际运用方式,见下图:
SGI STL 之空间配置器_第1张图片


1.第一级配置器

  第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置,释放,重新配置操作,并实现出类似C++ new_handler的机制.它不能直接运用C++ new_handler机制,因为它并非使用::operator new 来配置内存.所谓C++ new handler机制是,你可以要求系统在内配置需求无法被满足时,调用一个你所指定的函数.换句话说,一旦::operator new 无法完成任务,在丢出std::bad_alloc异常状态之前,会先调用由客端指定的处理例程.该例程通常即被称为new_handler. new_handler 解决内存不足的做法有特定的模式,可参考<<Effective C++>>2e 条款7.
  SGI第一级配置器的allocate()和realloc()都是在调用malloc和realloc()不成功后,该调用oom_malloc()和oom_realloc().后两者都有内循环,不断调用"内存不足处理例程",期望在某次调用之后,获得足够的内存而圆满完成任务.但如果"内存不足处理例程”并未被客端设定,oom_malloc()和oom_realloc()便调用__THROW_BAD_ALLOC, 丢出bad_alloc异常信息,或利用exit(1)硬生生中止程序.


2.第二级配置器

  第二级配置器多了一些机制,避免太多小额区块造成内存的碎片.小额区块带来的其实不仅是内存碎片,配置时的额外负担也是一个大问题.额外负担永远无法避免,毕竟系统要靠这多出来的空间来管理内存.若区块越小,额外负担所占的比例就越大,越显得浪费.
  SGI第二级配置器的做法是,如果区块够大,超过128bytes时,就移交第一级配置器处理.当区块小于128bytes时,则以内存池(memory pool)管理.此法又称为次层配置:每次配置一大块内存,并维护对应之自由链表(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的小额区块.

refill()函数

  当调用allocate()函数申请空间时,它会从free_list中寻找内存,当free_list中没有可用区块时,就调用refill(),准备为free_list重新填充空间,新的空间将取自内存池(经由chunk_alloc()完成).refill函数会接收一个额外的返回值(传引用的nobjs), 函数根据这个值做出不同的响应.
  

chunk_alloc()函数

  从内存池中取空间给free_list使用是chunk_alloc()函数的工作: chunk_alloc()函数以end_free - start_free来判断内存池的水量.如果水量充足,就直接调出20个区块返回给free_list.如果水量不足以提供20个区块,但还足够供应一个以上的区块,就拨出这不足20个区块的空间出去.这时候器传引用的njobs参数将被修改为实际能供应的区块数.如果内存池连一个区块空间都无法供应,对客端显然无法交待,此时便需要利用malloc()从heap中配置内存,为内存池注入活水源头以应付需求.新水量的大小为需求量的两倍,再加上一个随着配置次数增加而越来越大的附加量.
  举个例子,假设程序一开始,客端就调用chunk_alloc(32,20),于是malloc()配置40个32bytes区块,其中第一个交出,另19个交给free_list[3]维护,余20个留给内存池.接下来客端调用chunk_alloc(64, 20),此时free_list[7]空空如也,必须想内存池要求支持.内存池只够供应(32*20)/64=10个64bytes区块,就把这10个区块返回,第一个交给客端,余下9个由free_list[7]维护.此时内存池全空.接下来在调用chunk_alloc(96,20),此时free_list[11]空空如也,必须向内存池要求支持,而内存池此时也是空的,于是以malloc()配置40+n(附加量)个96bytes区块,其中第1个交出,另19个交给free_list[11]维护,余20+n(附加量)个区块留给内存池…
  万一山穷水尽,整个system heap空间都不够了(以至于无法为内存池诸如活水源头),malloc()行动失败,chunk_alloc()就四处寻找有无"尚有未用区块,且区块够大"之free_lists.找到了就挖一块交出,找不到调用第一级配置器.第一级配置器其实也是使用malloc()来配置内存,但它有out_of_memory处理机制(类似new_handler机制),或许有机会释放其他的内存拿来此处使用.如果可以,就成功,否则就发出异常.
以上便是整个第二级空间配置器的设计.


三.代码及测试

stl_alloc.h

#if 1
#include
#include
#include
using namespace std;
//#define __THROW_BAD_ALLOC   throw   bad_alloc
#define __THROW_BAD_ALLOC  cerr<<"Throw bad alloc, Out Of Memory."<
#elif  !defined  (__THROW_BAD_ALLOC)
#include
#define __THROW_BAD_ALLOC   cerr<<"out of memory"<
#endif

//一级配置器
template<int inst>
class __malloc_alloc_template
{
//oom : out of memory
private:
    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()
    //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()
    //realloc()无法满足需求时,改用oom_realloc()
        if(0 == result)
            oom_realloc(p,new_sz);
        return result;
    }
public:
    //set_new_handler(Out_Of_Memory);
    //以下仿真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);
    }
};

//初值为0,有待客端设定
template<int inst>
void (*__malloc_alloc_template::__malloc_alloc_oom_handler)() = 0;

template<int inst>
void* __malloc_alloc_template::oom_malloc(size_t n)
{
    void *result;
    void(* my_malloc_handler)();

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

/////////////////////////////////////////////////////////////////////////////////////
//二级配置器
enum {__ALIGN = 8}; //小型区块的上调边界
enum {__MAX_BYTES  = 128}; //小型区块的上限
enum {__NFREELISTS = __MAX_BYTES / __ALIGN}; //free_lists个数

template<bool threads, int inst>
class __default_alloc_template
{
public:
    static void* allocate(size_t n);
    static void  deallocate(void *p, size_t n);
    static void* reallocate(void *p, size_t, size_t new_sz);
private:
    static size_t  ROUND_UP(size_t bytes)
    {//将bytes上调至8的倍数
        return (((bytes) + __ALIGN-1) & ~(__ALIGN-1));
    }
private:
    //free_lists的节点结构
    union obj
    {
        union obj * free_list_link;
        char client_data[1];
    };
private:
    static obj* volatile free_list[__NFREELISTS]; //16个free_lists
    static size_t FREELIST_INDEX(size_t bytes)
    {//根据区块大小,决定使用第n号的free_lists
        return ((bytes)+__ALIGN-1) / __ALIGN - 1;
    }
private:
    static char *start_free; //内存池起始位置,只在chunk_alloc()中变化
    static char *end_free;   //内存池结束位置,只在chunk_alloc()中变化
    static size_t heap_size;
    static void *refill(size_t n); //返回一个大小为n的对象,并可能加入大小为n的其它区块到free_list
    static char* chunk_alloc(size_t size, int &nobjs); //配置一大块空间,可容纳njobs个大小为"size"的区块
};

//以下是static data member的定义与初始值设定
template<bool threads, int inst>
char* __default_alloc_template::start_free = 0;
template<bool threads, int inst>
char* __default_alloc_template::end_free = 0;
template<bool threads, int inst>
size_t __default_alloc_template::heap_size = 0;
template<bool threads, int inst>
typename __default_alloc_template::obj* volatile
__default_alloc_template::free_list[__NFREELISTS] = 
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};

template<bool threads, int inst>
void* __default_alloc_template::allocate(size_t n)
{
    obj * volatile *my_free_list;
    obj *result;

    //大于128就调用第一级配置器
    if(n > __MAX_BYTES)
    {
        return malloc_alloc::allocate(n);
    }

    my_free_list = free_list + FREELIST_INDEX(n); //寻找16个free_lists中合适的一个
    result = *my_free_list;

    if(result == 0)
    {//没有找到可用的free_list,准备重新填充free_list
        void *r = refill(ROUND_UP(n));
        return r;
    }

    *my_free_list = result->free_list_link; //调整free_list
    return result; 
}


template<bool threads, int inst>
void* __default_alloc_template::refill(size_t n)
{
    int nobjs = 20; //申请20*2个块,一半挂在自由链表,一半用作内存池(20为一个经验值)
    char *chunk = chunk_alloc(n, nobjs); //调用chunk_alloc(),尝试取得nobjs个区块作为free_list的新节点(njobs为传引用)
    obj * volatile *my_free_list; //my_free_list为二级指针,volatile为一个类型修饰符,与线程相关

    obj *result;
    obj *current_obj, *next_obj;
    int i;

    if(1 == nobjs) //如果只获得一个区块,这个区块就分配给调用者,free_list无新节点
        return chunk;

    //否则准备调整free_list,并纳入新节点
    my_free_list = free_list + FREELIST_INDEX(n);
    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);
}

//注意nobjs传引用,假设size已经上调至8的倍数
template<bool threads, int inst>
char* __default_alloc_template::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);

    //以下尝试这让内存池中的残余零头还有利用价值
        if(bytes_left > 0)
        {
        //内存池内还有一些零头,先配给适当的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)
        {
            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(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); //递归调用自己,为了修正nobjs
                }
            }
        //如果出现意外(山穷水尽,到处都没内存可用了),调用第一级配置器,看看out_of_memory机制能否尽点力
            end_free = 0;
            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); //递归调用自己,为了修正nobjs
    }
}

template<bool threads, int inst>
void __default_alloc_template::deallocate(void *p, size_t n)
{
    obj *q = (obj *)p;
    obj * volatile * my_free_list;

    if (n > (size_t) __MAX_BYTES)
    {//大于128就调用第一级配置器
        malloc_alloc::deallocate(p, n);
        return;
    }

    my_free_list = free_list + FREELIST_INDEX(n); //寻找对应的free_list

    //调整free_list,回收区块
    q->free_list_link = *my_free_list;
    *my_free_list = q;
}

memory

#include"stl_alloc.h"

test.cpp

#include
#include
#include "memory"
using namespace std;

void Out_Of_Memory()
{
    cout<<"Out Of Memory."<exit(1);
}

int main()
{
        void (*pfun)() = __malloc_alloc_template<0>::set_malloc_handler(Out_Of_Memory);//语句1
        int *p = (int*)__malloc_alloc_template<0>::allocate(sizeof(int) * 2073741824);
        if(p == NULL)
        {
                cout<<"Error."<exit(1);
        }
        cout<<"OK"<return 0;
}

test2.cpp

#include
#include
#include "memory"
using namespace std;

int main()
{
    char *p1 = (char *)__default_alloc_template<0,0>::allocate(sizeof(char) * 32);
    char *p2 = (char *)__default_alloc_template<0,0>::allocate(sizeof(char) * 32);
    char *p3 = (char *)__default_alloc_template<0,0>::allocate(sizeof(char) * 64);
    char *p4 = (char *)__default_alloc_template<0,0>::allocate(sizeof(char) * 96);
    return 0;
}

test.cpp 运行结果如下:
不加语句1, 调用默认的处理程序
这里写图片描述
加上语句1,调用了Out_Of_Memory()处理程序
这里写图片描述

test2.cpp:
这个测试没有输出结果,但每句可能会走不同的代码分支,可以根据之前介绍chunk_allco()函数时的例子,用单步调试的方法进行跟踪,就能够了解空间配置器是如何配置空间了.

你可能感兴趣的:(C/C++,STL)