STL配置器

配置器

如果你不曾仔细研读STL源码,你是不会发现在STL还会有空间配置器的存在的,因为它是隐藏在一切组件(特别是容器)的背后,默默工作。如果你需要自己实现一个STL,最先设计的就应该是空间配置器,因为它是一切STL的基础。

空间配置器的必要性

小块内存会带来内存碎片问题

如果任由STL中的容器自行通过malloc分配内存,那么频繁的分配和释放内存会导致堆中有很多的外部碎片。可能堆中的所有空闲空间很大,但当新的内存分配请求到来时,没有足够大的连续内存可以分配,这将导致内存分配失败,因此这样会导致内存浪费。

小块内存的频繁申请释放会带来性能问题

malloc在开辟内存空间的时候,会附带附加的额外信息,因为系统需要靠多出来的额外信息管理内存,特别是区块越小,额外负担所占的比例就越大,更加显得浪费。
在这里插入图片描述
而且调用malloc分配内存,由于内存空间是由操作系统管理的,当我们要去开辟时,必然要进行用户态/内核态的切换,这样系统调用产生性能问题。

为了解决上述问题,STL(SGI版)的空间配置器分为两级:一级空间配置器(__malloc_alloc_template)和二级空间配置器(__default_alloc_template)。如果你申请的内存大于128个字节,那么直接调用一级空间配置器向内存申请内存,如果你申请的内存小于等于128个字节,将被认为是小内存,那么将会调用二级空间配置器直接去内存池中申请。

STL空间配置器主要分为三个文件实现:

  • <stl_construct.h> 定义了全局函数construct()和destroy(),负责对象的构造和析构。
  • <stl_alloc.h> 定义了一二级配置器,配置器统称为alloc而非allocator!
  • <stl_uninitialized.h> 定义了一些全局函数,用来填充(fill)或者复制(copy)大块内存数据,也隶属于STL标准规范。

一级空间配置器

一级空间配置器使用malloc(), free(), realloc()等C函数执行实际的内存分配,释放,重新配置等操作。此外,这个配置器提供了当内存配置错误时的处理函数oommalloc,这个函数会调用_malloc_alloc_oom_handler()这个错误处理函数,去企图释放内存,然后重新调用malloc分配内存。如此循环,直到分配成功,返回指针(所以再一定程度上提高内存分配成功)。

主要代码:

  template <int inst>
  class __malloc_alloc_template
  {
  private:
      static void *oom_malloc(size_t);   //malloc调用内存不足
        static void *oom_realloc(void *, size_t); //realloc调用内存不足
      static void (* __malloc_alloc_oom_handler)();     //错误处理函数
  public:
        static void * allocate(size_t n)                     //
        {
            void *result = malloc(n);                     //一级空间配置器直接调用malloc
            if (0 == result) 
                    result = oom_malloc(n);
             return result;
        }

        static 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);           //一级空间配置器直接调用realloc
            if (0 == result)
                   result = oom_realloc(p, new_sz);
            return result;
        }

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

其内存分配流程如下所示

在这里插入图片描述

二级空间配置器

二级空间配置器使用内存池+自由链表的形式避免了小块内存带来的碎片化,提高了分配的效率,提高了利用率。SGI的做法是先判断要开辟的大小是不是大于128,如果大于128则就认为是一块大块内存,调用一级空间配置器直接分配。否则的话就通过内存池来分配,假设要分配8个字节大小的空间,那么他就会去内存池中分配多个8个字节大小的内存块,将多余的挂在自由链表上,下一次再需要8个字节时就去自由链表上取就可以了,如果回收这8个字节的话,直接将它挂在自由链表上就可以了。

为了便于管理,二级空间配置器在分配的时候都是以8的倍数对齐。也就是说二级配置器会将任何小块内存的需求上调到8的倍数处(例如:要7个字节,会给你分配8个字节。要9个字节,会给你16个字节),尽管这样做有内碎片的问题,但是对于我们管理来说却简单了不少。因为这样的话只要维护16个free_list就可以了,free_list这16个结点分别管理大小为8,16,24,32,40,48,56,64,72,80,88,86,96,104,112,120,128字节大小的内存块就行了。

