2.stg-stl内存分配机制

目录

  • 总览
  • 全局对象构建析构
  • 全局区间对象fill/copy
  • 双顶层内存缓冲器
  • 示例
  • reference

总览

大体stg-stl分为alloctor, iter, adapter, container, algorithms, functions
原图来自note/STL源码剖析.md at master · arkingc/note · GitHub


alloctor把对象内存申请&构造拆分开来

  • stl_construct.h 全局对象的构建(ctor), 析构(dector), 对其stl接口标准
  • stl_uninitialized.h 全局对象的赋值, 初始化等, 这部分也是对其stl接口标准
  • stl_alloc.h 全局双顶层对象缓冲

全局对象构建析构(stl_construct.h)

这里函数_XXX(比如_Construct)都是stg内置的函数, 下面这种是对其stl标准接口

// --------------------------------------------------
// Old names from the HP STL.

template 
inline void construct(_T1* __p, const _T2& __value)

这里以construct为线看一下, 这里有两个特点:

  1. 构建对象一般是: [内存分配] -> [构造器], 这里使用placement new算子, 仅仅调用构造器, 这样object(s)的内存和ctor就解耦了
  2. 区分对待trival对象, 因为trival对象可以使用更加高效的构建方式.
    简单的结构如下:
    construct(_T1* __p, const _T2& __value) 
      -> _Construct(_T1* __p, const _T2& __value) { new ((void*) __p) _T1(__value); }
    construct(_T1* __p) 
      -> void _Construct(_T1* __p) { new ((void*) __p) _T1(); }
    destroy(_ForwardIterator __first, _ForwardIterator __last) 
      -> _Destroy(_ForwardIterator __first, _ForwardIterator __last) 
        -> __destroy(_ForwardIterator __first, _ForwardIterator __last, _Tp*) 萃取当前类型是否有析构  
         有 -> __destroy_aux(_ForwardIterator __first, _ForwardIterator __last, __false_type) for-each destroy(&*__first); 
          无 -> 啥都不干, 这一步只是为了析构, trival无需特殊析构

在看过上面例子够, 这里引申两点:

  1. c++本身是静态语言, 不能运行时获取object的meta info, 所以萃取其实是一个编写上的契约, 比如当前以迭代器(iter)方式构建或者删除时, 从迭代器可以获取value类型, 从value类型就可以知道是否是trivial类型, 假如自定义的iter没有按照契约定义是否trival类型, 编译时内联生成代码时就不能找到属性
  2. 实际调用时通过重载来实现, 不存在运行时的开销(struct __true_type{}, struct __false_type)
  3. trivial类型不走ctor, copy, assign, dector, move直接使用memcpy, memmove等高效方式
  4. 如何界定object是trivial, 满足以下就是none trivial否则就是trivial
    a. 显示定义构造函数(ctor), 复制构造函数(copy), 移动构造(move), 赋值运算符(assign), 析构函数(dector)
    b. 类型有非POD(plain old data)类型成员
    c. 有虚函数, 有基类

POD类型如下:

  • 算数类型
  • enum
  • pointer(nullptr/object pointer/function pointer)
  • 到类成员的指针类型 比如C类, M成员, C::M*
  • pod类型组成的 class, struct, union

全局区间对象fill/copy (stl_uninitialized.h)

这里也是萃取了iter中是否为pod的类型优化

    uninitialized_copy(_InputIter __first, _InputIter __last,
                     _ForwardIter __result) (萃取value pointer type)
      -> __uninitialized_copy(_InputIter __first, _InputIter __last,
                     _ForwardIter __result, _Tp*) (从Tp萃取is pod)
        -> 是pod , 调用algo_base的 _OutputIter copy(_InputIter __first, _InputIter __last, _OutputIter __result)
        -> 不是pod foreach调用stl_contruct中的构建方法 for(***) _Construct(&*__cur, *__first);

uninitialized_fill, uninitialize_fill_n等等都是这个思路

双顶层内存缓冲器

这里stg没有使用stl标准的std::allocator, 而是使用内部实现的高效std::alloc, 这里并没有严格接口对其.
这里定义了内部接口供内部容器使用:

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

具体实现__malloc_alloc_template__default_alloc_template
其中__malloc_alloc_template没啥东西只是对malloc的简单封装, 可以设置oom时的回调函数
以下重点分析__default_alloc_template.

