STL源码剖析(二):空间配置器alloctor(2)

空间的配置和释放:alloc配置器

stl标准的空间配置为allocator,SGI也实现了这个版本的配置器,但由于其效率不高,SGI STL默认的配置器为alloc。

考虑到小型区块可能造成的内存破碎问题,SGI设计了双层级配置器:当配置区块超过128bytes时,便调用第一级配置器;如果小于128bytes,为降低额外开销overhead(稍后再讲),便采用复杂的memory pool整理方式。

为使得alloc符合STL规格,SGI将Alloc做了一层封装:

第一级配置器 __malloc_alloc_template

template <int __inst>
class __malloc_alloc_template {

private:
  
  // 以下函数将用来处理内存不足的情况
  static void* _S_oom_malloc(size_t);
  static void* _S_oom_realloc(void*, size_t);

#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
  static void (* __malloc_alloc_oom_handler)();
#endif

public:

  // 第一级配置器直接调用 malloc()
  static void* allocate(size_t __n)
  {
    void* __result = malloc(__n);
    // 以下无法满足需求时,改用 _S_oom_malloc()
    if (0 == __result) __result = _S_oom_malloc(__n);
    return __result;
  }

  // 第一级配置器直接调用 free()
  static void deallocate(void* __p, size_t /* __n */)
  {
    free(__p);
  }
  
  // 第一级配置器直接调用 realloc()
  static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
  {
    void* __result = realloc(__p, __new_sz);
    // 以下无法满足需求时,改用 _S_oom_realloc()
    if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
    return __result;
  }

  // 以下仿真 C++ 的 set_new_handler(),可以通过它指定自己的 out-of-memory handler
  // 为什么不使用 C++ new-handler 机制,因为第一级配置器并没有 ::operator new 来配置内存
  // 可能是历史原因
  static void (* __set_malloc_handler(void (*__f)()))()
  {
    void (* __old)() = __malloc_alloc_oom_handler;
    __malloc_alloc_oom_handler = __f;
    return(__old);
  }
};

当内存分配失败后,将通过out-of-memory handler尝试分配新内存,以_S_oom_malloc为例:

#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
// 初值为0,由客户自行设定
template <int __inst>
void (* __malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;
#endif

template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_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);
    }
}

C++ new handler机制:你可以要求系统在内存配置需求无法满足时,在抛出std::bad_alloc异常状态前,调用一个你所指定的函数,这个函数就称为new-handler。

第二级配置器 __default_alloc_template

malloc向系统申请内存时,需配置额外负担以管理内存,内存块越小,额外负担所占比例就越大。而在C++中,大部分内存都是小块的(如变量),如果小块内存也直接malloc,将造成大量的空间浪费。

 在上图中,蓝色部分为实际数据,绿色部分为字节补齐部分,上下两端的红色部记录内存块的大小(这也是为什么,在free一块内存时,只需要指导这块内存的首地址即可)。红色的部分称为cookie,两个cookie各占4字节。至于为什么上下两端都有,暂时不知道

因此,STL尽量减少malloc的次数,设置了如下的自由链表free-lists:

 free-list的节点结构为:

union _Obj {
     union _Obj* _M_free_list_link;  // 利用联合体特点
     char _M_client_data[1];    /* The client sees this.        */
};

free-list有三个值得注意的点:

  1. union的妙用,可以节省自由链表中指针的额外开销:在当前节点未分配前,节点中的值为下一个节点的地址;当节点分配后,free_list头指针指向它的下一个节点,而它内部的值被替换为用户的data值。=》从这也看出,自由链表从头节点分配效率更高。
  2. 区块按照8字节对齐:8、16、24、...、128bytes
  3. 配置器不仅负责区块的分配器,也负责区块的回收,也就是把一个区块挂到对应的free-list

