SGI STL allocate 源码剖析

SGI STL allocate源码刨析

通过剖析SGI STL二级空间配置器内存池源码深入理解其实现原理

  • 防止小块内存频繁的分配,释放,造成内存很多的碎片出来,内存没有更多的连续的大内存块。所以应用对于小块内存的操作,一般都会使用内存池来进行管理。

2.9版本

SGI STL包含了一级空间配置器和二级空间配置器,其中一级空间配置器allocator采用malloc和free来 管理内存,和C++标准库中提供的allocator是一样的,但其二级空间配置器allocator采用了基于freelist 自由链表原理的内存池机制实现内存管理。

空间配置器的相关定义

template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class vector : protected _Vector_base<_Tp, _Alloc> 

可以看到,容器的默认空间配置器是__STL_DEFAULT_ALLOCATOR( _Tp),它是一个宏定义,如下:

# ifndef __STL_DEFAULT_ALLOCATOR
	# ifdef __STL_USE_STD_ALLOCATORS
		# define __STL_DEFAULT_ALLOCATOR(T) allocator< T >
	# else
		# define __STL_DEFAULT_ALLOCATOR(T) alloc
	# endif
# endif

从上面可以看到__STL_DEFAULT_ALLOCATOR通过宏控制有两种实现,一种是allocator< T >,另一种 是alloc,这两种分别就是SGI STL的一级空间配置器和二级空间配置器的实现。

template <int __inst>
class __malloc_alloc_template // 一级空间配置器内存管理类 -- 通过malloc和free管理内存
    
template <bool threads, int inst>
class __default_alloc_template { // 二级空间配置器内存管理类 -- 通过自定义内存池实现内存管理
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;

// 考虑了线程安全问题

重要类型和变量定义

示意图

image-20211202124419715

// 内存池的粒度信息
enum {_ALIGN = 8};
enum {_MAX_BYTES = 128};
enum {_NFREELISTS = 16};
// 每一个内存chunk块的头信息
union _Obj {
	union _Obj* _M_free_list_link;
	char _M_client_data[1]; /* The client sees this. */
};
// 组织所有自由链表的数组,数组的每一个元素的类型是_Obj*,全部初始化为0
static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS];
// Chunk allocation state. 记录内存chunk块的分配情况	战备池
static char* start_free; // 指向pool的头
static char* end_free; // 指向pool的尾
static size_t heap_size; // 分配累计量

template <bool __threads, int __inst>
char* __default_alloc_template<__threads, __inst>::_S_start_free = 0;

template <bool __threads, int __inst>
char* __default_alloc_template<__threads, __inst>::_S_end_free = 0;

template <bool __threads, int __inst>
size_t __default_alloc_template<__threads, __inst>::_S_heap_size = 0;

重要的辅助接口函数

/*将 __bytes 上调至最邻近的 8 的倍数, 0 还是 0*/  —— 非常常用
static size_t _S_round_up(size_t __bytes)
{ return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1)); }  // enum {_ALIGN = 8};

/*返回 __bytes 大小的chunk块位于 free-list 中的编号*/
static size_t _S_freelist_index(size_t __bytes) {
return (((__bytes) + (size_t)_ALIGN-1) / (size_t)_ALIGN - 1); }

枚举类型的大小会随着其中的元素的大小(最大容量 char、short和int等)而发生变化,先转成size_t (这里是unsigned int)

_S_round_up

  • (size_t) _ALIGN-1) —— 0000 0000 0000 0111
  • ~((size_t) _ALIGN - 1)) —— 1111 1111 1111 1000
  • +7 然后 &,只保留了8以上的位数,非常巧妙

_S_freelist_index

  • +7 然后 / 8 - 1

重要的函数接口

// 分配内存的入口函数
static void* allocate(size_t __n)
    
// 负责把分配好的chunk块进行连接,添加到自由链表当中
static void* _S_refill(size_t __n);

// 分配相应内存字节大小的chunk块,并且给下面三个成员变量初始化
static char* _S_chunk_alloc(size_t __size, int& __nobjs);

// 把chunk块归还到内存池
static void deallocate(void* __p, size_t __n);

// 内存池扩容函数
template <bool threads, int inst>
void*
__default_alloc_template<threads, inst>::reallocate(void* __p,
                                                    size_t __old_sz,
                                                    size_t __new_sz);

二级空间配置器 allocate函数