设计要义&方法

  • 解决什么问题
    在glibc中其实也是一样的问题和思路, 针对client零乱大量的申请/释放, 会形成一些列大小不一且不相连的使用内存片段, 会产生出内碎片的问题
  • slot化
    和内存分页一个思路, 把memory分成不同chunk slot, 把请求离散标准化, 这样就形成了一系列的slot, [8, 16, 24, 32, 40, ..., 120, 128] 当需要5 byte时, 向上对其使用8 byte的slot分配, 这样slot内的内碎片均值为4. 但是slot是有边界的, 当申请>128 byte时直接使用__malloc_alloc_template, __default_alloc_template旨在解决小内存大量零碎调用
  • 全局缓充
    为了避免连续不同slot的调用还设置的全集缓存, 当前slot不够的直接从全局缓存出
  • 释放缓存
    释放缓存时实际是归还给了stg的内存缓存池, 以便后续调用时再次申请
    图示如下:



代码详情

  union _Obj {
        union _Obj* _M_free_list_link;
        char _M_client_data[1];    /* The client sees this.        */
  }; //union 用户视角char*, 系统视角满足当前slot长度, 首地址指向一个slot chunk地址
static _Obj* __STL_VOLATILE _S_free_list[] //全局slot, 每一个元素指向slot链表
 static void* allocate(size_t __n)
  {
    void* __ret = 0;
    if (__n > (size_t) _MAX_BYTES) {
      __ret = malloc_alloc::allocate(__n); // -> __malloc_alloc_template
    }
    else {
      _Obj* __STL_VOLATILE* __my_free_list
          = _S_free_list + _S_freelist_index(__n); //_S_freelist_index(__n)依据申请找到合适slot
      // 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;
      if (__result == 0)
        __ret = _S_refill(_S_round_up(__n));//申请内存
      else {
        *__my_free_list = __result -> _M_free_list_link; //以前以后缓存, 拔出第一片返回, 回写下一原有free到_S_free_list[x](*__my_free_list <-)
        __ret = __result;
      }
    }
    return __ret;
  };

同样deallocate把归还内存按照slot插入到起始

  static void deallocate(void* __p, size_t __n)
  {
    if (__n > (size_t) _MAX_BYTES)
      malloc_alloc::deallocate(__p, __n); // free
    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; //回写_S_free_list对应slot
      // lock is released here
    }
  }

重点是_S_refill和_S_chunk_alloc, 只要调入_S_refill一定是内存当前slot内存不够了
_S_refill调用_S_chunk_alloc分配足量内存, 串联slot内链表结构.
_S_chunk_alloc 分配足够了内存, 具体如何足量下面有解释

/* 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 
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
    int __nobjs = 20;
    char* __chunk = _S_chunk_alloc(__n, __nobjs); //调用一个__n size chunk, 可以返回多个,   返回内存划分:[__chunk, __chunk + __n*nobjs][free_start, free_end]
    _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);//只够一个就地返回

    /* Build free list in chunk */ 
      __result = (_Obj*)__chunk;
      *__my_free_list = __next_obj = (_Obj*)(__chunk + __n); //返回客户端的
      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);
}

_S_chunk_alloc的责任是分配给足量的内存, 把内存切分一部分给slot缓存, 一部分全局缓存.
client需求一个__n的内存, 足量体现在:

  • _S_chunk_alloc看全局缓存余量是否够20__n, 如果够一次划给slot20__n
  • _S_chunk_alloc看全局缓存余量[1__n, 不足20__n], 一次划给最大的最大数目__n大小
  • _S_chunk_alloc按照需求内存220__n + left_space >> 4, 把20*__n划给slot, 这个体现在返回__nobjs和_S_start_free的设置, 这部分代码相对多一些
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); //够20
    } 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); //够至少一个, 有多少给多少slot
    } else {
        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) {   //把余下全局缓冲区挂入__bytes_left缓冲区
            _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; //_S_start_free当slot接到_S_free_list中
            *__my_free_list = (_Obj*)_S_start_free; //回写_S_free_list
        }
        _S_start_free = (char*)malloc(__bytes_to_get);
        if (0 == _S_start_free) { //当内存不足时
            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; //在上面之前全局缓存已经挂入slot
                    _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);
            // 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;//整块[_S_start_free, _S_end_free] 下载递归调用, 划分slot
        return(_S_chunk_alloc(__size, __nobjs));
    }
}

调用示例

假如slot目前为空

  • call a(6)
slot 8 16 ... 128 全局
8*19 8 -> ... -> 8 -> ^ ^ ^ ^ 20*8
  • call a(128)
