STL剖析——空间配置器

来源:互联网 发布:gossip协议《算法》 编辑:程序博客网 时间:2024/05/22 14:43

近来闲来无趣,看了下STL的空间配置器。

首先恭喜我入坑,网上无数的人都在剖析这STL的空间配置器,所以,刚好,今天我也加入了这个团队,这个应该是我目前看到的比较完整架构的一个源码了吧,希望能提示我的水平,毕竟设计的很好。在这里特别说下,没有操作系统的知识,所以在这里的线程和锁的这些问题,我们一概不讨论

STL的空间配置器,就是一个为了给容器进行分配内存和管理内存的东西,容许我盗几张图给大家说明。

这里写图片描述
这个是整个STL的一个结构,后续我会再写博客对这些进行分析。

1.为什么会利用SGI的空间配置器?


首先这个问题具有很强的目的性,因为接触一个东西只有了解了怎么样用才是最快的学习方式之一。
空间配置器而言是对内存管理,所以解决的问题就是内存上的容易出现的问题。所以我们先来看内存上会出现什么问题?

这里写图片描述

这个时候就会出问题了,这个时候就会出现一个外碎片的问题。这个时候的碎片处理方式就可以进行优化,STL当中也就考虑到了这种情况。

另外一个问题就是,对于很多情况下,我们经常会做一些大量的内存分配与释放,比如说在高并发的情况下,但是,在这种情况下,如果你每次都对操作系统进行分配请求与释放请求,这样毫无疑问,效率非常低下,所以,作为提倡效率的一门语言,C++充分的考虑了这个问题。

接下来,就让我们去瞧一瞧。

2.SGI空间配置器的接口

首先我们来看看关于接口部分当我们对一个容器进行new 的时候,在这里提供了一个宏__USE_MALLOC,定义了这个宏,调用第一级配置器,没有定义的,调用第二级配置器。另外就是,接下来,STL 为了让整个符合规范化,在这里,STL封装了一个类叫做simple_alloc,通过这个接口,我们来调用空间配置器进行内存管理。
这里写图片描述

3.SGI空间配置器的一级空间配置器

接下来所说的一级空间配置器,一级空间配置器是一个叫做__malloc_alloc_template的类,首先我们需要知道,只有当在要求分配字节数在128个字节以上的时候这个时候才会去调用一级分配器,这个时候所以简单的也就是采用了malloc和free机制,不过,设计STL的人很聪明,在这为了要求对空间利用的最大化,它在这里还考虑了out of memory机制。

