SGI STL空间配置器内存池剖析注解

6 篇文章 0 订阅
4 篇文章 0 订阅

C++的STL中的容器都是需要使用空间配置器来分配内存资源的

template<typename T, typename _Alloc=allocator<T>>
class vector
{
};

STL有很多版本,其中SGI STL是C++ STL的“民间版本”,因为其代码可读性很强。所以实际C++ STL和SGI STL是不同的。

C++ STL中的空间配置器默认是allocator

空间配置器的主要作用就是:

  • 分离了对象的内存开辟和对象的构造
  • 分离了对象的内存释放和对象的析构

空间配置器中主要包括了四个方法:

  • allocate:负责容器的内存空间开辟
  • deallocate:负责容器的内存空间释放
  • construct:负责在容器中构造对象
  • destroy:负责析构容器中的对象

SGI STL中的空间配置器有两个:一级allocator和二级allocator

一级allocator的内存管理是通过malloc/free实现的,二级allocator的内存管理是通过自定义内存池来实现的。

两种重要辅助函数_S_round_up(size_t)和_S_freelist_index(size_t)

// 计算出大于__bytes的最临近 8 的倍数的内存大小
size_t _S_round_up(size_t __bytes)
{
	return ((_bytes) + (size_t)_AGLIN - 1) & ~((size_t)_AGLiN - 1);
}

// 在_S_free_list[]中找到可以分配__byte内存大小的freelist
size_t _S_freelist_index(size_t __bytes)
{
	return ((_bytes) + (size_t)_AGLIN - 1) / (size_t)AGLIN - 1;
}

内存分配函数:allocate

/*
[0][1][2][3][...][14][15]
↓  ↓  ↓  ↓   ↓    ↓   ↓
当锁定到有对应大小内存块的freelist之后,每次分配freelist的头结点给外界
*/
/* __n must be > 0      */
static void* allocate(size_t __n)
{
	void* __ret = 0;
	
	// 当需要分配的空间大小大于128字节的时候,直接使用malloc进行分配
	if (__n > (size_t) _MAX_BYTES) {
	  __ret = malloc_alloc::allocate(__n);
	}
	
	// 需要分配的空间<128字节
	else {
	  // __my_free_list指向_S_free_list对应位置上的freelist
	  // 使用__my_free_list这个二级指针来遍历_S_free_list这个指针数组
	  // 找到可以分配_n内存大小的freelist
	  _Obj* __STL_VOLATILE* __my_free_list
		  = _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*/
	  // RAII管理锁,用OOP的方式来加解锁保证了allocate()的线程安全
	  // 栈上的对象出作用域之后自动析构的方式来释放锁
	  _Lock __lock_instance; 
	#     endif
	
	  _Obj* __RESTRICT __result = *__my_free_list;
	  if (__result == 0)
		// 如果freelist[index]上没有内存块,就分配内存块到该freelist上
		__ret = _S_refill(_S_round_up(__n));
	  else {
		// freelist[index]头结点的下一个节点成为头结点,而freelist原来的头结点分配给外界
		*__my_free_list = __result -> _M_free_list_link;
		__ret = __result;
	  }
	}
	
	return __ret;
};

构造freelist函数:_S_refill

// 分配_n大小的内存块,并将chunk块挂载到_S_free_list[_S_freelist_index(__n)]自由链表上
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
    int __nobjs = 20;
    char* __chunk = _S_chunk_alloc(__n, __nobjs); // 给freelist分配chunk块
    _Obj* __STL_VOLATILE* __my_free_list; // 用于遍历_S_free_list
    _Obj* __result;
    _Obj* __current_obj;
    _Obj* __next_obj;
    int __i;

	// 如果只有一个chunk块的话,那么就直接返回出去
	// 因为没有多余的chunk块了,所以也不需要将chunk块挂载到相应的freelist上
    if (1 == __nobjs) return(__chunk);
    __my_free_list = _S_free_list + _S_freelist_index(__n);

    /* Build free list in chunk */
	  // 分配一个chunk块出去,并将剩余chunk块挂载到*__my_free_list上
      __result = (_Obj*)__chunk;
      *__my_free_list = __next_obj = (_Obj*)(__chunk + __n);

	  // 将剩余的内存块切分成chunk块大小的内存块并串联起来形成一条静态链表
      for (__i = 1; ; __i++) {
        __current_obj = __next_obj;
        __next_obj = (_Obj*)((char*)__next_obj + __n);
	  
	  	// 如果当前chunk是链表中的最后一块,那么该节点就指向nullptr
	  	// 否则当前位置的节点就指向下一个位置的chunk块
        if (__nobjs - 1 == __i) {
            __current_obj -> _M_free_list_link = 0;
            break;
        } else {
            __current_obj -> _M_free_list_link = __next_obj;
        }
		
      }
	  
    return(__result); // 链表的第一个节点被分配出去使用
}

