STL源码学习系列二: 空间配置器(Allocator)

空间适配器(Allocator)


前言

侯捷在《STL源码剖析》一书讲到:
这里写图片描述
这是STL学习的第一部分,空间适配器,所谓空间适配器,就是用来管理内存的一个器具。对于STL来说,空间适配器是它可以正常工作的基础,也为它可以高效工作提供了动力。对于使用STL来说,它是不和用户直接打交道的,而是隐藏在一切STL组建之后,默默为各种内存申请提供支持的。


主要思想

对象构造前的空间配置和对象析构后的空间释放,由stl_alloc.h负责,SGI对此的设计哲学如下:

   (1) 向system heap 要求空间;  
   (2) 考虑多线程(multi-threads)状态;
   (3) 考虑内存不足时的应变措施;  
   (4) 考虑过多“小型区块”可能造成的内存碎片(fragment)问题。

C++的内存配置基本操作是::operator new(),内存释放基本操作是::operator delete()。这两个全局函数相当于C的malloc() 和 free() 函数。SGI正是以malloc()和free() 完成内存的配置和释放。考虑到小型区块所可能造成的内存破碎问题,SGI 设计了双层级配置器,第一级配置器直接使用malloc() 和 free() ,第二级配置器则是情况采用不同的策略:以配置128bytes区块为界,大于则调用第一级配置器,小于则采用复杂的memory pool整理方式,同时也取决是否定义了_USE_MALLOC。


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

SGI有一个标准空间适配器,同时还有一个特殊空间适配器:

标准空间适配器为: std::allocator,这个适配器只是对newdelete的浅层包装,所以没有什么技术含量,所以在SGI中从没使用过这个标准适配器。
特殊空间适配器是: std:alloc,这是一个具有次分配能力的特殊空间适配器,它具有一级和二级适配器,它们协调工作。

Std::alloc的主要思想是:定义一个空间大小阈值,128bytes,如果申请的空间大于128bytes,那么就调用第一级空间适配器来完成分配工作,如果小于128bytes,那么就调用第二级空间适配器来完成。
这里写图片描述

需要注意的是,SGI版STL提供了一层更高级的封装,定义了一个simple_alloc类,无论是用哪一级都以模板参数alloc传给simple_alloc,这样对外体现都是只是simple_alloc。

而它的代码实现比较简单,仅仅是调用一级或者二级配置器的接口 allocator.h

#ifdef _ALLOCATOR_H_
#define _ALLOCATOR_H_
/*
 *为alloc类封装接口
 */
#include"alloc.h"
#include"construct.h"
#include<cassert>
#include<new>

namespace EasySTL
{
    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));
            }
    };
}
#endif

第一级配置器

对于第一级适配器:直接调用malloc和free来完成分配与释放内存的工作(没有调用new和delete),最为重要的是,第一级适配器具有new-handle机制,用户可以指定当出现out-of-memory时的处理函数,在SGI里面,当第一级alloc失败时,会接着调用oom_alloc函数来尝试分配内存,如果oom发现没有指定new-handler函数的话,那就无能为力了!会抛出__THROW_BAD_ALLOC这个异常,下面是第一级适配器的主要流程(对于alloc来讲,其他如realloc一样):

1、空间分配器的“分线器”

static void * allocate(size_t n)
{
    void *result = malloc(n);//直接使用第一级分配器,直接使用malloc
    if (0 == result) result = oom_malloc(n);//第一级分配器失效了,那就使 用第二级分配器,oom(out of memeory)
    return result;//将分配的空间以void*的方式返回,用户可以随意转化为需要的类型
}

n为我们想要申请的内存,如果malloc可以满足要求的话,直接返回,否则交由oom_malloc()来处理。

2、具有new-handler的oom_malloc

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);//如果内存已经得到满足的话,那么就可以返回了,如果不满足,那么
        //就继续释放,分配....
    }
}

其中的__malloc_alloc_oom_handler就是由用户设定的new-handler,我们可以通过下面的函数来设定这个句柄:

static void (* set_malloc_handler(void (*f)()))()
{
    void (* old)() = __malloc_alloc_oom_handler;//old handler of outing of memory
    __malloc_alloc_oom_handler = f;//new handler for hander the out of memory
    return(old);
}

是不是看起来oom_malloc也没做什么事情,就是一直在循环申请内存?在一个循环里oom_handler->malloc….就等着某一个时刻成功申请到内存了,就可以返回交差了!!


第二级配置器

