C++-----SGI STL空间配置器(二)二级空间配置器 __default_alloc_template

考虑到小型区块所可能造成的内存碎片问题,SGI设计了双层级配置器,第一级配置器参见博文https://blog.csdn.net/FDk_LCL/article/det ails/89457601。在这里我们讲重点介绍第二级空间配置器。

第二级空间配置器多了一一些机制,避免太多小额区块造成内存碎片的问题,(关于内存碎片可以参考博文https://blog.csdn.net/FDk_LCL/article/details/89482835)小额区块带来的其实不仅是内存碎片,配置时的额外负担也是一个大问题。额外负担是无法避免的,毕竟系统要靠这多出来的空间来管理内存。如下图所示:(该图来自《STL源码分析》)区块越小,额外负担所占的比例就越大,越显得浪费。

 SGI第二级空间配置器的做法是,如果区块够大,超过128bytes,就移交第一级配置器处理。当区块小于128bytes时,则以内存池(memory pool)管理,此法又称为层次配置(sub-allocation):每次配置一大块内存,并维护对应的自由链表(free-list)。下次若再有相同大小的内存需求,就直接从free-list拨出。如果客端释还小额区块,就由配置器回收到free-list中。为了方便管理,SGI第二级空间配置器会主动将任何小额区块的内存需求量上调至8的倍数(例如客端要求30bytes,就自动调整为32bytes),并维护16个free-list,各自管理大小分别为8、16、24、32、40、48、56、64、72、80、88、96、104、112、120、128的小额区块。

free-list的结点结构如下:

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

我们知道对于链表,我们需要在每个结点中设置一个指向下一结点的指针next,这无疑造成了一种额外负担。在这里,我们注意_Obj的类型是联合体(union),并不是我们日常建立链表结构所使用的结构体(struct)。 union _Obj  中的 char _M_client_data[1]是C/C++中常见的变长数组实现方式。union在这里实现一物两用,_M_free_list_link有两个作用:一个是指向下一块空白内存(当存在于free-list中时),一个就是供用户使用的一块内存(不存在与free-list)。free_list_link是指向下一个内存区域,对于client_data,这个数组就是指本块内存。读者可以通过下面程序加深理解:

#include <iostream> 
using namespace std;
 
union obj
{
    union obj *free_list_link;
    char client_data[1];
};
     
int main(int argc, char const *argv[])
{
    //假设这两个是要分配出去的内存。
    char mem[100] = { 0 };
    char mem1[100] = { 0 };
     
    union obj *p1 = (union obj *)mem; //用一个变量表示这个结构
     
   //p1->free_list_link 设置为下一个内存的起始段
    p1->free_list_link = (union obj *)mem1 ;
  
    //可以看到mem和client_data 两个指针值是一致的
    cout <<"mem             = " << (void *)mem << endl;
    cout <<"p1->client_data = " << (void *)p1->client_data << endl;
    //这两个输出结构一样
	cout <<"mem1            = " << (void *)mem1 << endl;
    cout <<"p1->client_link = "  << (void *)p1->free_list_link << endl;
 
    return 0;
}

这个问题搞清楚了之后,对于上面提起的当区块小于128bytes时,则以内存池(memory pool)管理。其空间的分配大致是这样的:配置器分配空间时,先从free-list中拨出,如果有,就直接从free-list拨出,该需求大小的区块位于free-list对应编号的第一位置,然后从该链表中拨出,这样该区块就不位于free-list中对应编号内,第一位置向后移动指向。如果对应编号只有一个区块,在拨出区块后,则指向0,表示free-list在该对应编号处没有该大小的区块;当下次需要这么大的区块,对应编号处没有我们需要的区块,那么则需要向free-list填充区块,继而转向内存块分配函数,然后分配所需大小的新区块(一次性缺省分配20个,不够20个就有多少分配多少,至少一个),分配成功后,第一个区块直接划给客端,其余的就填充进free-list,这样下次再有相同大小的内存时,可直接从free-list中拨出,如果一个区块都分配不出来,就调用一级配置器的内存不足处理例程。配置器还可以回收释放的内存,释放的小额内存区块划进free-list中(其实就是头插在对边编号的链表中)。

SGI缺省的free-list中都是0值,也就是说该链表中最开始没有可用的小额内存区块。(其实道理很简单,它并不知道我们的求取是多大,除非他是神仙!!!)。

这里给出了相关源码:

template <bool __threads, int __inst>
typename __default_alloc_template<__threads, __inst>::_Obj* __STL_VOLATILE
__default_alloc_template<__threads, __inst> ::_S_free_list[
] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };

