揭秘——STL空间配置器

为什么要有空间配置器呢?这主要是从两个方面来考虑的。
1、小块内存带来的内存碎片问题
  单从分配的角度来看。由于频繁分配、释放小块内存容易在堆中造成外碎片(极端情况下就是堆中空闲的内存总量满足一个请求,但是这些空闲的块都不连续,导致任何一个单独的空闲的块都无法满足这个请求)。

2、小块内存频繁申请释放带来的性能问题。
  关于性能这个问题要是再深究起来还是比较复杂的,下面我来简单的说明一下。
  开辟空间的时候,分配器会去找一块空闲块给用户,找空闲块也是需要时间的,尤其是在外碎片比较多的情况下。如果分配器其找不到,就要考虑处理假碎片现象(释放的小块空间没有合并),这时候就要将这些已经释放的的空闲块进行合并,这也是需要时间的。
  malloc在开辟空间的时候,这些空间会带有一些附加的信息,这样的话也就造成了空间的利用率有所降低,尤其是在频繁申请小块内存的时候。


 为了解决上面这些问题,所以就提出有了内存池的概念。内存池最基本的思想就是一次向heap申请一块很大的内存(内存池),如果申请小块内存的话就直接到内存池中去要。这样的话,就能够有效的解决上面所提到的问题。


下面我们来剖析一下STL里面的空间配置器(SGI版)。
   STL里面的空间配置主要分为两级,一级空间配置器(__malloc_alloc_template)和二级空间配置器(__default_alloc_template)。在STL中默认如果要分配的内存大于128个字节的话就是大块内存,调用一级空间配置器直接向系统申请,如果小于等于128个字节的话则认为是小内存,则就去内存池中申请。一级空间配置器很简单,直接封装了malloc和free处理,增加_malloc_alloc_oom_handle处理机制。二级空间配置器才是STL的精华,二级空间配置器主要由memoryPool+freelist构成。

一级空间配置器:
执行流程:


 SGI以malloc来配置内存。当malloc()失败后,就调用oom_alloc(),如果客户端没有设置内存不足处理机制,则就直接抛出bad_alloc异常信息,或者直接终止程序。如果客户端设置了内存不足处理机制,则他就会一直调用内存处理机制,企图在某次调用之后获得一块足够的内存。但是如果内存不足处理机制设计不好的话,存在死循环的危险。
注意:"内存不足处理机制"是客户端的责任,设置"内存不足处理介质"也是客户端的责任。

程序:
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:14px;">typedef void(*MALLOCALLOC)();           //将void (*)()   重命名成MALLOCALLOC  
  2. template<int inst>  
  3. class _MallocAllocTemplate  
  4. {  
  5. private:  
  6.        static void* _OomMalloc(size_t);       //malloc失败的时候调用的函数  
  7.        static MALLOCALLOC _MallocAllocOomHandler;         //函数指针,内存不足的时候的处理机制  
  8. public:  
  9.        static void* _Allocate(size_t n)                        //分配空间n个字节的空间  
  10.        {  
  11.               void *result=0;  
  12.               result = malloc(n);  
  13.               if (0 == result)                    //若果malloc失败,则就调OOM_malloc  
  14.                      _OomMalloc(n);  
  15.               return result;  
  16.        }  
  17.        static void _DeAllocate(void *p)                //释放这块空间  
  18.        {  
  19.               free(p);  
  20.        }  
  21.        static MALLOCALLOC _SetMallocHandler(MALLOCALLOC f)    //这是一个函数,参数是一个函数指针,返回值也是一个函数指针  
  22.        {  
  23.               MALLOCALLOC old = _MallocAllocOomHandler;  
  24.               _MallocAllocOomHandler = f;              //将内存分配失败的句柄设置为f(让它指向一个内存失败了,让系统去释放其他地方空间的函数)  
  25.               return old;  
  26.        }  
  27. };  
  28. template<int inst>  
  29. void(* _MallocAllocTemplate<inst>::_MallocAllocOomHandler)()=0;    //默认不设置内存不足处理机制  
  30. template<int inst>  
  31. void* _MallocAllocTemplate<inst>::_OomMalloc(size_t n)  
  32. {  
  33.        MALLOCALLOC _MyMallocHandler;     //定义一个函数指针  
  34.        void *result;                 
  35.        while (1)  
  36.        {  
  37.               _MyMallocHandler = _MallocAllocOomHandler;  
  38.               if (0 == _MyMallocHandler)                  //没有设置内存不足处理机制  
  39.                      throw std::bad_alloc();                  //则抛出异常  
  40.               (*_MyMallocHandler)();                 //调用内存不足处理的函数,申请释放其他地方的内存  
  41.               if (result = malloc(n))                //重新申请内存  
  42.                      break;  
  43.        }  
  44.        return result;                              //申请成功时,则返回这块内存的地址  
  45. }  
  46. typedef _MallocAllocTemplate<0> malloc_alloc;</span>  