我们重点来看看第二级配置器,这才是SGI的经典(个人),我们需要再次知道第二级分配器是怎么“被”工作的,当用户申请的内存大小小于128bytes时,SGI配置器“分线器”就会将这个工作交由第二级分配器来完成。此时,第二级分配器就要开始工作了。第二级分配器的原理较为简单,就是向内存池中申请一大块内存空间,然后按照大小分为16组,(8,16…..128),每一个大小都对应于一个free_list链表,这个链表上面的节点就是可以使用的内存空间,需要注意的是,配置器只能分配8的倍数的内存,如果用户申请的内存大小不足8的倍数,配置器将自作主张的为用户上调到8的倍数,所以有时候你明明超出边界了但是系统却没有阻止你的行为的时候,你应该知道是SGI空间配置器救了你。
当然,第二级配置器的原理远没有这么简单,上面我们说到第二级配置器如何管理内存,现在,我们要开始为用户分配内存和回收内存了。

二级配置器分配过程:当用户申请一个内存后,第二级配置器首先将这个空间大小上调到8的倍数,然后找到对应的free_list链表,如果链表尚有空闲节点,那么就直接取下一个节点分配给用户,如果对应链表为空,那么就需要重新申请这个链表的节点,默认为20个此大小的节点。如果内存池已经不足以支付20个此大小的节点,但是足以支付一个或者更多的该节点大小的内存时,返回可完成的节点个数。如果已经没有办法满足该大小的一个节点时,就需要重新申请内存池了!所申请的内存池大小为:2*total_bytes+ROUND_UP(heap_size>>4),total_bytes是所申请的内存大小,SGI将申请2倍还要多的内存。为了避免内存碎片问题,需要将原来内存池中剩余的内存分配给free_list链表。如果内存池申请内存失败了,也就是heap_size不足以支付要求时,SGI的次级配置器将使用最后的绝招–>查看free_list数组,查看是否有足够大的没有被使用的内存,如果这些办法都没有办法满足要求时,只能调用第一级配置器了,我们需要注意,第一级配置器虽然是用malloc来分配内存,但是有new-handler机制(out-of-memory),如果无法成功,只能抛出bad_alloc异常而结束分配。
上面说到的配置器“分线器”,其实就是次级配置器的入口,如果次级配置器发现所需要分配的内存大于128bytes时,就会交由第一级配置器去完成,否则由自己完成。
这里写图片描述
下面,我们就来看看这个次级配置器是怎么实现的:

 enum {__ALIGN = 8};//小型区域的上调边界,用户如果申请30bytes大小内存,系统将返回32bytes给用户
 enum {__MAX_BYTES = 128};//小型区域的上限,超过这个大小将直接由第一级配置器直接配置内存
 enum {__NFREELISTS = __MAX_BYTES/__ALIGN};//free-list链表的个数,从8一直到128,总共需要16个链表维护每个级别大小的链表

我相信不需要说什么就可以明白,上面的代码是在定义最小河最大的空间大小,和需要的链表的个数,为什么要定义这个呢?
也就是说,如果你觉得SGI STL的配置器过于频繁的调用次级配置器,那么你可以修改进入次级配置器的条件,比如可以修改成16-128,或者32-256等等。我们需要明白的事情是,虽然次级配置器解决了内存碎片的问题,但是给内存管理带来了额外的负担,有可能需要不断去调整free_list,甚至去反过来调用第一级配置器,所以如果你觉得进入配置器的区间不够合理的话,可以自己调整!(但别玩死了!)。

下面这个函数返回离bytes最近的可以被8整除的整数,上取整。

static size_t ROUND_UP(size_t bytes)
{
        return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
}

下面我们看看free_list的节点的数据结构:

union obj 
{
        union obj * free_list_link;
        char client_data[1];    /* The client sees this.        */
};

如此巧妙地运用union来管理节点,如果还没有被分配,那么free_list_link有效,如果已经分配给用户,那么client_data[1]有效。不会造成多余的浪费!

下面这个定义就是我们所说的free_list链表数组,每一个数组元素都将对应一个链表:

static obj * __VOLATILE free_list[]; 

下面这个函数将找到所申请的内存所对应的空闲链表,注意,SGI将用户的内存申请大小上调至8的倍数:

  static  size_t FREELIST_INDEX(size_t bytes) 
  {
        return (((bytes) + __ALIGN-1)/__ALIGN - 1);
  }

次级配置器将会使用内存池,所以下面给出内存池的区间。

  static char *start_free; //内存池的开始位置
  static char *end_free; //内存池的结束位置