总而言之,由于自由链表中最开始没有可用的小额区块,所以当我们开辟小额内存时,就需要向free-list填充区块,而填充的内存则是底层通过malloc()一次性分配一定数量的该额度区块,(实际上就是一次性分配大块内存,然后分割成对应区块大小的小块)。这样一来,在下一次需要同样大小的区块时,就直接从free-list中拨出,而不需要频繁的调用malloc()函数产生小额内存块,减少了内存碎片的产生,由于时直接从链表上直接拨出的,并没有函数调用的开销,因此也提高了效率。

下面我们跟踪STL SGI 二级空间配置器源码,对其进行详细分析:

 enum {_ALIGN = 8};    //小型区块上调边界
 enum {_MAX_BYTES = 128};  //小型区块的上限
 enum {_NFREELISTS = 16}; // free-list 个数

//----------------------------------------------------------

static char* _S_start_free;   //内存池起始位置
static char* _S_end_free;     //内存池结束位置
static size_t _S_heap_size;   //向system heap 申请内存的大小

//----------------------------------------------------------


//将__bytes 上调最临近的八的倍数
 static size_t _S_round_up(size_t __bytes) 
 { 
     return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1));
 }
//返回__bytes 大小的小额区块位于 free-list 中的编号
 static  size_t _S_freelist_index(size_t __bytes) 
 {
     return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1);
 }

 我们先来学习 allocate():

static void* allocate(size_t __n)    //分配大小为__n的区块
  {
    void* __ret = 0;

    //如果所需区块的内存大小大于128,则转向第一级空间配置器的allocate()
    if (__n > (size_t) _MAX_BYTES) {
      __ret = malloc_alloc::allocate(__n);   
    }
    else {
      //寻找16个free list中适当的一个
      _Obj* __STL_VOLATILE* __my_free_list  //这里是二级指针,便于调整free-list
          = _S_free_list + _S_freelist_index(__n);
   
#ifndef _NOTHREADS
      /*REFERENCED*/
      _Lock __lock_instance;
#endif
      _Obj* __RESTRICT __result = *__my_free_list;  //将对应位置的区块拨出(第一个)
      if (__result == 0)   //如果free-list中没有对应大小的区块
        __ret = _S_refill(_S_round_up(__n));   //调用 _S_refill()
      else {
        *__my_free_list = __result -> _M_free_list_link;   //调整free-list
        __ret = __result;
      }
    }

    return __ret;
  };

 区块从free-list中拨出的过程,如下图所示:(图片截取自《STL源码分析》)

                       

如果free-list 中没有对应大小的区块,就调用_S_refill()填充函数。

template <bool __threads, int __inst>
void*                                     //填充__n大小的区块到free-list
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n) 
{
    int __nobjs = 20;   //缺省取得20个新区块
    char* __chunk = _S_chunk_alloc(__n, __nobjs);   //调用_S_chunk_alloc()
    _Obj* __STL_VOLATILE* __my_free_list;
    _Obj* __result;
    _Obj* __current_obj;
    _Obj* __next_obj;
    int __i;
    //如果获得一个新区块,直接划给用户,free-list任然无新节点
    if (1 == __nobjs) return(__chunk);
    __my_free_list = _S_free_list + _S_freelist_index(__n);

    /* Build free list in chunk */
      __result = (_Obj*)__chunk;     //这一块返回给客端
      *__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
      //接下来的区块填补到free-list
      for (__i = 1; ; __i++) {    //从一开始,因为第0个要返回给客端
        __current_obj = __next_obj;
        __next_obj = (_Obj*)((char*)__next_obj + __n);
        if (__nobjs - 1 == __i) {
            __current_obj -> _M_free_list_link = 0;    
            break;
        } else {
            __current_obj -> _M_free_list_link = __next_obj;
        }
      }
    return(__result);
}