自由链表的结点类型为:

union _Obj
{
       union _Obj* _M_free_list_link;
       char _M_client_data[1];    /* The client sees this.        */
};

内存池模型:
在这里插入图片描述
free_list[16]:
在这里插入图片描述
如上图所示,自由链表是一个指针数组,有点类似与hash桶,它的数组大小为16,每个数组元素代表所挂的区块大小,比如free _ list[0]代表下面挂的是8bytes的区块,free _ list[1]代表下面挂的是16bytes的区块…….依次类推,直到free _ list[15]代表下面挂的是128bytes的区块

同时我们还有一个被称为内存池地方,以start _ free和 end _ free记录其大小,用于保存未被挂在自由链表的区块,它和自由链表构成了伙伴系统。

二级空间配置器的类:

enum { _ALIGN = 8 };              //按照基准值8的倍数进行内存操作
enum { _MAXBYTES = 128 };        //自由链表中最大的块的大小是128
enum { _NFREELISTS = 16 };       //自由链表的长度,等于_MAXBYTES/_ALIGN
template <bool threads, int inst>  //非模板类型参数
class _DefaultAllocTemplate
{
       union _Obj                      //自由链表结点的类型
       {
              _Obj* _freeListLink;         //指向自由链表结点的指针
              char _clientData[1];          //this client sees
       };
private:
       static char* _startFree;             //内存池的头指针
       static char* _endFree;               //内存池的尾指针
       static size_t _heapSize;              //记录内存池已经向系统申请了多大的内存
       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)&(~(_ALIGN-1));     //将n向上取成8的倍数
       }
       static void* _Refill(size_t n);          //在自由链表中申请内存,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);        //n要不等于0
};

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

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

  2. 向内存池申请的时候可以多申请几个,STL默认一次申请nobjs=20个,将多余的挂在自由链表上,这样能够提高效率。
    进入refill函数后,先调chunk_alloc(size_t n,size_t& nobjs)函数去内存池中申请,如果申请成功的话,再回到refill函数。
    这时候就有两种情况,如果nobjs=1的话则表示内存池只够分配一个,这时候只需要返回这个地址就可以了。否则就表示nobjs大于1,则将多余的内存块挂到自由链表上。
    如果chunk_alloc失败的话,在他内部有处理机制。

  3. 进入chunk_alloc(size_t n,size_t& nobjs )向内存池申请空间的话有三种情况:

  • 内存池剩余的空间足够nobjs*n这么大的空间,则直接分配好返回就可以了。
  • 内存池剩余的空间leftAlloc的范围是n<=leftAlloc<nobjs*n,则这时候就分配nobjs=(leftAlloc)/n这么多个的空间返回。
  • 内存池中剩余的空间连一个n都不够了,这时候就要向heap申请内存,不过在申请之前先要将内存池中剩余的内存挂到自由链表上,之后再向heap申请。
    • 如果申请成功的话,则就再调一次chunk_alloc重新分配。
    • 如果不成功的话,这时候再去自由链表中看看有没有比n大的空间,如果有就将这块空间还给内存池,然后再调一次chunk_alloc重新分配。
    • 如果没有的话,则就调用一级空间配置器分配,看看内存不足处理机制能否处理。

优缺点

引入两层配置器帮我们解决内存碎片以及效率低下的问题,但是也带来一些问题:

  • 内碎片的问题,自由链表所挂区块都是8的整数倍,因此当我们需要非8倍数的区块,往往会导致浪费,比如我只要1字节的大小,但是自由链表最低分配8块,也就是浪费了7字节,我以为这也就是通常的以空间换时间的做法,这一点在计算机科学中很常见。

  • 我们发现似乎没有释放自由链表所挂区块的函数?确实是的,由于配置器的所有方法,成员都是静态的,那么他们就是存放在静态区。释放时机就是程序结束,这样子会导致自由链表一直占用内存,自己进程可以用,其他进程却用不了。

参考资料

侯捷 《STL源码剖析》
揭秘——STL空间配置器
STL空间配置器allocator详解

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值