STL空间配置器

来源:互联网 发布:软件开发等级考试 编辑:程序博客网 时间:2024/06/01 08:38

我们为什么要研究STL中的空间配置器呢?

因为STL中的算法,容器等,申请内存不是直接向操作系统要!而是向STL的另一组件空间配置器申请,
STL为什么要这么做呢,假设现在我们要用STL中的list,我们一会插入一个节点,一会插入一个节点,
这些节点的地址在内存中可能不是连续的,假如我们现在要向操作系统申请一大块内存,当我们去申请
的时候操作系统会告诉我们还有足够的内存,但是结果却申请不出来,原因是我们在用list申请空间的时候
在内存中断断续续的没有连接起来,把一整块内存划分了许多小块内存。

这里写图片描述

这就是内存碎片问题。

SGI对这种问题的设计哲学如下:
1. 像system heap要求空间
2. 考虑多线程状态
3. 考虑内存不足时的应对措施
4. 考虑过多的“小型区块”可能造成的内存碎片(fragment)问题。

考虑到小型区块所可能造成的内存碎片问题,SGI设计了双层级配置器,第一层配置器直接使用
malloc()和free(),第二级配置器则视情况采用不同的策略。当配置区块大于128bytes字节,
则视之为“足够大”,去调用一级空间配置器,如果小于128bytes,则视之为过小,便采用memory
pool 整理方式,而不再求助于第一级配置器,

那么问题来了?整个设计究竟只开放一级配置,还是同时开启二级配置器呢?取决于
__USE_MALLOC是否被定义?(经检测STL中没有定义这个宏)

#if __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

无论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 0 == n ? 0 : (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, n*sizeof(T));    }};

其内部的四个函数其实都是单纯的转调用,调用传给配置器的(一级或二级)的成员函数。
SGI的容器全部都使用这个simple_alloc接口。

下面我们就先研究一下一级配置器。
一级空间配置器的处理流程:
这里写图片描述

SGI,以malloc()来配置内存,如果失败就会调用oom_malloc(),如果客户端没有设置
内存不足处理机制,就直接返回std::bad_alloc,如果设置了内存不足处理机制,那么
就会一直循环直到分配到内存为止,如果设计的不好,很容易发生死循环。

注意:设计“内存不足处理机制”是客户端的责任,设定“内存不足处理机制”也是客户端的责任。

typedef void(*MallocAlloc)();template<int Inst> //预留参数class __MallocAllocTemplate{private:    static void *_OomMalloc(size_t); //malloc调用失败调用的函数    static MallocAlloc _MallocAllocOomHandler; //函数指针内存不足的时候处理机制public:    static void* Allocate(size_t n)    {        void* result;        result = malloc(n);        if (n == 0)//如果malloc失败,调用_Oomalloc        {            _OomMalloc(n);         }        return result;    }    static void DeAllocate(void* p) //释放内存    {        free(p);    }    static MallocAlloc _SetMallocHandler(MallocAlloc f) //参数是函数指针,返回值也是函数指针    {        MallocAlloc old = _MallocAllocOomHandler;        _MallocAllocOomHandler = f;        return f;    }};template<int Inst>void(*__MallocAllocTemplate<Inst>::_MallocAllocOomHandler)() = 0; //默认不设置内存不足处理机制template<int Inst>void* __MallocAllocTemplate<Inst>::_OomMalloc(size_t n){    MallocAlloc _MyMallocHandler;    void* result;    while (1)    {        _MyMallocHandler = _MallocAllocOomHandler;        if (0 == _MyMallocHandler)            throw std::bad_alloc();        (*_MyMallocHandler)();     //调用内存不足的处理函数        if (result = malloc(n))     //重新申请内存            return result;     }}