为freelist提供内存块函数:_S_chunk_alloc

// 给对应的freelist申请大内存,为之后将大内存分割成以chunk为单位的静态链表做准备
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; // __nobjs个节点的内存大小
    size_t __bytes_left = _S_end_free - _S_start_free; // 检测备用内存块是否有空间


	/*
		1.当对应freelist上的备用内存块很充足时,
		  将__bytes_left分成__nobjs个chunk块
		2.当对应freelist上的备用内存块不是很充足,但是能够分配chunk块时,
		  __bytes_left内存块能分成多个chunk就分成多少个chunk块,但是只要能够分成一个chunk
		3.当对应freelist上的备用内存块不能分配一个chunk块时,
		  使用malloc申请一个大内存块,然后分割成chunk块。没有用的内存块就当做备用内存块使用

		注意:用于_S_start_free,_S_end_free,_S_heap_free都是静态变量
		所以不用freelist上的备用内存块是可以混合起来使用的,并不是freelist只能
		使用自己链表上的备用大内存块
	*/
    if (__bytes_left >= __total_bytes) {
		// 分配出去__total_bytes大小的内存
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return(__result);
    } else if (__bytes_left >= __size) { 
		// 计算__bytes_left的内存块能够切分成多少个__size大小的chunk块,然后将能够整数分配的内存块进行分配
        __nobjs = (int)(__bytes_left/__size); // __bytes_left能够分成多少个大小为__size的chunk块
        __total_bytes = __size * __nobjs; // __total_bytes为能够整数切分chunk的总内存大小

		// 分配出去__total_bytes大小的内存
		__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);

	  	// 走到这里的__bytes_left大小的内存块一定是内存池中的内存碎片,需要被回收
	  	// 将被分割成小块的内存 头插到有对应内存块大小的freelist中
        // 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);

            ((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
            *__my_free_list = (_Obj*)_S_start_free;
        }
		// malloc分配内存
        _S_start_free = (char*)malloc(__bytes_to_get);
		
        if (0 == _S_start_free) { // malloc分配异常
        	// 遍历_S_free_list[]中的freelist,判断是否存在freelist中的内存块比__size大
        	// 如果存在,就将该freelist上的大内存块分割成__size和另一个小内存块,并将分割下来的__size返回
            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;
				

				// 如果存在freelist中有大内存块,就将该内存块切割__size大小下来
                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_free_list[]中的freelist中没有比__size大的内存块了
		// 所以就会使用一级空间配置器重新申请大内存	
	   	_S_end_free = 0;	// In case of exception.
            _S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
            // This should either throw an
            // exception or remedy the situation.  Thus we assume it
            // succeeded.
        }

		// 更新freelist分配得到的内存大小_S_heap_size和freelist的尾指针_S_end_free
        _S_heap_size += __bytes_to_get;
        _S_end_free = _S_start_free + __bytes_to_get;
        return(_S_chunk_alloc(__size, __nobjs));
    }
}


// 一级空间配置器的allocate就是通过malloc来开辟内存
static void* allocate(size_t __n)
{
// allocate底层就是用malloc开辟空间内存
void* __result = malloc(__n);
// 如果malloc申请内存失败,就会使用oom将有大内存块的进程杀死,并释放其占用的内存空间
if (0 == __result) __result = _S_oom_malloc(__n);
return __result;
}

// 用户提前注册的OOM回调函数
template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)
{
    void (* __my_malloc_handler)(); // 定义一个函数指针
    void* __result;

    for (;;) {
		// 用户可以提前注册一个__malloc_alloc_oom_handler回调函数
		// 在系统的内存资源不够用的时候,会自动调用该回调,将指定的内存资源进行回收
        __my_malloc_handler = __malloc_alloc_oom_handler;
		// 如果注册了回调函数,就回调。否则就抛出异常
        if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }
        (*__my_malloc_handler)(); // 调用OOM回调函数

		// 进行了OOM之后,重新尝试执行malloc
		// 如果任然malloc失败,就再次执行OOM回调函数,直到malloc申请内存成功
        __result = malloc(__n);
        if (__result) return(__result);
    }
}

