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


    STL 内存空间的分配和回收

    STL中控制内存空间的分配和回收的内容在中,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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    template
    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)); }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

    template  
    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 vector : protected _Vector_base<_Tp, _Alloc> {
    	.......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

    第一级内存分配器

    template 
    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

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

    template 
    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 
    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

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

    第二级内存分配器

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

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

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

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

    源码分析

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

    template 
    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);
    
    } ;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    __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;
      };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

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

    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;
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    _S_refill 填充内存

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

    template 
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    _S_chunk_alloc 申请内存

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

    1. 计算申请内存大小
    2. 判断预留空间是否足够分配申请的大小,够则返回
    3. 判断预留空间是否足够分配1个对象的大小,够则返回
    4. 向系统申请内存,将申请到内存放入预留空间,重新调用函数
    5. 系统申请失败则查找是否链表上有更大的内存,有则取出给预留空间,重新调用函数
    6. 到这一步说明实在没内存了,转向一级分配器,尝试OOM处理
    template 
    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));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    二级分配器全貌

    在这里插入图片描述

    小结

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

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

  • 相关阅读:
    golang清空数组的方法
    VB.NET—窗体引起的乌龙事件
    Document类型【2】
    基于HTML+CSS+JS实现七夕情人节表白代码【含代码】
    D3JS简介
    【数据结构与算法】后续遍历的非递归实现
    力扣算法 Java 刷题笔记【十大排序算法】代码实现
    HarmonyOS鸿蒙原生应用开发设计- 图标库
    ORACLE-统计信息收集&&分析表和索引
    c++在visual studio上的默认配置
  • 原文地址:https://blog.csdn.net/ii0789789789/article/details/126557189