如果自由链表中没有可分配的节点了,将从内存池获取内存并分配节点:

  // 配置一大块空间,可容纳nobjs个大小为 size的区块
  // 如果内存池空间也不够了,nobjs可能会降低
  static char* _S_chunk_alloc(size_t __size, int& __nobjs);

  // Chunk allocation state.
  static char* _S_start_free;  // 内存池起始位置。只在 _S_chunk_alloc() 中变化
  static char* _S_end_free;    // 内存池结束位置。只在 _S_chunk_alloc() 中变化
  static size_t _S_heap_size;

 同样,二级配置器也需要符合STL标准接口,需要封装函数 allocate():

  static void* allocate(size_t __n)
  {
    void* __ret = 0;

    // 如果需求区块大于 128 bytes,就转调用第一级配置
    if (__n > (size_t) _MAX_BYTES) {
      __ret = malloc_alloc::allocate(__n);
    }
    else {
      // 根据申请空间的大小寻找相应的空闲链表(16个空闲链表中的一个)
      _Obj* __STL_VOLATILE* __my_free_list
          = _S_free_list + _S_freelist_index(__n);
      _Obj* __RESTRICT __result = *__my_free_list;
      // 空闲链表没有可用数据块,就将区块大小先调整至 8 倍数边界,然后调用 _S_refill() 重新填充
      if (__result == 0)
        __ret = _S_refill(_S_round_up(__n));
      else {
        // 如果空闲链表中有空闲数据块,则取出一个,并把空闲链表的指针指向下一个数据块  
        *__my_free_list = __result -> _M_free_list_link;
        __ret = __result;
      }
    }

    return __ret;
  };

 回收区块:deallocate(),其中关于线程安全的代码未粘贴

  // 空间释放函数 deallocate()
  static void deallocate(void* __p, size_t __n)
  {
    // 大于 128 bytes,就调用第一级配置器的释放
    if (__n > (size_t) _MAX_BYTES)   
      malloc_alloc::deallocate(__p, __n);   
    else {
      // 否则将空间回收到相应空闲链表(由释放块的大小决定)中  
      _Obj* __STL_VOLATILE*  __my_free_list
          = _S_free_list + _S_freelist_index(__n);   
      _Obj* __q = (_Obj*)__p;
      // 调整空闲链表,回收数据块
      __q -> _M_free_list_link = *__my_free_list;   
      *__my_free_list = __q;
    }
  }

free lists的填充

在allocate时,如果没有可用区块时,就调用refill(),为free list重新填充空间。新的空间取自内存池(经由chunk_alloc()完成),取得20个新节点,如果内存池空间也不足,获得的节点数可能小于20。以下为refill()源码:

template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
    int __nobjs = 20;
    // 调用 _S_chunk_alloc(),缺省取 20 个区块作为 free list 的新节点
    char* __chunk = _S_chunk_alloc(__n, __nobjs);
    _Obj* __STL_VOLATILE* __my_free_list;
    _Obj* __result;
    _Obj* __current_obj;
    _Obj* __next_obj;
    int __i;

    // 如果只获得一个数据块,那么这个数据块就直接分给调用者,空闲链表中不会增加新节点
    if (1 == __nobjs) return(__chunk);
    // 否则根据申请数据块的大小找到相应空闲链表 
    __my_free_list = _S_free_list + _S_freelist_index(__n);   

    /* 在chunk空间建立free list */
      __result = (_Obj*)__chunk;
      *__my_free_list = __next_obj = (_Obj*)(__chunk + __n);  
      // 从1开始,第0个数据块给调用者,地址访问即chunk~chunk + n - 1  
      for (__i = 1; ; __i++) {    // 头插法
        __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);
}

内存池取内存的过程chunk_alloc() :

// 从内存池中取空间
template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size, 
                                                            int& __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);
	    // 内存池的剩余空间分给合适的空闲链表
        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;
        }
        // 配置 heap 空间,用来补充内存池
        _S_start_free = (char*)malloc(__bytes_to_get); 
        if (0 == _S_start_free) {  // heap 空间不足,malloc() 失败
            size_t __i;
            _Obj* __STL_VOLATILE* __my_free_list;
	         _Obj* __p;
            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) {
                    *__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;
        return(_S_chunk_alloc(__size, __nobjs));  // 递归调用自己
    }
}
  • 内存池空间完全满足容量,直接分配
  • 内存池空间不足,但能分配一个以上的区块,则先给分配出来给调用方用着先
  • 如果一个区块都分配不了了,就准备扩充内存池了,在此之前,需将内存池中较小的零头区块分配给合适的free list。(96bytes的没有,可能有16bytes的)
  • 调用malloc补充内存池
    • 假如malloc这里配置了40+n(附加量)个96bytes的区块,一个交给调用方,19个交给free list,另外20+n(附加量)个交给内存池。
  • 如果heap空间不足,malloc失败,那就从大块的free list找有没有空闲的节点,有的话就把这个区块释放了,拿给小区块的free list用用。
  • 如果free list中的大区快也都用完了,只能借助第一级配置器(借用里面的out-of-memory机制,看有没有哪个程序可以释放一部分资源出来)

内存池的实际操作:

tip:

对于容器,如vector,默认的配置器为alloc(也可以自己指定为allocator),而alloc配置器默认使用二级配置器。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值