释放内存函数:deallocate

  // 将__p位置上的,大小为__n的chunk块头插到对应的freelist,相当于将chunk块回收到内存池中了
  /* __p may not be 0 */
  static void deallocate(void* __p, size_t __n)
  {
  	// 当__n大小超过128字节时,直接通过free进行释放
    if (__n > (size_t) _MAX_BYTES)
      malloc_alloc::deallocate(__p, __n);
    else {
	  // 在_S_free_list[]中找到能够存放__n大小的freelist
      _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节点头插到_S_free_list[_S_freelist_index(__n)]自由链表上
      __q -> _M_free_list_link = *__my_free_list;
      *__my_free_list = __q;
	  
      // lock is released here,临界区结束
    }
  }

扩缩容内存函数: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;
	// 如果__old_sz和__new_sz大于128字节的话,那么就使用库函数realloc进行扩缩容
    if (__old_sz > (size_t) _MAX_BYTES && __new_sz > (size_t) _MAX_BYTES) {
        return(realloc(__p, __new_sz));
    }

	// 如果__old_sz在内存对齐之后和__new_sz一样大,那么就不用进行内存的扩缩容
    if (_S_round_up(__old_sz) == _S_round_up(__new_sz)) return(__p);

	/*
	申请__new_sz大小的内存,并把__p位置上数据拷贝到新地址处
	如果__new_sz>__old_sz的话,那么就将旧数据全部拷贝到新地址处
	如果__new_sz<__old_sz的话,那么就旧数据中__new_sz大小的部分拷贝到新地址处
	*/
    __result = allocate(__new_sz);
    __copy_sz = __new_sz > __old_sz? __old_sz : __new_sz;
    memcpy(__result, __p, __copy_sz);
    deallocate(__p, __old_sz);
    return(__result);
}

SGI STL二级空间配置器内存池优点

为了防止小块内存的频繁申请和释放造成的内存碎片问题,而严重的内存碎片问题会导致大内存块无法申请。所以一般会使用内存池进行小块内存的申请和释放。

一般的内存池:能够做到内存块按照一定字节数进行内存对齐,并分配内存。当申请内存的时候,就会找到内存对齐到指定的freelist上找chunk块(只会在自己的freelist上找内存块)。如果没有chunk就会直接malloc申请。如果malloc失败了,直接就系统崩溃了。

SGI STL的二级空间配置器的内存池相比而言:

  • 找chunk块时,不仅会在自己的freelist和备用内存上找,也会去其他的freelist和备用内存块上找。并且如果备用内存过小的话,就将备用内存挂载到有相应的freelist上。
  • malloc失败的时候,SGI STL的内存池有两种防护措施,一种是找大内存块,然后切割成小内存块。一种是通过OOM回调函数,强制回收内存,使得malloc不会失败。

总结SGI STL中二级空间配置器的内存池优点:

  1. 会申请大内存挂载到对应的freelist上,并将其中的一部分分割成chunk块串联起来形成一个静态链表,剩下的另一部分作为备份内存块放在freelist的下面。该内存块不仅可以供当前位置上的freelist使用,也可以给其他的freelist使用。
  2. 如果备用的内存块中只有很小的一块内存,并不够当前需要的chunk块大小的话,就会将这个小内存块挂载到对应freelist中。这样就可以充分利用备用的内存块
  3. 当使用malloc申请大内存块失败的时候,会检查比申请的内存块大的chunk块,找到之后会将大内存块分割成执行内存块进行使用。如果没有大内存块可以分割了,也可以使用用户提前注册的OOM回调函数进行强制的指定内存回收。

SGI STL二级空间配置器内存池的不足

SGI STL的二级空间配置器中不同freelist可能指向自己申请的内存块,也可能指向其他freelist没有用完的备用内存块,这样就会导致freelist之间的指向关系很复杂。在多线程环境下,就需要使用大粒度的锁才能保证线程安全。
而tcmalloc中,虽然有三个cache,但是每一个cache中的哈希桶对应的内存块大小都是相同的,因此在加锁的时候,只需要对一个哈希桶进行加锁即可,这样就减少了加锁的粒度,从而减少了多线程竞争锁时的开销。

SGI STL中如果内存池中没有足够大小的内存块的话,首先会进行malloc直接分配,如果malloc失败了,才会从其他有大内存块的freelist中切割对应大小的内存块使用。另外如果没有大内存块的话,就会调用一级空间配置器的allocate,进而会调用用户提前注册的OOM回调函数,强制回收内存资源。
而tcmalloc中,如果对应大小的freelist中没有足够的内存块了,首先会去有大内存块的freelist上切割内存块,如果没有大内存块,才会进行malloc申请大内存块,然后再进行分割。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hyzhang_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值