STL源码阅读笔记-内存空间的分配和回收

本文详细解析了STL中内存分配器的设计理念及其实现细节,包括双层内存分配机制、内存碎片处理策略以及多线程环境下内存管理的考虑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

STL 内存空间的分配和回收

STL中控制内存空间的分配和回收的内容在<stl_alloc.h>中,SGI对这部分的设计哲学如下:

  • 向system heap中获得空间
  • 考虑内存不足时的做法
  • 考虑小内存造成的内存碎片问题
  • 考虑多线程情况

不过在阅读书和源码的过程中都先忽略了多线程的情况,所以这边也先不考虑多线程的情况,后面看看有没有机会看看多线程中时如何实现的。

SGI 在设计了双层的空间适配器,第一级配置器直接使用malloc和free进行内存的分配和回收。第二级配置器使用内存池的方式对内存对象进行管理,默认将128字节作为分割。大于128字节的内存直接由一级分配器进行分配,小于等于128字节的内存则在内存池内进行分配。通过__USE_MALLOC的宏定义可以决定是否开放第二级配置。

# ifdef __USE_MALLOC
	typedef __malloc_alloc_template<0> malloc_alloc;
	typedef malloc_alloc alloc;     // 将一级分配器设置为 alloc
	....
# else
	typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;   // 将二级分配器设置为alloc
	....
# endif

在基础的空间适配器之上,SGI还额外设计包装了一个接口使其适配STL的规范,

template<class _Tp, class _Alloc>
class simple_alloc {

public:
    static _Tp* allocate(size_t __n)
      { return 0 == __n ? 0 : (_Tp*) _Alloc::allocate(__n * sizeof (_Tp)); }
    static _Tp* allocate(void)
      { return (_Tp*) _Alloc::allocate(sizeof (_Tp)); }
    static void deallocate(_Tp* __p, size_t __n)
      { if (0 != __n) _Alloc::deallocate(__p, __n * sizeof (_Tp)); }
    static void deallocate(_Tp* __p)
      { _Alloc::deallocate(__p, sizeof (_Tp)); }
};

simple_alloc 的接口都只是在内部将调用都转换成_Alloc的调用,STL的所有容器都使用的是simple_alloc的接口,例如:

template <class _Tp, class _Alloc> 
class _Vector_base {
public:
	.......
    typedef simple_alloc<_Tp, _Alloc> _M_data_allocator;

  _Tp* _M_allocate(size_t __n)
    { return _M_data_allocator::allocate(__n); }
  void _M_deallocate(_Tp* __p, size_t __n) 
    { _M_data_allocator::deallocate(__p, __n); }
};

// 在stl_config 里定义
// define __STL_DEFAULT_ALLOCATOR(T) alloc 
template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class vector : protected _Vector_base<_Tp, _Alloc> {
	.......
}

所以容器与分配器之间关系如下图:
alloc关系图

第一级内存分配器

template <int __inst>
class __malloc_alloc_template {
private:
  static void* _S_oom_malloc(size_t);
  static void* _S_oom_realloc(void*, size_t);

public:
  static void* allocate(size_t __n)
  {
    void* __result = malloc(__n);
    if (0 == __result) __result = _S_oom_malloc(__n);
    return __result;
  }
  static void deallocate(void* __p, size_t /* __n */)
  {
    free(__p);
  }
  static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
  {
    void* __result = realloc(__p, __new_sz);
    if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
    return __result;
  }

  static void (* __set_malloc_handler(void (*__f)()))()
  {
    void (* __old)() = __malloc_alloc_oom_handler;
    __malloc_alloc_oom_handler = __f;
    return(__old);
  }

};

// 如果使用的一级分配器,则执行以下宏定义
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc;

一级分配器基本上就是对malloc、realloc 和 free进行了简单的包装。只有当malloc和realloc分配内存不足时,会重试调用OOM的处理

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);
    }
}

template <int __inst>
void* __malloc_alloc_template<__inst>::_S_oom_realloc(void* __p, 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 = realloc(__p, __n);
        if (__result) return(__result);
    }
}

oom 的处理也很简单,就是不断尝试调用OOM的处理函数(需要通过__set_malloc_handler来设置),直到分配成功,或者OOM处理函数不存在为止。

第二级内存分配器

在具体看二级分配器的源码前,先介绍下二级分配器是如何设计的。

二级分配器构建了一个长度为16的数组管理16个链表,每个链表都管理了一部分内存,其中第i个链表管理的是一组内存大小为 (i + 1) * 8 的内存。链表上的每一块内存都会记录下一块内存的地址,形成链表。当分配的内存小于128字节的时候,分配器会找到合适的链表,然后分配一块内存出去;回收的时候也是判断是否128字节,如果小于则会找到合适的位置重新插回链表。大概模型如下图所示:
在这里插入图片描述

而大于128字节的部分则直接由一级分配器进行分配。

而为了避免在填充上述链表的时候频繁去向系统申请内存,二级管理器在单次申请的时候,会额外多申请一部分的内存捏在手上不进行分配,等待下一次填充。
二级分配器额外捏在手上的内存

源码分析

接下来就要看看源码里是怎么对上述模型进行构建的。

template <bool threads, int inst>
class __default_alloc_template {

private:
    // 定义管理的内存大小、链表长度
    enum {_ALIGN = 8};
    enum {_MAX_BYTES = 128};
    enum {_NFREELISTS = 16}; // _MAX_BYTES/_ALIGN
	static size_t   _S_round_up(size_t __bytes)     {
   		return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1)); 
   }