public:

  /* __n must be > 0      */
  static void* allocate(size_t __n)
  {
    void* __ret = 0;

    if (__n > (size_t) _MAX_BYTES) {
      __ret = malloc_alloc::allocate(__n);
    }
    else {
      _Obj* __STL_VOLATILE* __my_free_list         // —— _Obj** 二级指针 + volatile         
          = _S_free_list + _S_freelist_index(__n);
      // Acquire the lock here with a constructor call.
      // This ensures that it is released in exit or during stack
      // unwinding.
#     ifndef _NOTHREADS
      /*REFERENCED*/
      _Lock __lock_instance;     // 临界区代码
#     endif
      _Obj* __RESTRICT __result = *__my_free_list; // 编号链的对应编号位置下指向其free list的空表第一个空位置的指针
      if (__result == 0)
        __ret = _S_refill(_S_round_up(__n)); // 分配内存池
      else {
        *__my_free_list = __result -> _M_free_list_link; // 指向下一个节点,嵌入式指针
        __ret = __result;
      }
    }

    return __ret; // 返回当前free list的第一个空闲位置
  };

_S_refill函数

/* Returns an object of size __n, and optionally adds to size __n free list.*/
/* We assume that __n is properly aligned.                                */
/* We hold the allocation lock.                                         */
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n) // n已通过ROUND_UP(n)调整至8的倍数,表示申请放置元素所需的单元区域的大小【即每一片片的大小】
{
    int __nobjs = 20; // 预设每个free list切分取20个区块(若pool不足时可能取不够20块),
	// 实际上这里最好可以设置为全局变量,这样方便以后想切分成30或更多区块时在全局下进行修改即可,而不是需要进入具体源码段
    
    char* __chunk = _S_chunk_alloc(__n, __nobjs); // 调用chunk_alloc()函数生成一大段区块,且nobjs是pass-by-reference,
	// 所以在内部操作时nobjs可能已被修改不再是20了!!
    
    _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); // 首先取到对应的编号区域位置,其解引用存放的是free list空表的头端第一个位置

    /* Build free list in chunk */
      
    __result = (_Obj*)__chunk; // chunk为对应编号区创建出的free list的空表的头端地址
      
    *__my_free_list = __next_obj = (_Obj*)(__chunk + __n); // result取完位置后需要返回容器去放置元素对象的,free list空表的头端必须下移一位
	// 所以*my_free_list解引用存放的指针必须下移一位即【chunk+n(因为每个区块相差的距离为元素类型的大小n),然后转化为obj*型】
    
      for (__i = 1; ; __i++) { // for循环是将nobjs个小区块串接起来
        __current_obj = __next_obj;
        __next_obj = (_Obj*)((char*)__next_obj + __n);
        if (__nobjs - 1 == __i) { // 已经 串起 __nobj - 1 个 chunck 块
            __current_obj -> _M_free_list_link = 0;
            break;
        } else {
            __current_obj -> _M_free_list_link = __next_obj;
        }
      }
    return(__result);
}

_S_chunk_alloc

/* We allocate memory in large chunks in order to avoid fragmenting     */
/* the malloc heap too much.                                            */
/* We assume that size is properly aligned.                             */
/* We hold the allocation lock.                                         */
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; // pool最终剩余的大小

    if (__bytes_left >= __total_bytes) { // pool空间足够满足20块需求,此时并不需申请新的内存空间,所以不用更新heap_size
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return(__result);
    } else if (__bytes_left >= __size) { // pool空间只足以满足一块或以上需求,此时并不需申请新的内存空间,所以不用更新heap_size
        __nobjs = (int)(__bytes_left/__size);
        __total_bytes = __size * __nobjs;
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return(__result);
    } else {         
        // pool空间不足以满足一块分配需求,即pool为碎片空间(或者可能为0)
        // 若为malloc返回0,则此时打算从对应编号区的右临近编号区的free list下取出一个空格区块
		// (为什么往右取临近,因为右边的free list下的每个单元区块的容量都比当前编号区的单元区块容量大,
		// 即能满足申请放置元素对象的大小要求!!)        
        
        size_t __bytes_to_get = 
	  2 * __total_bytes + _S_round_up(_S_heap_size >> 4); // 申请创建新的内存空间的总容量大小
        
        // Try to make use of the left-over piece.
        if (__bytes_left > 0) {
            _Obj* __STL_VOLATILE* __my_free_list =
                        _S_free_list + _S_freelist_index(__bytes_left); // 剩余内存挂到对应的index下面

            ((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
            *__my_free_list = (_Obj*)_S_start_free;
        }
        
        _S_start_free = (char*)malloc(__bytes_to_get); // 原 start_free 已经使用了
        
        if (0 == _S_start_free) { // 若申请失败即已无内存时,则从对应编号区的右临近编号区的free list下取出一个空格区块
            size_t __i;
            _Obj* __STL_VOLATILE* __my_free_list;
	    _Obj* __p;
            // Try to make do with what we have.  That can't
            // hurt.  We do not try smaller requests, since that tends
            // to result in disaster on multi-process machines.
            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));
                    // Any leftover piece will eventually make it to the
                    // right free list.
                }
            }
            
	    _S_end_free = 0;	// In case of exception.     异常情况
            _S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get); // 使用第一级配置器,再次malloc
            // This should either throw an
            // exception or remedy the situation.  Thus we assume it
            // succeeded.
        }
        
        _S_heap_size += __bytes_to_get;
        _S_end_free = _S_start_free + __bytes_to_get;
        
        return(_S_chunk_alloc(__size, __nobjs)); 
        // 分配好pool空间后需要将free list的那份分割出去,而为了提高代码效率,不写重复代码,所以通过递归再试一次。
		// 此时递归进入的肯定是 if(bytes_left >= total_bytes)或 if(bytes_left > size)的执行部分!

    }
}