二级空间配置器:
  二级空间配置器使用内存池+自由链表的形式避免了小块内存带来的碎片化,提高了分配的效率,提高了利用率。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字节大小的内存块就行了。

自由链表的结点类型:
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:14px;">union _Obj  
  2. {  
  3.        union _Obj* _M_free_list_link;  
  4.        char _M_client_data[1];    /* The client sees this.        */  
  5. };</span>  


内存池模型:


  为了将自由链表下面的结点串起来,又不引入额外的指针,所以我们要学会一物两用,因为在自由链表下面挂的最小的内存块都是8个字节,足够存放一个地址,所以我们就在这些内存块里面存放其他内存块的地址,这样的话就将这些内存块链接起来了。



二级空间配置器的类:
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:14px;">enum { _ALIGN = 8 };              //按照基准值8的倍数进行内存操作  
  2. enum { _MAXBYTES = 128 };        //自由链表中最大的块的大小是128  
  3. enum { _NFREELISTS = 16 };       //自由链表的长度,等于_MAXBYTES/_ALIGN  
  4. template <bool threads, int inst>  //非模板类型参数  
  5. class _DefaultAllocTemplate  
  6. {  
  7.        union _Obj                      //自由链表结点的类型  
  8.        {  
  9.               _Obj* _freeListLink;         //指向自由链表结点的指针  
  10.               char _clientData[1];          //this client sees  
  11.        };  
  12. private:  
  13.        static char* _startFree;             //内存池的头指针  
  14.        static char* _endFree;               //内存池的尾指针  
  15.        static size_t _heapSize;              //记录内存池已经向系统申请了多大的内存  
  16.        static _Obj* volatile _freeList[_NFREELISTS];    //自由链表  
  17. private:  
  18.        static size_t _GetFreeListIndex(size_t bytes)   //得到这个字节对应在自由链表中应取的位置  
  19.        {  
  20.               return (bytes +(size_t) _ALIGN - 1) / (size_t)_ALIGN - 1;       
  21.        }  
  22.        static size_t _GetRoundUp(size_t bytes)        //对这个字节向上取成8的倍数  
  23.        {  
  24.               return (bytes + (size_t)_ALIGN - 1)&(~(_ALIGN-1));     //将n向上取成8的倍数  
  25.        }  
  26.        static void* _Refill(size_t n);          //在自由链表中申请内存,n表示要的内存的大小  
  27.        static char* _chunkAlloc(size_t size,int& nobjs);    //在内存池中申请内存nobjs个对象,每个对象size个大小  
  28. public:  
  29.        static void* Allocate(size_t n);      //n要大于0  
  30.        static void DeAllocate(void *p,size_t n);        //n要不等于0  
  31. };</span>  