二级空间配置器的逻辑步骤:
假如现在申请n个字节:

  1. 判断n是否大于128,如果大于128则直接调用一级空间配置器。如果不大于,则将n上调至8的倍数处,
    然后再去自由链表中相应的结点下面找,如果该结点下面挂有未使用的内存,则摘下来直接返回这块空间的
    地址。否则的话我们就要调用refill(size_t n)函数去内存池中申请。

  2. 向内存池申请的时候可以多申请几个,STL默认一次申请nobjs=20个,将多余的挂在自由链表上,
    这样能够提高效率。 进入refill函数后,先调chunk_alloc(size_t n,size_t& nobjs)函数去内存池中申
    请,如果申请成功,这时候就有两种情况:

    1. 如果nobjs=1的话则表示内存池只够分配一个,这时候只需要返回这个地址就可以了。
    2. 如果nobjs大于1,则将多余的内存块挂到自由链表上。
      如果chunk_alloc失败的话,在他内部有处理机制。
  3. 进入chunk_alloc(size_t n,size_t& nobjs )向内存池申请空间的话有三种情况:
    3.1. 内存池剩余的空间足够nobjs*n这么大的空间,则直接分配好返回就可以了。
    3.2. 内存池剩余的空间够分配一个size大小,但是不够nobjs*n个,返回剩余大小除以size对象大小
    3.3. 内存池中剩余的空间连一个n都不够了,这时候就要向heap申请内存,
    不过在申请之前先要将内存池中剩余的内存挂到自由链表上,之后再向heap申请。

    3.3.1、 如果申请成功的话,则就再调一次chunk_alloc重新分配。
    3.3.2、如果不成功的话,这时候再去自由链表中看看有没有比n大的空间,
    如果有就将这块空间还给内存池,然后再调一次chunk_alloc重新分配。
    3.3.3、如果没有的话,则就调用一级空间配置器分配,看看内存不足处理机制能否处理。

这里写图片描述

代码如下:

