2.stg-stl内存分配机制

2022-01-04  本文已影响0人  db24cc

目录

总览

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


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

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

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

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

template <class _T1, class _T2>
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类型如下:

全局区间对象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 _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)); }
};

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

设计要义&方法


代码详情

  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 <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); //调用一个__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的内存, 足量体现在:

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); //够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目前为空

slot 8 16 ... 128 全局
8*19 8 -> ... -> 8 -> ^ ^ ^ ^ 20*8
slot 8 16 ... 128 全局
8*19 8 -> ... -> 8 -> ^ ^ ^ ^ 32 = 20*32 - 128
slot 8 16 32 ... 128 全局
8*19 8 -> ... -> 8 -> ^ ^ 32->^ 128*19 128 -> ... -> 128 -> ^ 20*128
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 _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
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 <class _Tp, class _Alloc> //__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<int>(3,0);
for (auto i = 4; i < 8; ++i){
  emplace_v = vector<int>(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

上一篇 下一篇

猜你喜欢

热点阅读