slot 8 16 ... 128 全局
8*19 8 -> ... -> 8 -> ^ ^ ^ ^ 32 = 20*32 - 128
  • call a(128)
slot 8 16 32 ... 128 全局
8*19 8 -> ... -> 8 -> ^ ^ 32->^ 128*19 128 -> ... -> 128 -> ^ 20*128
  • call d(8)
slot 8 16 32 ... 128 全局
8*20 8 -> ... -> 8 -> ^ ^ 32->^ 128*19 128 -> ... -> 128 -> ^ 20*128

有此看出:

  1. 专注小内存分配, 大内存交给malloc
  2. 把小内存划分离散的slot, 有点像一些列的齿轮, 齿比是8字节间隔, 看需求咬合那个最合适
  3. slot要一次申请多个, 能够就给slot缓存, 这样避免了多次小内存申请
  4. 再设置一级全局缓存, 补充空的slot内存需求
  5. 当内存不足时考虑了, 把全局内存归并slot, 利用现有slot化解, 如果不能化解报错
  6. 当free内存时, 不是真正free, 按照slot放回, 减少系统调用, 下次再需要直接返回
  7. slot 存储overhead为0, 使用union, 系统视角一个指针指向next free slot, 用户视角为未初始化内存

示例

下面以常用vector示例看下如何使用内存分配机制

vector代码片段如下:

template 
class vector : protected _Vector_base<_Tp, _Alloc> 
{
  // requirements:

  __STL_CLASS_REQUIRES(_Tp, _Assignable);

private:
  typedef _Vector_base<_Tp, _Alloc> _Base;

define __STL_DEFAULT_ALLOCATOR(T) alloc  // 配置在stl_config.h 
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc; //默认是没有__USE_MALLOC, alloc使用上述__default_alloc_template分配

//vector base代码片段
template  //__default_alloc_template
class _Vector_base {
public:
  typedef _Alloc allocator_type; //__default_alloc_template
  allocator_type get_allocator() const { return allocator_type(); }

  _Vector_base(const _Alloc&)
    : _M_start(0), _M_finish(0), _M_end_of_storage(0) {}
  _Vector_base(size_t __n, const _Alloc&)
    : _M_start(0), _M_finish(0), _M_end_of_storage(0) 
  {
    _M_start = _M_allocate(__n);
    _M_finish = _M_start;
    _M_end_of_storage = _M_start + __n;
  }


  ~_Vector_base() { _M_deallocate(_M_start, _M_end_of_storage - _M_start); }

typedef simple_alloc<_Tp, _Alloc> _M_data_allocator; // 用上述simple_alloc做接口, 传入__default_alloc_template实现类型
  _Tp* _M_allocate(size_t __n)
    { return _M_data_allocator::allocate(__n); } //调入内存分配

假如有如下代码

auto v = vector(3,0);
for (auto i = 4; i < 8; ++i){
  emplace_v = vector(i, 0);
}

v申请43(size(int)v.size()) 12 bytes, 归并申请到16字节

slot 8 16 ... ^ 全局
^ 19 free list ... ^ 12*20

emplace_v(i = 4)申请4*4后, 直接使用free list

slot 8 16 ... 128 全局
^ 18 free list ... ^ 12*20

emplace_v释放后, 插入slot 16首位

slot 8 16 ... 128 全局
^ 19 free list ... ^ 12*20

emplace_v(i = 5)申请4*5后, 使用slot 24, 从全局free分配, 分配10个

slot 8 16 24 ... ^ 全局
^ 19 free list 9 free list ... ^ ^

emplace_v释放后 slot 16归还

slot 8 16 24 ... ^ 全局
^ 19 free list 10 free list ... ^ ^

emplace_v (i = 6)申请46和上一轮情况相同
emplace_v (i = 7)申请4
7, 使用slot 30, 全局为空, 申请

slot 8 16 24 30+... ^ 全局
^ 19 free list 10 free list 19 free list ... ^ 20*30

emplace_v释放4*7

slot 8 16 24 30+... ^ 全局
^ 19 free list 10 free list 20 free list ... ^ 20*30

reference:

https://github.com/arkingc/note/blob/master/C%2B%2B/STL%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90.md#4stl%E5%85%AD%E5%A4%A7%E9%83%A8%E4%BB%B6
https://backendhouse.github.io/post/stl%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-traits/
https://stackoverflow.com/questions/51659101/why-can-static-data-member-not-be-initialized-in-class-in-c11

你可能感兴趣的:(2.stg-stl内存分配机制)