这样将把第一块返回给客端的剩余“大块”内存“分割”成指定大小的区块,并填充到free-list中。新的区块取自内存池,由_S_chunk_alloc()完成。

这个函数的具体思想为:

  1. 内存池剩余空间完全满足20个区块的需求量,则直接取出对应大小的空间;
  2. 内存池剩余空间不能完全满足20个区块的需求量,但是可以提供一个及以上的区块,则取出整数个区块大小的空间,正所谓有多少就取多少;
  3. 内存池剩余空间不能满足一个需求区块的大小,则对其进行以下处理:
  • 首先判断内存池中是否由残余零头的内存空间,如果由则进行回收,将其划入free-list中的适当位置;
  • 然后向system heap 申请空间,补充内存池
  1. 若heap内存充足,则空间分配成功;
  2. 若heap内存不足,出现malloc()调用失败;于时搜索适当的free-list,即搜寻free-list大于等于需求块的区块,将其编入内存池,然后递归调用_S_chunk_alloc()。函数从内存池中取空间重复上述过程。如果很不幸,free-list中没有合适的内存空间可用。这时候便调用第一级配置器的“内存不足处理例程”,以尝试着解决内存不足的问题。最终要么内存不足的问题被解决,要么抛出bad_alloc的异常。

下面来看该函数的源代码:

emplate <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size, int& __nobjs) 
{                                                 //nobjs为引用形式,返回实际获取的区块个数
    char* __result;
    size_t __total_bytes = __size * __nobjs;    //客端需求空间容量
    size_t __bytes_left = _S_end_free - _S_start_free;   //内存池剩余容量
    if (__bytes_left >= __total_bytes) {    //如果剩余容量可以满足客端需求
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return(__result);                     //直接返回所需容量大小
    } else if (__bytes_left >= __size) {      //如果剩余容量大于一个区块的内存大小
        __nobjs = (int)(__bytes_left/__size);   //内存池还有几个满足条件的区块
        __total_bytes = __size * __nobjs;
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return(__result);                  //返回这一部分内存大小给客端
    } else {                              //如果一个区块的大小都不能满足的话
        size_t __bytes_to_get = 
	  2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
        // 尝试着利用内存池残余的内存大小,将残余内存区块放在对应编号的free-list中
        if (__bytes_left > 0) {
            _Obj* __STL_VOLATILE* __my_free_list =
                        _S_free_list + _S_freelist_index(__bytes_left);

            ((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
            *__my_free_list = (_Obj*)_S_start_free;
        }
        //调用malloc(),向system heap申请内存空间
        _S_start_free = (char*)malloc(__bytes_to_get);
        if (0 == _S_start_free) {   //如果malloc调用失败
            size_t __i;
            _Obj* __STL_VOLATILE* __my_free_list;
	    _Obj* __p;             
            //尝试在free-list中是否有没用的区块
            for (__i = __size;
                 __i <= (size_t) _MAX_BYTES;
                 __i += (size_t) _ALIGN) {
                __my_free_list = _S_free_list + _S_freelist_index(__i);
                __p = *__my_free_list;
                if (0 != __p) {   //如果存在的话。重新配置内存池,递归调用_S_chunk_alloc()
                    *__my_free_list = __p -> _M_free_list_link;
                    _S_start_free = (char*)__p;
                    _S_end_free = _S_start_free + __i;
                    return(_S_chunk_alloc(__size, __nobjs));
                   
                }
            }
	    _S_end_free = 0;	//如果不存在,此时山穷水尽则使用第一级配置器内存不足处理例程
            _S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
        }
        _S_heap_size += __bytes_to_get;
        _S_end_free = _S_start_free + __bytes_to_get;
        //递归调用自己,修正__nobjs
        return(_S_chunk_alloc(__size, __nobjs));
    }
}

关于内存池在这里我们做出简要介绍:内存池实在真正使用内存之前,先申请分配一定数量的、大小相等的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样,内存池允许在运行期间以常数时间规划内存块,并且尽量避免了内存碎片的情况产生,使得内存分配的效率得到提高。

内存池实际操作结果如下图:(该图来自《STL源码分析》)

 

如上图,假如程序一开始,客端就调用 _S_chunk_alloc(32, 20),于是 malloc() 配置 40 个 32 bytes 区块,其中第 1 个返回给客端,另 19 个交给 _S_free_list [3] 维护,余 20 个留给内存池。接下来客端调用 _S_chunk_alloc(64, 20),此时 _S_free_list [7] 是空的,必须向内存池要求空间,内存池只够供应 (32 * 20) / 64 = 10 个 64 bytes 区块,就把这 10 个区块返回,第 1 个交给客端,余 9 个由 _S_free_list [7] 维护,此时内存池全空,接下来再调用 _S_chunk_alloc(96, 20),此时 _S_free_list [11] 空空如也,必须向内存池要求支持,而内存池此时也是空的,于是以 malloc() 配置 40 + n(附加量)个 96 bytes 区块,其中第 1 个交出,另 19 个交给 _S_free_list [11] 维护,余 20 + n 个区块留给内存池……如果到最后,整个 system heap 空间都不够了,malloc() 分配失败,_S_chunk_alloc() 就四处寻找有无 “尚有未用区块,且足够大” 的 free-list。找到了就挖一块交出,找不到就调用第一级配置器,第一级其实也是调用 malloc() 来配置内存,但它有 out-of-memory 处理机制,或许有机会释放其它的内存拿来此处使用。如果可以,就成功,否则发出 bad_alloc 异常。  

当然,配置也有回收区块的能力,此处的区块回收其实就是将不用的区块头插在对应编号的free-list中。

我们直接给出源码:

static void deallocate(void* __p, size_t __n)
  {
    if (__n > (size_t) _MAX_BYTES)   //如果__n大于128,就调用第一级配置器deallocate()
      malloc_alloc::deallocate(__p, __n);
    else {
      _Obj* __STL_VOLATILE*  __my_free_list
          = _S_free_list + _S_freelist_index(__n);  //获取区块在free-list中的编号
      _Obj* __q = (_Obj*)__p;              //将区块插入在free-list的对应编号中
      // acquire lock
#ifndef _NOTHREADS
      /*REFERENCED*/
      _Lock __lock_instance;
# endif /* _NOTHREADS */
      __q -> _M_free_list_link = *__my_free_list;
      *__my_free_list = __q;
      // lock is released here
    }
  }

区块回收纳入free-list的操作如图所示:(该图来自《STL源码分析》)

最后,我们其实可以发现SGI STL的第一级配置器和第二级配置器从根本上都是调用malloc()从system heap上获取内存,只不过在这里以128bytes作为限定,当大于128bytes时,我们使用第一级配置器,其allocate()和deallocate()函数就是通过malloc()和free()函数实现的,而当小于128bytes时,SGI通过自由链表(free-list)和对象池对内存进行管理,当我们需要小额区块时,只需要在对应的编号处取拨出内存块即可,当我们不想要时,只需要将其回收到free-list中即可,这样避免了频繁调用malloc()和free()的使用,在一定程度上减少了内存碎片产生。由于是直接从free-list中拨出内存块是以常数时间进行的,大大提高了内存开辟的效率。如果free-list中没有内存块是,则需要向填充free-list。我们采用以下一张图对第一级配置器和第二级配置器进行详细总结。

​​​​​​

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值