enum { _ALIGN = 8 }; //小区间的上调边界enum { _MAXBYTES=128}; //小型区块的上限enum { _NFREELISTS = _MAXBYTES / _ALIGN }; //free_lists的个数template<bool threads,int Inst>class __DefaultAllocTemplate{private:    union _Obj  //自由链表节点的类型    {         _Obj* _FreeListLink;        char _ClientData[1];    };private:    static char* _StartFree;  //内存池的头指针    static char* _EndFree;   //内存池的尾指针    static size_t _HeapSize;  //记录内存池已经向heap申请了多大空间    static _Obj* volatile _FreeList[_NFREELISTS]; //自由链表private:    static size_t _GetFreeListIndex(size_t bytes) //得到节点的下标    {        return (bytes + (size_t)_ALIGN - 1) / (size_t)_ALIGN - 1;    }    static size_t _GetRoundUp(size_t bytes)  //向上取8的倍数    {        return (bytes + (size_t)_ALIGN - 1)&(~(size_t)(_ALIGN - 1));    }    static void* _ReFill(size_t n);  //在自由链表中取    static char* _ChunkAlloc(size_t size, int& nobjs); //在内存池中申请nobjs个对象,每个对象size大小public:    static void* Allocate(size_t n);   //n大于0    static void DeAllocate(void* p, size_t n); };template<bool threads,int Inst>char* __DefaultAllocTemplate<threads, Inst>::_StartFree = 0;template<bool threads,int Inst>char* __DefaultAllocTemplate<threads, Inst>::_EndFree = 0;template<bool threads,int Inst>size_t __DefaultAllocTemplate<threads, Inst>::_HeapSize = 0;template<bool threads,int Inst>typename __DefaultAllocTemplate<threads, Inst>::_Obj* volatile  //前面加typename表示后边是类型__DefaultAllocTemplate<threads, Inst>::_FreeList[_NFREELISTS] = { 0 };template<bool threads,int Inst>void* __DefaultAllocTemplate<threads, Inst>::Allocate(size_t n) //分配空间{    void* ret;    if (n > _MAXBYTES) //大于128在一级空间配置器中申请    {        ret = malloc_alloc::Allocate(n);    }    else //在自由链表中找    {        _Obj* volatile* MyFreeList = _FreeList + _GetFreeListIndex(n);        _Obj* result = *MyFreeList;        if (result == NULL)        {            ret = _ReFill(_GetRoundUp(n));        }        else        {            *MyFreeList = result->_FreeListLink;            ret = result;        }    }    return ret;}template<bool threads,int Inst>void __DefaultAllocTemplate<threads, Inst>::DeAllocate(void* p, size_t n)//回收空间{    if (n > _MAXBYTES)    {        malloc_alloc::DeAllocate(p);    }    else //将内存回收到自由链表当中    {        _Obj* q = (_Obj*)p;        _Obj*volatile* MyFreeList = _FreeList + _GetFreeListIndex(n);        q->_FreeListLink = *MyFreeList;        *MyFreeList = q;    }}template<bool threads,int Inst>void* __DefaultAllocTemplate<threads, Inst>::_ReFill(size_t n){    int nobjs = 20;    char* chunk = _ChunkAlloc(n, nobjs);    if (1 == nobjs)    {        return chunk;    }    _Obj* ret = (_Obj*)chunk; //将第一份返回    _Obj* volatile * MyFreeList = _FreeList + _GetFreeListIndex(n);    *MyFreeList = (_Obj*)(chunk + n); //将第二个对象的地址放到链表中    _Obj* cur = *MyFreeList;    _Obj* next= 0;    cur->_FreeListLink = 0;    for (int i = 2; i < nobjs; i++) //将剩下的挂在自由链表下    {        next = (_Obj*)(chunk + n*i);        cur->_FreeListLink = next;        cur = next;    }    cur->_FreeListLink = 0;    return ret;}template<bool threads,int Inst>char* __DefaultAllocTemplate<threads, Inst>::_ChunkAlloc(size_t size, int& nobjs) //向系统中申请{    char* result = 0;    size_t totalBytes = size*nobjs; //总共要申请的字节数    size_t leftBytes = _EndFree - _StartFree;  //内存池中剩余的字节数    if (leftBytes >= totalBytes)  //如果剩余的大小大于申请的大小就直接返回    {        result = _StartFree;        _StartFree += totalBytes;        return result;    }    else if (leftBytes > size) //至少大于一个    {        nobjs = (int)(leftBytes / size);        result = _StartFree;        _StartFree += (nobjs*size);        return result;    }    else  //内存池已经不够一个了    {        size_t NewBytes = 2 * totalBytes + _GetRoundUp(_HeapSize >> 4);        //扩容之前将剩下的就近挂在链表下边        if (leftBytes > 0)         {            _Obj* volatile * MyFreeList = _FreeList + _GetFreeListIndex(leftBytes);            ((_Obj*)_StartFree)->_FreeListLink = *MyFreeList;            *MyFreeList = (_Obj*)_StartFree;        }        //开辟新的内存        _StartFree = (char*)malloc(NewBytes);        if (_StartFree == 0) //申请失败        {            //如果申请不到,就在自由链表中找,找到了在调用,_ChunkAlloc函数            for (size_t i = size; i < (size_t)_MAXBYTES; i += (size_t)_ALIGN)            {                _Obj* volatile *MyFreeList = _FreeList + _GetRoundUp(i);                _Obj* p = *MyFreeList;                if (p != NULL)                {                    _StartFree = (char*)p; //将这块内存拿回到内存池中                    *MyFreeList = p->_FreeListLink;                    _EndFree = _StartFree + i;                    return _ChunkAlloc(size, nobjs);                }            }            //如果还找不到就去一级空间配置器下去申请,如果还申请不到,那里会抛异常            _EndFree = NULL;            _StartFree = (char*)malloc_alloc::Allocate(NewBytes);        }        //开辟成功,就更新heap_size,更新end_Free;        _HeapSize += NewBytes;        _EndFree = _StartFree + NewBytes;        return _ChunkAlloc(size, nobjs);    }}

空间配置器的其他问题:

  1. 在空间配置器中所有的函数和变量都是静态的,所以它们在程序结束的时候才会被释放,
    二级空间配置器没有将内存还给操作系统,只是把他们都挂在自由链表下,只有当你的程序结束时
    才还给操作系统。

  2. 假如我不断的开辟小块内存,最后将整个heap的内存都挂在自由链表下,但是都没有用这些
    空间,再想开辟一块大的空间就会失败。

  3. 二级空间配置器会出现内碎片问题,假如我一直申请char,那么就会浪费7/8的空间,
    但总体来说性能还是比较高的。

原创粉丝点击