SGI版的空间配置器解析

这篇文章主要介绍的是STL中空间配置器的底层实现。。。 
1、为什么要存在空间配置器? 

为什么要有空间配置器呢?这主要是从两个方面来考虑的。

1、小块内存带来的内存碎片问题

  单从分配的角度来看。由于频繁分配、释放小块内存容易在堆中造成外碎片(极端情况下就是堆中空闲的内存总量满足一个请求,但是这些空闲的块都不连续,导致任何一个单独的空闲的块都无法满足这个请求)。

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

  关于性能这个问题要是再深究起来还是比较复杂的,下面我来简单的说明一下。

  开辟空间的时候,分配器会去找一块空闲块给用户,找空闲块也是需要时间的,尤其是在外碎片比较多的情况下。如果分配器其找不到,就要考虑处理假碎片现象(释放的小块空间没有合并),这时候就要将这些已经释放的的空闲块进行合并,这也是需要时间的。

  malloc在开辟空间的时候,这些空间会带有一些附加的信息,这样的话也就造成了空间的利用率有所降低,尤其是在频繁申请小块内存的时候。

频繁的申请小额空间,开销非常大。

STL中使用的是空间配置器来解决上面的问题。。。 
2、STL中的空间配置器 
STL中的空间配置器主要分为两级,一级空间配置器(__MallocAllocTemplate)和二级空间配置器(__DefaultAllocTemplate)。 

1>、一级空间配置器 
在SGI版的一级空间配置器中它的Allocate()直接使用malloc,Deallocate()直接使用free,相当于就是对malloc,free做了一层最简单的封装,一级空间配置器还提供了__Malloc_Alloc_OOM_Handler来处理内存不足的情况,如果没有设置此时就会抛出bad_alloc的异常,否则就会循环开辟空间,直到开辟成功才返回。 
一级空间配置器的流程图:

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

直接调用malloc和free来配置释放内存,简单明了。

typedef void(*MALLOCALLOC)();           //将void (*)()   重命名成MALLOCALLOC
/* oom_alloc为静态函数成员,用于处理malloc时的内存不足问题
 _malloc_alloc_handler为静态数据成员,为void(*)()类型的函数指针,用于用户自己制定内存分配策略
	*/
template<int inst>
class _MallocAllocTemplate
{
private:
       static void* _OomMalloc(size_t);       //malloc失败的时候调用的函数
       static MALLOCALLOC _MallocAllocOomHandler;         //函数指针,内存不足的时候的处理机制
public:
       static void* _Allocate(size_t n)                        //分配空间n个字节的空间
       {
              void *result=0;
              result = malloc(n);
              if (0 == result)                    //若果malloc失败,则就调OOM_malloc
                     _OomMalloc(n);
              return result;
       }
       static void _DeAllocate(void *p)                //释放这块空间
       {
              free(p);
       }
	 /*此静态成员函数接受一个void(*)()类型的函数指针作为参数,返回
	void(*)()类型的函数指针。其作用为用用户自己定制的内存调度方法替换
	_malloc_alloc_handler,由此实现类似C++的set_new_handler方法。
	*/

       static MALLOCALLOC _SetMallocHandler(MALLOCALLOC f)    //这是一个函数,参数是一个函数指针,返回值也是一个函数指针
       {
              MALLOCALLOC old = _MallocAllocOomHandler;
              _MallocAllocOomHandler = f;              //将内存分配失败的句柄设置为f(让它指向一个内存失败了,让系统去释放其他地方空间的函数)
              return old;
       }
};
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))                //重新申请内存
                     break;
       }
       return result;                              //申请成功时,则返回这块内存的地址
}
typedef _MallocAllocTemplate<0> malloc_alloc;   //一级空间配置重命名

2>、二级空间配置器 
在STL中默认使用的是二级空间配置器,二级空间配置器主要是由memory pool(内存池)+FreeList(自由链表)构成的,这种形式避免了小块内存带来的内存碎片问题,提高了分配效率,也提高了利用率。 

SGI的做法主要是: 
先判断要开辟的空间的大小是否大于128,是的话则调用一级空间配置器,因为里面存在内存不足处理机制;否则就要通过内存池来分配了。假设要分配8个字节大小的内存,那仫二级空间配置器就会去内存中开辟若干个8个字节大小的内存块,将多余的内存块挂在自由链表上,当下次想继续开辟8个字节大小的内存块直接在自由链表中取就可以了,如果回收的话则将这8个字节大小的空间直接头插到自由链表中去。为了方便管理,SGI的二级空间配置器在开辟空间的时候会将要开辟的字节的大小调整至8的倍数(比如要开辟25个大小的内存,系统会给你分配32个字节的内存)。 

自由链表的结点类型:

union Obj    //自由链表的结点类型
    {
        union Obj * FreeListLink;  //指向自由链表的结点指针
        char ClientData[1];     //客户端数据
    };

二级空间配置器的结构:

二级空间配置器的类:


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当自由链表中没有对应的内存块,系统会执行以下策略:

如果用户需要是一块n字节的区块,且n <= 128(调用第二级配置器),此时Refill填充是这样的:(需要注意的是:系统会自动将n字节扩展到8的倍数也就是RoundUP(n),再将RoundUP(n)传给Refill)用户需要n块,且自由链表中没有,因此系统会向内存池申请nobjs * n大小的内存块,默认nobjs=20

  • 如果内存池大于 nobjs * n,那么直接从内存池中取出

  • 如果内存池小于nobjs * n,但是比一块大小n要大,那么此时将内存最大可分配的块数给自由链表,并且更新nobjs为最大分配块数x (x < nobjs)

  • 如果内存池连一个区块的大小n都无法提供,那么首先先将内存池残余的零头给挂在自由链表上,然后向系统heap申请空间,申请成功则返回,申请失败则到自己的自由链表中看看还有没有可用区块返回,如果连自由链表都没了最后会调用一级配置器

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
};
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;
       //先判断要分配的空间大小是不是大于128个字节
       if (n>_MAXBYTES)      //大于_MAXBYTES个字节则认为是大块内存,直接调用一级空间配置器
       {
              ret = malloc_alloc::_Allocate(n);
       }
       else       //否则就去自由链表中找
       {
              _Obj* volatile *myFreeList = _freeList+_GetFreeListIndex(n);  //让myFreeList指向自由链表中n向上取8的整数倍
              _Obj* result = *myFreeList;
              if (result == 0)  //这个结点下面没有挂内存,则就要去内存池中申请
              {
                     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)  //如果n大于自由链表中结点所能挂的最大内存块,则就直接调用一级指针的释放函数
       {
              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)     //n表示要申请的字节个数
{
       int nobjs = 20;           //向内存池申请的时候一次性申请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)         //如果剩余的内存足够分配一个size,
       {
              nobjs=(int)(leftBytes/size);
              result = _startFree;
              _startFree +=(nobjs*size);
              return result;
       }
       else            //内存池中的内存已经不够一个size了
       {
              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 (0 == _startFree)                   //如果开辟失败
              {
                     //如果开辟失败的话,则表明系统已经没有内存了,这时候我们就要到自由链表中找一块比n还大的内存块,如果还没有的话,那就掉一级空间配置器
                     for (size_t i = size; i <(size_t)_MAXBYTES;i+=(size_t)_ALIGN)
                     {
                           _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(i);
                           _Obj* p =*myFreeList;
                           if (NULL != p)       //在自由链表找到一块内存块
                           {
                                  _startFree =(char*)p;                  
                                  //将这个内存块摘下来给内存池
                                  *myFreeList = p->_freeListLink;
                                  _endFree = _startFree + i;
                                  return _chunkAlloc(size, nobjs);  //内存池开辟好的话,就再调一次chunk分配内存
                           }
                     }
                     //要是再找不到的话,就调一级空间配置器,其中有内存不足处理机制,要是还不行的话,他会自动抛出异常
                     _endFree = NULL;
                     _startFree=(char*)malloc_alloc::_Allocate(NewBytes);
              }      
              //开辟成功的,就更新heapSize(记录总共向系统申请了多少内存),,更新_endFree
              _heapSize += NewBytes;
              _endFree = _startFree + NewBytes;
              return _chunkAlloc(size, nobjs);             //内存池开辟好的话,就再调一次chunk分配内存
       }
}
 
typedef _DefaultAllocTemplate<0,0>  default_alloc;

也就是STL可能存在的问题,通俗的讲就是优缺点吧

我们知道,引入相对的复杂的空间配置器,主要源自两点:

1. 频繁使用malloc,free开辟释放小块内存带来的性能效率的低下 
2. 内存碎片问题,导致不连续内存不可用的浪费

引入两层配置器帮我们解决以上的问题,但是也带来一些问题:

  1. 内碎片的问题,自由链表所挂区块都是8的整数倍,因此当我们需要非8倍数的区块,往往会导致浪费,比如我只要1字节的大小,但是自由链表最低分配8块,也就是浪费了7字节,我以为这也就是通常的以空间换时间的做法,这一点在计算机科学中很常见。
  2. 我们发现似乎没有释放自由链表所挂区块的函数?确实是的,由于配置器的所有方法,成员都是静态的,那么他们就是存放在静态区。释放时机就是程序结束,这样子会导致自由链表一直占用内存,自己进程可以用,其他进程却用不了。

猜你喜欢

转载自blog.csdn.net/qq_38936579/article/details/81708459