下面就是激动人心的内存分配函数了,该函数将首先判断区间大小,如果大于128bytes,就调用第一级配置器,小于128bytes就寻找对应的free_list链表,如果有可用的,直接拿,如果没有,则调用函数refill来重新分配节点(20个)。

 static void * allocate(size_t n){...}

有申请就需要回收内存,下面就是次级配置器的空间释放函数,该函数将判断区间大小,大于128bytes直接调用第一级配置器来回收内存,小于128bytes则回收到相应的free_list链表中,较为简单。

static void deallocate(void *p, size_t n){...}

下面我们来看看refill函数,这个函数完成重新给链表分配节点的工作。新的空间将从内存池中取到,(内存池由chunk_alloc管理),缺省的话只取20个新节点,但有可能小于20个节点。

void *alloc::refill(size_t bytes)

下面就是最为重要的也是最难的内存池了。当然,下面这个函数只是从内存池中取出空间给free_list用而已。不过整个过程很曲折,很经典,需要仔细品味,很棒的设计!

char *alloc::chunk_alloc(size_t bytes,size_t& nobjs){...}

配置器的流程

讲完了上面的内容,我们现在应该很清楚次级配置器是怎么工作的了,其实,我们可以看到第一级配置器就是为了配合第二级配置器而存在的,重点在于次级配置器的设计原理。最后我们来走一遍配置器的流程:

Alloc_bytes is the ask memory.
If(Alloc_bytes>128)
       First_alloc work start...
Else
       Second_alloc  work start....
In Second_alloc:
If(free_list!=NULL)
     Get an node of this free list and return it
Else 
Refill function start to work...
      Chunk_alloc function start to work..
In chunk_alloc:
If memory pool’s size bigger wanted,just assign
Else if the memory pool’s size can reach more than 1 nodes
Then alloc...
Else
   First allocator start to work...
   ...call itself ....

源码

alloc.h

#ifndef _ALLOC_H_
#define  _ALLOC_H_
/*
 *  简单的空间适配器
 */
#include<cstdlib>

namespace EasySTL
{
    /*
     * 空间是配器
    */
    class alloc
    {
        private:
            enum{_ALIGN=8};  //小型区域块上调边界
            enum{_MAX_BYTES=128};  //小型区块的上限
            enum{_NFREELIST=_MAX_BYTES/_ALIGN};  //freelist的个数16
            enum{_NOBJS=20};  //每次增加节点个数

            union obj  //freelist节点
            {
                union obj* free_list_next;
                char client[1];
            };

            static obj* volatile free_list[_NFREELIST];

            static char* start_free;  //内存池开始位置,只在chunk_alloc()中变化
            static char* end_free;  //内存池结束位置
            static size_t heap_size;

            //根据需要的区块大小,选择freelist编号,从1开始
            static size_t FREELIST_INDEX(size_t bytes)
            {
                return ((bytes+_ALIGN-1)/_ALIGN-1);
            }

            //将bytes上调至8的倍数,如果8n<byte<8(n+1),得到8(n+1)
            static size_t ROUND_UP(size_t bytes)
            {
                return ((bytes+_ALIGN-1)&~(_ALIGN-1));
            }

            //返回一个大小为n的对象,并可能加入大小为n的其他区域到freelist
            static void *refill(size_t bytes);

            //配置一大块空间,可容纳_NOBJS个大小为size的区块
            //如果配置_NOBJS个区块有所不便,_NOBJS可能会降低
            static char *chunk_alloc(size_t bytes,size_t& nobjs);
        public:
            static void *allocate(size_t n);
            static void deallocate(void *p,size_t bytes);
            static void *reallocate(void *p,size_t old_sz,size_t new_sz);
    };
}
#endif

alloc.cpp

#include"alloc.h"

namespace EasySTL
{
    char *alloc::start_free=0;
    char *alloc::end_free=0;
    size_t alloc::heap_size=0;