二级空间配置器的逻辑步骤:
假如现在申请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 )向内存池申请空间的话有三种情况:
  3.1、内存池剩余的空间足够nobjs*n这么大的空间,则直接分配好返回就可以了。
  3.2、内存池剩余的空间leftAlloc的范围是n<=leftAlloc<nobjs*n,则这时候就分配nobjs=(leftAlloc)/n这么多个的空间返回。
  3.3、内存池中剩余的空间连一个n都不够了,这时候就要向heap申请内存,不过在申请之前先要将内存池中剩余的内存挂到自由链表上,之后再向heap申请。
   3.3.1、如果申请成功的话,则就再调一次chunk_alloc重新分配。
   3.3.2、如果不成功的话,这时候再去自由链表中看看有没有比n大的空间,如果有就将这块空间还给内存池,然后再调一次chunk_alloc重新分配。
   3.3.3、如果没有的话,则就调用一级空间配置器分配,看看内存不足处理机制能否处理。

流程框图:


代码实现:
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <span style="font-size:14px;">enum { _ALIGN = 8 };              //按照基准值8的倍数进行内存操作  
  2. enum { _MAXBYTES = 128 };        //自由链表中最大的块的大小是128  
  3. enum { _NFREELISTS = 16 };       //自由链表的长度,等于_MAXBYTES/_ALIGN  
  4. template <bool threads, int inst>  //非模板类型参数  
  5. class _DefaultAllocTemplate  
  6. {  
  7.        union _Obj                      //自由链表结点的类型  
  8.        {  
  9.               _Obj* _freeListLink;         //指向自由链表结点的指针  
  10.               char _clientData[1];          //this client sees  
  11.        };  
  12. private:  
  13.        static char* _startFree;             //内存池的头指针  
  14.        static char* _endFree;               //内存池的尾指针  
  15.        static size_t _heapSize;              //记录内存池已经向系统申请了多大的内存  
  16.        static _Obj* volatile _freeList[_NFREELISTS];    //自由链表  
  17. private:  
  18.        static size_t _GetFreeListIndex(size_t bytes)   //得到这个字节对应在自由链表中应取的位置  
  19.        {  
  20.               return (bytes +(size_t) _ALIGN - 1) / (size_t)_ALIGN - 1;       
  21.        }  
  22.        static size_t _GetRoundUp(size_t bytes)        //对这个字节向上取成8的倍数  
  23.        {  
  24.               return (bytes + (size_t)_ALIGN - 1)&(~(_ALIGN-1));     //将n向上取成8的倍数  
  25.        }  
  26.        static void* _Refill(size_t n);          //在自由链表中申请内存,n表示要的内存的大小  
  27.        static char* _chunkAlloc(size_t size,int& nobjs);    //在内存池中申请内存nobjs个对象,每个对象size个大小  
  28. public:  
  29.        static void* Allocate(size_t n);      //n要大于0  
  30.        static void DeAllocate(void *p,size_t n);        //n要不等于0  
  31. };  
  32. template<bool threads,int inst>  
  33. char* _DefaultAllocTemplate<threads,inst>::_startFree = 0;        //内存池的头指针  
  34. template<bool threads, int inst>  
  35. char* _DefaultAllocTemplate<threads, inst>::_endFree=0;           //内存池的尾指针  
  36. template<bool threads, int inst>  
  37. size_t _DefaultAllocTemplate<threads, inst>::_heapSize = 0;              //记录内存池已经向系统申请了多大的内存  
  38. template<bool threads, int inst>  
  39. typename _DefaultAllocTemplate<threads, inst>::_Obj* volatile      //前面加typename表示后面是个类型  
  40. _DefaultAllocTemplate<threads, inst>::_freeList[_NFREELISTS] = {0};    //自由链表  
  41.   
  42. template<bool threads, int inst>  
  43. void* _DefaultAllocTemplate<threads, inst>::Allocate(size_t n)    //分配空间  
  44. {  
  45.        void *ret;  
  46.        //先判断要分配的空间大小是不是大于128个字节  
  47.        if (n>_MAXBYTES)      //大于_MAXBYTES个字节则认为是大块内存,直接调用一级空间配置器  
  48.        {  
  49.               ret = malloc_alloc::_Allocate(n);  
  50.        }  
  51.        else       //否则就去自由链表中找  
  52.        {  
  53.               _Obj* volatile *myFreeList = _freeList+_GetFreeListIndex(n);  //让myFreeList指向自由链表中n向上取8的整数倍  
  54.               _Obj* result = *myFreeList;  
  55.               if (result == 0)  //这个结点下面没有挂内存,则就要去内存池中申请  
  56.               {  
  57.                      ret = _Refill(_GetRoundUp(n));      //到内存池中申请  
  58.               }  
  59.               else            //已经在自由链表上找到了内存  
  60.               {  
  61.                      *myFreeList= result->_freeListLink;      //把第二块空间的地址放到自由链表上  
  62.                      ret = result;  
  63.               }  
  64.        }  
  65.        return ret;  
  66. }  
  67. template<bool threads, int inst>  
  68. void _DefaultAllocTemplate<threads, inst>::DeAllocate(void *p, size_t n)   //回收空间  
  69. {  
  70.        //先判断这个字节的大小  
  71.        if (n > _MAXBYTES)  //如果n大于自由链表中结点所能挂的最大内存块,则就直接调用一级指针的释放函数  
  72.        {  
  73.               malloc_alloc::_DeAllocate(p);  
  74.        }  
  75.        else        //将这块内存回收到自由链表中  
  76.        {  
  77.               _Obj* q = (_Obj*)p;  
  78.               _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(n);  
  79.               q->_freeListLink = *myFreeList;  
  80.               *myFreeList = q;  
  81.        }  
  82. }  
  83.   
  84. template<bool threads,int inst>  
  85. void* _DefaultAllocTemplate<threads, inst>::_Refill(size_t n)     //n表示要申请的字节个数  
  86. {  
  87.        int nobjs = 20;           //向内存池申请的时候一次性申请20个  
  88.        char* chunk = _chunkAlloc(n,nobjs);    //因为现在链表中没有,所以要想内存池中申请,多余的再挂到自由链表下面  
  89.        if (1 == nobjs)          //只分配到了一个对象  
  90.        {  
  91.               return chunk;  
  92.        }  
  93.        _Obj* ret = (_Obj*)chunk;                  //将申请的第一个对象作为返回值  
  94.        _Obj* volatile *myFreeList = _freeList+ _GetFreeListIndex(n);  
  95.        *myFreeList =(_Obj*)(chunk+n);             //将第二个对象的地址放到自由链表中  
  96.        _Obj* cur= *myFreeList;  
  97.        _Obj* next=0;  
  98.        cur->_freeListLink = 0;  
  99.        for (int i = 2; i < nobjs; ++i)             //将剩下的块挂到自由链表上  
  100.        {  
  101.               next= (_Obj*)(chunk + n*i);  
  102.               cur->_freeListLink = next;  
  103.               cur = next;  
  104.        }  
  105.        cur->_freeListLink = 0;  
  106.        return ret;  
  107. }  
  108. template<bool threads, int inst>  
  109. char* _DefaultAllocTemplate<threads, inst>::_chunkAlloc(size_t size, int& nobjs)  //向系统中申请内存  
  110. {  
  111.        char* result = 0;  
  112.        size_t totalBytes = size*nobjs;        //总共请求的内存大小  
  113.        size_t leftBytes = _endFree - _startFree;      //内存池剩余的大小  
  114.        if (leftBytes>=totalBytes)     //如果剩余的大小大于等于申请的大小,则返回这个这内存  
  115.        {  
  116.               result = _startFree;  
  117.               _startFree += totalBytes;  
  118.               return result;  
  119.        }  
  120.        else if (leftBytes>size)         //如果剩余的内存足够分配一个size,  
  121.        {  
  122.               nobjs=(int)(leftBytes/size);  
  123.               result = _startFree;  
  124.               _startFree +=(nobjs*size);  
  125.               return result;  
  126.        }  
  127.        else            //内存池中的内存已经不够一个size了  
  128.        {  
  129.               size_t NewBytes = 2 * totalBytes+_GetRoundUp(_heapSize>>4);       //内存池要开辟的新的容量  
  130.               if (leftBytes >0)  //剩余的内存挂到自由链表上  
  131.               {  
  132.                      _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(leftBytes);  
  133.                      ((_Obj*)_startFree)->_freeListLink = *myFreeList;  
  134.                      *myFreeList = (_Obj*)_startFree;  
  135.               }  
  136.                 
  137.               //开辟新的内存  
  138.               _startFree = (char*)malloc(NewBytes);  
  139.               if (0 == _startFree)                   //如果开辟失败  
  140.               {  
  141.                      //如果开辟失败的话,则表明系统已经没有内存了,这时候我们就要到自由链表中找一块比n还大的内存块,如果还没有的话,那就掉一级空间配置器  
  142.                      for (size_t i = size; i <(size_t)_MAXBYTES;i+=(size_t)_ALIGN)  
  143.                      {  
  144.                            _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(i);  
  145.                            _Obj* p =*myFreeList;  
  146.                            if (NULL != p)       //在自由链表找到一块内存块  
  147.                            {  
  148.                                   _startFree =(char*)p;                    
  149.                                   //将这个内存块摘下来给内存池  
  150.                                   *myFreeList = p->_freeListLink;  
  151.                                   _endFree = _startFree + i;  
  152.                                   return _chunkAlloc(size, nobjs);  //内存池开辟好的话,就再调一次chunk分配内存  
  153.                            }  
  154.                      }  
  155.                      //要是再找不到的话,就调一级空间配置器,其中有内存不足处理机制,要是还不行的话,他会自动抛出异常  
  156.                      _endFree = NULL;  
  157.                      _startFree=(char*)malloc_alloc::_Allocate(NewBytes);  
  158.               }        
  159.               //开辟成功的,就更新heapSize(记录总共向系统申请了多少内存),,更新_endFree  
  160.               _heapSize += NewBytes;  
  161.               _endFree = _startFree + NewBytes;  
  162.               return _chunkAlloc(size, nobjs);             //内存池开辟好的话,就再调一次chunk分配内存  
  163.        }  
  164. }  
  165.   
  166. typedef _DefaultAllocTemplate<0,0>  default_alloc;</span>  