template <int inst>class __malloc_alloc_template {private:static void *oom_malloc(size_t);                            //处理内存不够的函数static void *oom_realloc(void *, size_t);#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG    static void (* __malloc_alloc_oom_handler)();           //处理内存不够的一个句柄#endifpublic:static void * allocate(size_t n){    void *result = malloc(n);                               //进行分配n个内存空间    if (0 == result)                                        //判断是否分配成功        result = oom_malloc(n);                             //内存分配不成功,进入OOMMalloc查看是否可以操作系统释放内存分配,如果无法分配,抛出异常。    return result;                                          }//第一级空间配置器直接使用freestatic void deallocate(void *p, size_t /* n */){    free(p);}static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz){    void * result = realloc(p, new_sz);    if (0 == result) result = oom_realloc(p, new_sz);    return result;}//通过这个函数设置文件句柄,从而间接实现set_new_handler。static void (* set_malloc_handler(void (*f)()))(){    void (* old)() = __malloc_alloc_oom_handler;    __malloc_alloc_oom_handler = f;             //设置文件句柄    return(old);                                //返回旧的,操作系统惯用写法。}

这里我想你就知道了,为什么说new是抛异常了,也会发现这个设计者所做的很好了,尤其是set_new_handler的机制,可以通过这个达到了,对内存的最大利用,当然,new-handler函数需要自己设计,一个好的new-handler函数是非常重要的,否则就非常坑了,关于这里的问题如果不清楚,也可以去参考 effective c++。’
这里写图片描述

4.SGI空间配置器的二级空间配置器


当对象所需内存大于128个字节的时候,这个时候我们就需要去调用第二级空间配置器。
这个时候我们进行维护的就是16个自由链表,这16个自由链表的节点就是采用一个指针数组。这个指针数组用来维护整个内存块。

allocate()
进行空间分配,当大于128个字节的时候调用第一级空间配置器,否则,去寻找对应的自由链表,然后取出对应自由链表的头节点,当自由链表为空的时候,这个时候去内存池发出分配请求。调用refill函数。

deallocate()
进行空间的释放,当大于128个字节的时候,这个时候去调用第一级空间配置器的释放,然后直接进行free,否则,这个时候把内存空间还给自由链表,这个时候的还给自由链表的一种方式就是类似于链表的头插的那种方式。

class __default_alloc_template {private:  // Really we should use static const int x = N  // instead of enum { x = N }, but few compilers accept the former.# ifndef __SUNPRO_CC    enum {__ALIGN = 8};                                                  //限定最大对齐数    enum {__MAX_BYTES = 128};                                     //最大字节数    enum {__NFREELISTS = __MAX_BYTES/__ALIGN};     //自由链表个数 # endif    //得到一个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];    /* The client sees this.        */            };private:# ifdef __SUNPRO_CC    static obj * __VOLATILE free_list[];        // Specifying a size results in duplicate def for 4.1# else    //声明自由链表,其实就是一个指针数组    static obj * __VOLATILE free_list[__NFREELISTS];# endif    //巧妙实现查找到对应自由链表  static  size_t FREELIST_INDEX(size_t bytes) {        return (((bytes) + __ALIGN-1)/__ALIGN - 1);  }  // Returns an object of size n, and optionally adds to size n free list.  //填充的函数  static void *refill(size_t n);  // Allocates a chunk for nobjs of size size.  nobjs may be reduced  // if it is inconvenient to allocate the requested number.  //分配大块内存的函数  static char *chunk_alloc(size_t size, int &nobjs);  // Chunk allocation state.  static char *start_free;                           //维护内存池的开始  static char *end_free;                            //维护内存池的结束  static size_t heap_size;                          //记录所有的从堆上开辟的内存的大小# ifdef __STL_SGI_THREADS    static volatile unsigned long __node_allocator_lock;    static void __lock(volatile unsigned long *);    static inline void __unlock(volatile unsigned long *);# endif# ifdef _PTHREADS    static pthread_mutex_t __node_allocator_lock;# endif# ifdef __STL_WIN32THREADS    static CRITICAL_SECTION __node_allocator_lock;    static bool __node_allocator_lock_initialized;  public:    __default_alloc_template() {    // This assumes the first constructor is called before threads    // are started.        if (!__node_allocator_lock_initialized) {            InitializeCriticalSection(&__node_allocator_lock);            __node_allocator_lock_initialized = true;        }    }  private:# endif    class lock {        public:            lock() { __NODE_ALLOCATOR_LOCK; }            ~lock() { __NODE_ALLOCATOR_UNLOCK; }    };    friend class lock;public:  /* n must be > 0      */    //分配内存函数  static void * allocate(size_t n)  {    obj * __VOLATILE * my_free_list;            //指向一个自由链表节点,在这里一定要是在内存上取到的    obj * __RESTRICT result;                    //    //当大于128个字节的时候,去调用第一级分配器    if (n > (size_t) __MAX_BYTES) {        return(malloc_alloc::allocate(n));    }    my_free_list = free_list + FREELIST_INDEX(n);            //寻找对应的自由链表    // Acquire the lock here with a constructor call.    // This ensures that it is released in exit or during stack    // unwinding.#       ifndef _NOTHREADS        /*REFERENCED*/        lock lock_instance;#       endif    result = *my_free_list;                           if (result == 0) {                            //如果result为NULL,这个时候向利用内存池进行填充处理。        void *r = refill(ROUND_UP(n));        return r;    }    //当获得了多块内存的,时候进行调整,把第一块返回,然后剩下的放到自由链表下面    *my_free_list = result -> free_list_link;       return (result);  };  /* p may not be 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;    }    my_free_list = free_list + FREELIST_INDEX(n);        //寻找字节大小对应的自由链表    // acquire lock#       ifndef _NOTHREADS        /*REFERENCED*/        lock lock_instance;#       endif /* _NOTHREADS */    q -> free_list_link = *my_free_list;        //放回自由链表,采用的方式就是采用头插的方式。    *my_free_list = q;                                  //    // lock is released here  }  static void * reallocate(void *p, size_t old_sz, size_t new_sz);} ;

chunk_alloc()
内存池当中没有内存块,首先会查看是否自由链表下挂有剩余的内存节点,这个时候会向操作系统进行请求分配出2的你需求大小+n个内存块(heap_size>>4),一般是40+n个,然后取出其中的20个放到自由链表当中,返回第一个对象空间,这样自由链表下面链了19个,剩下的20+n返回了内存池,提供给下一次进行分配请求使用。

然后进行的是从堆上申请空间,进行配置的问题,首先就开2*需求量+n(n也为8的整数倍)的内存空间,
如果开辟成功,那么就把这块开出的空间放入内存池当中,接下来再次进行递归操作,进行内存分配。修正分配对象个数。
如果因为堆空间不足,开辟失败,这个时候我们需要进行对[对象大小,最大]这几个的自由链表进行再次遍历,查看是否还有能够进行使用的内存块,如果有,那么就调整自由链表,把可以使用的内存块返回到内存池当中去,然后进行递归调用chunk_alloc,修正分配对象的个数。如果遍历完这几个自由链表发现没有可以使用的内存块,那么这个时候再去调用第一级配置器,利用oom_malloc进行操作,看系统是否会使更多的内存空间变成可分配大小,供你使用,如果可以,那么就会分配完成返回。如果不行,那么就会抛出异常。

chuck_alloc()
如果内存池当中有块,并且内存块的大小满足你的需求,那么就将这些空间返回出去

chuck_alloc()
内存池当中有内存块,但是内存块的大小比你的需求要小,这个时候只能确保提供出一个以上的区块,简单的说就是把剩余的区块返回出去。

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;                            //这个时候让result指向内存池开始        start_free += total_bytes;                        //移动需求量的大小        return(result);                                    //返回    } else if (bytes_left >= size) {                    //当你的内存池剩余大小能分配1——nobjs-1个。        nobjs = bytes_left/size;                        //计算你能分配多少个           total_bytes = size * nobjs;                        //计算分配的字节大小        result = start_free;                            //调整result        start_free += total_bytes;                        //移动start_free        return(result);                                    //返回内存块    }    else {                                                //此时内存池中连一个大小都不能分配。        size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);    //得到一个你需要向系统申请的字节大小,2倍的需求量+n        // Try to make use of the left-over piece.        //为了把内存池当中的剩余的内存块放入自由链表当中,采用的是头插的方式。        if (bytes_left > 0) {                       obj * __VOLATILE * my_free_list =                        free_list + FREELIST_INDEX(bytes_left);            ((obj *)start_free) -> free_list_link = *my_free_list;            *my_free_list = (obj *)start_free;        }        //进行内存分配申请        start_free = (char *)malloc(bytes_to_get);        //如果申请内存失败,说明此时堆上已没有内存空间        if (0 == start_free) {            int i;            obj * __VOLATILE * my_free_list, *p;            // Try to make do with what we have.  That can't            // hurt.  We do not try smaller requests, since that tends            // to result in disaster on multi-process machines.            //寻找自由链表字节数size——128字节之间的自由链表是否还有内存块可用            for (i = size; i <= __MAX_BYTES; i += __ALIGN) {                my_free_list = free_list + FREELIST_INDEX(i);                p = *my_free_list;                                               if (0 != p) {                                                //如果有可用的大块自由链表内存块                    *my_free_list = p -> free_list_link;                    //取出这块内存块。                    start_free = (char *)p;                                    //放入内存池当中。                    end_free = start_free + i;                                //调整end_free.                    return(chunk_alloc(size, nobjs));                        //返回调用,调整nobjs                    // Any leftover piece will eventually make it to the                    // right free list.                }            }        end_free = 0;    // In case of exception.                //修正end_free,防止出现end_free和start_free不同在0的情况,出现你的内存池剩余大小出现很大问题。            start_free = (char *)malloc_alloc::allocate(bytes_to_get);        //调用一级分配器的,利用oom_malloc的set_new_handler机制进行分配出内存空间            // This should either throw an            // exception or remedy the situation.  Thus we assume it            // succeeded.        }        heap_size += bytes_to_get;                //分配出来以后,调整heap_size,记录总共向堆申请的内存空间。        end_free = start_free + bytes_to_get;    //调整end_free,把开辟出来的内存放入内存池当中。        return(chunk_alloc(size, nobjs));        //进行递归,进行分配调整nobjs。    }}

refill()
当自由链表为空的时候,这个时候我们需要向内存池发出分配请求。根据内存池中请求分配的对象内存的个数,进行不同的处理,如果是只分配到了一个节点,那么就返回这个节点。如果是大于1的节点数目,那么就把这块区间链接成为链表的形式。就是当前这一块对象内存的内容放下一块对象内存的地址,一直到,最后一块,最后一块放NULL。

/* Returns an object of size n, and optionally adds to size n free list.*//* We assume that n is properly aligned.                                *//* We hold the allocation lock.                                         */template <bool threads, int inst>void* __default_alloc_template<threads, inst>::refill(size_t n){    int nobjs = 20;                                        //默认开20个对象内存大小    char * chunk = chunk_alloc(n, nobjs);                //调用chunk_alloc进行分配内存。    obj * __VOLATILE * my_free_list;                    //    obj * result;                                        //    obj * current_obj, * next_obj;                        //    int i;    if (1 == nobjs) return(chunk);                        //如果开辟是一个内存块,那么直接返回,这个时候传引用。    my_free_list = free_list + FREELIST_INDEX(n);        //找到对应的自由链表。    /* Build free list in chunk */      result = (obj *)chunk;                            //result指向开辟出来的第一块内存对象块      *my_free_list = next_obj = (obj *)(chunk + n);    //把第二块放到自由链表上,此时自由链表链19个内存块。      for (i = 1; ; i++) {                                //进行内存块之间的link修正。        current_obj = next_obj;        next_obj = (obj *)((char *)next_obj + n);        //取到下一块内存块        if (nobjs - 1 == i) {                                       current_obj -> free_list_link = 0;            //如果是最后一块内存块,让它的link指向NULL            break;        } else {                                                   current_obj -> free_list_link = next_obj;    //其他情况,让当前内存块的link指向下一个内存块。        }      }    return(result);                //返回第一个内存块}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[# ifdef __SUNPRO_CC    __NFREELISTS# else    __default_alloc_template<threads, inst>::__NFREELISTS# endif] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };                //初始化自由链表这个指针数组

这里写图片描述

5.总结


空间配置器缺点:

1.有内碎片的问题,

2.内存池在结束之前不能释放,被分割为小块的内存一直挂在自由链表下面,会使得内存的占用率过高。(因为这个时候你无法知道释放所需要的头部是哪一块)

解决方案:

①在二级空间配置器的内存池上面设置一个分配内存的上限,也就是当 _heapsize 到达一定的值时,程序就会自动的抛出异常,提醒用户二级空间配置器占用的内存过多了?

②实现一个机制,保存每次 malloc 出来的空间的首地址,并记录开辟的空间在自由链表中(已分为小块)挂的位置。当发现开辟的空间分成小块全都挂在自由链表下时,说明可以释放空间了,现将每个小块从自由链表中删除,然后 free 。

这里写图片描述

3 0
原创粉丝点击