    alloc::obj* volatile alloc::free_list[alloc::_NFREELIST]=
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};

    //空间配置
    void *alloc::allocate(size_t bytes)
    {
        if(bytes>_MAX_BYTES)  //调用一级配置器
        {
            return malloc(bytes);
        }

        //选择freelist编号
        size_t index=FREELIST_INDEX(bytes);

        obj *my_list=free_list[index];

        if(my_list==0)  //没有可用的freelist,准备重新填充freelist
        {
            void *r=refill(ROUND_UP(bytes));
            return r;
        }

        //调整freelist,将list后面的空间前移,返回list所指的空间
        free_list[index]=my_list->free_list_next;

        return my_list;
    }

    //空间释放
    void alloc::deallocate(void *ptr,size_t bytes)
    {
        if(bytes>_MAX_BYTES) 
        {
            free(ptr);
            return;
        }

        size_t index=FREELIST_INDEX(bytes);
        obj* node=static_cast<obj *>(ptr);
        node->free_list_next=free_list[index];
        free_list[index]=node;
    }

    //重新分配内存空间
    void *alloc::reallocate(void *ptr,size_t old_sz,size_t new_sz)
    {
        deallocate(ptr,old_sz);
        ptr=allocate(new_sz);
        return ptr;
    }

    //重新填充
    //返回一个大小为n的对象,并且有时候会为适当的freelist增加节点
    //假设bytes已经上调为8的倍数
    void *alloc::refill(size_t bytes)
    {
        //记录获取的区块数目
        size_t nobjs=_NOBJS;

        //从内存池中取nobjs个区块作为freelist的新节点
        char* chunk=chunk_alloc(bytes,nobjs);
        obj* volatile *my_list=0;
        obj* result;
        obj *current_obj=0, *next_obj=0;

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

        //准备调整freelist,纳入新节点
        my_list=free_list+FREELIST_INDEX(bytes);

        //在chunk中建立freelist
        result=(obj *)(chunk);  //这一块准备返回给客服端

        //引导freelist指向新配置的空间(取自内存池)
        *my_list=next_obj=(obj *)(chunk+bytes);

        //将freelist中的节点串联起来
        for(int i=1;;++i) //从1开始,因为第0个返回给客户端
        {
            current_obj=next_obj;
            next_obj=(obj *)((char *)next_obj+bytes);
            if(nobjs-1==i)
            {
                current_obj->free_list_next=0;
                break;
            }
            current_obj->free_list_next=next_obj;
        }
        return result;
    }

    //内存池(一大块空闲空间)取空间给freelist使用,bytes已经上调为8的倍数
    char *alloc::chunk_alloc(size_t bytes,size_t& nobjs)
    {
        char *result;
        size_t bytes_need=bytes * nobjs;  //需求字节数
        size_t bytes_left=end_free-start_free;  //内存池剩余空间

        if(bytes_left>bytes_need)  //能满足需求量
        {
            result=start_free;
            start_free+=bytes_need;
            return result;
        }

        else if(bytes_left>=bytes)  //不能完全满足需求,能满足一个及以上的区块
        {
            nobjs=bytes_left/bytes;
            bytes_need=nobjs*bytes;
            result=start_free;
            start_free+=bytes_need;
            return result;
        }
        else  //一个区块也无法提供
        {
            //每次申请两倍的新内存
            size_t bytes_to_get=2*bytes_need+ROUND_UP(heap_size>>4);

            //试着让内存池中的残余零头还有利用价值
            if(bytes_left>0)
            {
                obj * volatile *my_list=free_list+FREELIST_INDEX(bytes_left);
                ((obj *)start_free)->free_list_next=*my_list;
                *my_list=(obj *)start_free;
            }

            //调整heap空间,用来补充内存池
            start_free=(char *)malloc(bytes_to_get);
            if(start_free==0)
            {
                //heap空间内存不足,malloc分配失败
                obj * volatile * my_list=0,*p=0;
                //在freelist上寻找未使用的大区域块
                for(int i=bytes;i<_MAX_BYTES;i+=_ALIGN)
                {
                    my_list=free_list+FREELIST_INDEX(i);
                    p=*my_list;
                    if(p!=0) //freelist中尚有未用区块
                    {
                        //调整freelist释放出未用区块
                        *my_list=p->free_list_next;
                        start_free=(char *)p;
                        end_free=start_free+i;
                        //递归调用自己,为了修正nobjs 任何残余零头都将被编入十大那该的freelist备用
                        return chunk_alloc(bytes,nobjs);
                    }
                }
                end_free=0;
            }
            heap_size+=bytes_to_get;
            end_free=start_free+bytes_to_get;
            return chunk_alloc(bytes,nobjs);
        }
    }
}

allocator.h

#ifdef _ALLOCATOR_H_
#define _ALLOCATOR_H_
/*
 *为alloc类封装接口
 */
#include"alloc.h"
#include"construct.h"
#include<cassert>
#include<new>

namespace EasySTL
{
    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));
            }
    };

}

#endif

End

猜你喜欢

转载自blog.csdn.net/qq_34777600/article/details/80413718