空间配置器的其他问题:
1、在空间配置器中所有的函数和变量都是静态的,所以他们在程序结束的时候才会被释放发。二级空间配置器中没有将申请的内存还给操作系统,只是将他们挂在自由链表上。所以说只有当你的程序结束了之后才会将开辟的内存还给操作系统。

2、由于它没有将内存还给操作系统,所以就会出现二种极端的情况。
 2.1、假如我不断的开辟小块内存,最后将整个heap上的内存都挂在了自由链表上,但是都没有用这些空间,再想要开辟一个大块内存的话会开辟失败。
 2.2、再比如我不断的开辟char,最后将整个heap内存全挂在了自由链表的第一个结点的后面,这时候我再想开辟一个16个字节的内存,也会失败。
  或许我比较杞人忧天吧,总的来说上面的情况只是小概率情况。如果非得想办法解决的话,我想的是:针对2.1我们可以引入释放二级空间配置器的方法,但是这个释放比较麻烦。针对2.2我们可以合并自由链表上的连续的小的内存块。

3、二级空间配置器会造成内碎片问题,极端的情况下一直申请char,则就会浪费7/8的空间。但是整体来说,空间配置器的性能还是蛮高的。


总的流程:



转自:http://blog.csdn.net/lf_2016/article/details/53511648
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值