__PRIVATE:
  union _Obj {
        union _Obj* _M_free_list_link;
        char _M_client_data[1];    /* The client sees this.        */
  };
private:
    static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS]; 
	static  size_t _S_freelist_index(size_t __bytes) {
        return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1);
  }
  static char* _S_chunk_alloc(size_t __size, int& __nobjs);

  // Chunk allocation state.
  static char* _S_start_free;
  static char* _S_end_free;
  static size_t _S_heap_size;

public:
  static void* allocate(size_t __n);
  static void deallocate(void* __p, size_t __n);
  static void* reallocate(void* __p, size_t __old_sz, size_t __new_sz);

} ;

__default_alloc_template 就是二级分配器的结构,和一级分配器一致,开放了三个函数用于内存的管理。其中_S_free_list中管理了16个链表,_S_start_free和_S_end_free分别指向预留内存的开始和结束。

allocate

  /* __n must be > 0      */
  static void* allocate(size_t __n)
  {
    void* __ret = 0;
    // 大于128字节直接用一级分配器分配
    if (__n > (size_t) _MAX_BYTES) {
      __ret = malloc_alloc::allocate(__n);
    }
    else {
      //_S_freelist_index会返回对应大小的内存的所在链表下标
      _Obj* __STL_VOLATILE* __my_free_list  = _S_free_list + _S_freelist_index(__n);
      _Obj* __RESTRICT __result = *__my_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;
  };

在这里插入图片描述
填充的过程在后面

deallocate

 static void deallocate(void* __p, size_t __n)
  {
  	// 大于128字节的说明是从一级分配器里获得的,需要直接释放
    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;
    }
  }

在这里插入图片描述

_S_refill 填充内存

在分配过程中如果链表是空的话, 会触发填充的过程。填充主要是向系统或者持有的预留内存上申请一块,然后将申请出来的内存进行切分,形成链表后挂在管理链表的数组里。

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); // __nobjs是引用,
    _Obj* __STL_VOLATILE* __my_free_list;
    _Obj* __result;
    _Obj* __current_obj;
    _Obj* __next_obj;
    int __i;

    // 如果申请到内存只有1个,那么直接返回
    if (1 == __nobjs) return(__chunk);
    __my_free_list = _S_free_list + _S_freelist_index(__n);

    // 将申请到的内存进行切分
      __result = (_Obj*)__chunk;
      *__my_free_list = __next_obj = (_Obj*)(__chunk + __n);   
      for (__i = 1; ; __i++) {  // 第0号是需要返回给外部的,所以这边从1开始
        __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);
}

_S_chunk_alloc 申请内存

申请内存的过程主要是如下:

  1. 计算申请内存大小
  2. 判断预留空间是否足够分配申请的大小,够则返回
  3. 判断预留空间是否足够分配1个对象的大小,够则返回
  4. 向系统申请内存,将申请到内存放入预留空间,重新调用函数
  5. 系统申请失败则查找是否链表上有更大的内存,有则取出给预留空间,重新调用函数
  6. 到这一步说明实在没内存了,转向一级分配器,尝试OOM处理
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 {
        // 一次都不够了,需要重新申请了, heap_size 会逐渐加大
        size_t __bytes_to_get =  2 * __total_bytes + _S_round_up(_S_heap_size >> 4);  
        
        // 如果之前还有剩余需要分配掉
        if (__bytes_left > 0) {
            // 这边__bytes_left一定小于128字节,所以直接往对应的free_list内插入就行
            _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;
        }

        _S_start_free = (char*)malloc(__bytes_to_get);
        //  处理向系统申请内存失败的情况
        if (0 == _S_start_free) {
            size_t __i;
            _Obj* __STL_VOLATILE* __my_free_list;
	          _Obj* __p;

            // 从大的freelist里找是否有空闲内存
            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;
                    // 将free_list中更大的内存拿出来当作_S_start_free的起点,然后重新调用函数进行分配
                    return(_S_chunk_alloc(__size, __nobjs));
                }
            }
	         _S_end_free = 0;	// In case of exception. // 到这里说明自己的资源已经确实分配不出来了
            _S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get); // 这边会调用一级分配器,这个调用主要是一级分配器内有个OOM的处理,也许可以分配成功,失败的话,也由以及一级分配器抛出OOM的信息
        }

        _S_heap_size += __bytes_to_get;  // 累计获得的内存大小,下次分配会逐渐变大
        _S_end_free = _S_start_free + __bytes_to_get;  //管理新分配出来的内存

        // 有了内存之后重新调用自己
        return(_S_chunk_alloc(__size, __nobjs));
    }
}

二级分配器全貌

在这里插入图片描述

小结

从二级分配器的实现上可以看出,代码是考虑了各个方面的性能。比如避免大量malloc调用,优先申请较大部分的内存回来自己管理,在Python中也有类似的操作,不过STL里malloc申请的内存是逐渐加大,而python一次性就是申请256K的内存回来管理。在给链表上分配内存的时候也和Python有区别,Python一般一次分配4KB内存到链表上,而STL里最多一次分配20个块到对应链表上。

在内存优化上,链表头部保存下一个节点的地址,这一点倒是和Python的一致,不过Python的链表是双向链表,STL里的是单向。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值