异常情况,调用一级配置器

deallocate

  /* __p may not be 0 */
  static void deallocate(void* __p, size_t __n)
  {
  		// 若传入的指针p,满足其解引用的元素大小在0~128范围内,但p却不是有std::alloc生成的比如是malloc生成的
		// 此时传入这里仍可进行处理,虽然其带有cookie但不影响,只是将指针指向修改以下而已
		// 即此时std::alloc外的指针内存地址只要满足其解引用元素大小在0~128范围内均可被其回收使用,
		// 只不过如果p所指大小不是8的倍数时,可能会导致灾难!!

    if (__n > (size_t) _MAX_BYTES) // 传入的元素大小超出128,不属于std::alloc的操作范围,调用的是free
      malloc_alloc::deallocate(__p, __n);
    else {
      _Obj* __STL_VOLATILE*  __my_free_list
          = _S_free_list + _S_freelist_index(__n);
      _Obj* __q = (_Obj*)__p;

      // 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
    }
  }

一级空间配置器 allocate

  static void* allocate(size_t __n)
  {
    void* __result = malloc(__n);
    if (0 == __result) __result = _S_oom_malloc(__n);
    return __result;
  }
template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)
{
    void (* __my_malloc_handler)(); // 不带形参的函数
    void* __result;

    for (;;) { // 不停循环,知道调用成功malloc 或者 抛出异常 为止
        __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);
    }
}


#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
template <int __inst>
void (* __malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;
#endif

reallocate 扩容或者缩容

使用的不多

template <bool threads, int inst>
void*
__default_alloc_template<threads, inst>::reallocate(void* __p,  // 原地址
                                                    size_t __old_sz, // 原先大小
                                                    size_t __new_sz) // 目的大小
{
    void* __result;
    size_t __copy_sz;

    if (__old_sz > (size_t) _MAX_BYTES && __new_sz > (size_t) _MAX_BYTES) { // _MAX_BYTES 128
        return(realloc(__p, __new_sz));
    }
    if (_S_round_up(__old_sz) == _S_round_up(__new_sz)) return(__p); // 调正到8的倍数是相同的,则不用重新分配
    
    __result = allocate(__new_sz); // 申请 __new__sz
    __copy_sz = __new_sz > __old_sz? __old_sz : __new_sz;
    memcpy(__result, __p, __copy_sz);
    deallocate(__p, __old_sz); // 释放 __old__sz
    return(__result);
}

总结

SGI STL二级空间配置器内存池的实现优点:

  1. 对于每一个字节数的chunk块分配,都是给出一部分进行使用,另一部分作为备用,这个备用可以给当前字节数使用,也可以给其它字节数使用

  2. 对于备用内存池划分完chunk块以后,如果还有剩余的很小的内存块,再次分配的时候,会把这些小的内
    存块再次分配出去,备用内存池使用的干干净净!

  3. 当指定字节数内存分配失败以后,有一个异常处理的过程,bytes - 128字节所有的chunk块进行查看,如果哪个字节数有空闲的chunk块,直接借一个出去

  4. 如果上面操作失败,还会调用oom _malloc这么一个预先设置好的malloc内存分配失败以后的回调函数(一直for循环),没设置回调函数malloc失败则throwd __THROW_BAD_ALLOC 异常

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值