菜鸟笔记
提升您的技术认知

一篇文章搞懂stl中的空间配置器allocator-ag真人游戏

table of contents

3

allocator是什么东西呢?
allocator是stl的六大组件之一.
顾名思义,他的名字叫空间配置器
其作用就是为各个容器管理内存(内存开辟 内存回收)
其实allocator配置的对象不只是内存,它也可以向硬盘索取空间.
但是在这里我们主要考虑对内存的管理
我们在使用stl库的时候不用去考虑各种内存的操作,就是因为allocator已经帮我们做好了内存配置的工作
那么allocator是怎么实现内存的管理的呢?
举个简单的例子

vector vec;

我们在声明上面的vec的时候,allocator是怎么工作的呢?
其实对于一个标准的stl 容器,当vetor vec 的真实语句应该是

 vetor>vec

第二个参数就是我们用到的allocator,这里是标准版本的空间配置器,也就是说不同版本的stl里面都会有这个()

但是在sgi版本的stl中,它还有一个更高效的空间配器,名字叫alloc 用这个空间配置器的时候不能用标准写法,它不接受任何参数

也就是说在sgi版本的stl里面有两个空间配置器 一个是标准的allocator,一个是sgi它自己做的alloc

两种配置器的用法是这样的

 vetor>vec//标准的allocator空间配置器
 vetorvec//alloc空间配置器

下面将分别就这两个空间配置器的工作原理开始讲解

要想知道空间配置器是怎么工作的 ,就得先了解c 对一个对象内存配置操作和释放操作是什么样的

举个例子 我们创建然后销毁一个对象

class foo{};
foo *pf = new foo;    
delete pf;

我们看其中第二行和第三行,虽然都是只有一句,当是都完成了两个动作。

当你 new 一个对象的时候两个动作是:

先调用::operator new 分配一个对象大小的内存,

然后在这个内存上调用foo::foo()构造对象。

同样,当你 delete 一个对象的时候两个动作是:

先调用foo::~foo() 析构掉对象,

再调用::operator delete将对象所处的内存释放。

为了精密分工,stl 将allocator决定将这四个操作分开。分别用 4 个函数来实现,我们来看allocator的源码

#include // for new
#include  //  size_t
#include  // for unit_max
#include  // for cerr
using namespace std;
namespace sld {
template 
class allocator
{
public:
	typedef t		value_type;
	typedef t*		pointer;
	typedef const t*	const_pointer;
	typedef t&		reference;
	typedef const t&	const_reference;
	typedef size_t		size_type;
	typedef ptrdiff_t	difference_type;
	template 
	struct rebind
	{
		typedef allocator other;
	};
	//申请内存
	pointer allocate(size_type n, const void* hint = 0)
	{
		t* tmp = (t*)(::operator new((size_t)(n * sizeof(t))));
		//operator new 和new operator是不同的
		if (!tmp)
			cerr << "out of memory"<~t();
	}
	
	//取地址
	pointer address(reference x)
	{
		return (pointer)&x;
	}
	
	const_pointer const_address(const_reference x)
	{
		return (const_pointer)&x;
	}
	size_type max_size() const 
	{
		return size_type(uint_max/sizeof(t));
	}
};
}

由源码我们可以看到
allocator源码中对应四个阶段分别对应了四个函数,

而这四个函数中创建和释放内存分别就是对new 和delete的简单封装
所以虽然sgi里面有这个标准的空间配置器,

但是sgi自己从未使用过它,也不建议我们使用  
如果要自己写个配置器完全可以以这个类为模板。

而需要做的工作便是写下自己的 allocate和deallocate即可。

对应着我们上面说的四个操作

在该空间配置器中分别有四个函数和它对应,

内存的配置:alloc::allocate();

对象的构造:::construct();

对象的析构:::destroy();

内存的释放:alloc::deallocate();

这四个函数在sgi版本stl的头文件分布如下

stl规定空间配置器放置在

在memory中又有如上图所示的三个文件:在stl_construct中定义了两个函数负责对象的构造与析构.

   在stl_alloc.h中就是空间配置器alloc作用函数的放置地点,负责内存的开辟和回收.在这里定义了一二级配置器(后面会讲解)

还有一个头文件定义了一些全局函数主要负责填充或者复制大块内存数据

2.1----对象的构造与析构

template 
inline void destroy(t* pointer) {
    pointer->~t();                               //只是做了一层包装,将指针所指的对象析构---通过直接调用类的析构函数
}
template 
inline void construct(t1* p, const t2& value) {
  new (p) t1(value);                            //用placement new在 p 所指的对象上创建一个对象,value是初始化对象的值。
}
template                 //destory的泛化版,接受两个迭代器为参数
inline void destroy(forwarditerator first, forwarditerator last) {
  __destroy(first, last, value_type(first));    //调用内置的 __destory(),value_type()萃取迭代器所指元素的型别
}
template 
inline void __destroy(forwarditerator first, forwarditerator last, t*) {
  typedef typename __type_traits::has_trivial_destructor trivial_destructor;
  __destroy_aux(first, last, trivial_destructor());        //trival_destructor()相当于用来判断迭代器所指型别是否有 trival destructor
}
template 
inline void                                                //如果无 trival destructor ,那就要调用destroy()函数对两个迭代器之间的对象元素进行一个个析构
__destroy_aux(forwarditerator first, forwarditerator last, __false_type) {
  for ( ; first < last;   first)
    destroy(&*first);
}
template                         //如果有 trival destructor ,则什么也不用做。这更省时间
inline void __destroy_aux(forwarditerator, forwarditerator, __true_type) {}
inline void destroy(char*, char*) {}          //针对 char * 的特化版
inline void destroy(wchar_t*, wchar_t*) {}    //针对 wchar_t*的特化版

2.1.1 对象的构造:::construct();

里面就调用对象的构造过程

2.1.2对象的析构:::destroy();

这里除了一个调用对象析构函数的destroy()函数外,其他的这些destroy函数又是什么意思呢?
destroy函数泛化版 接受两个迭代器作为参数,也就是会把这两个迭代器指向内存之间的对象都给析构掉
其内部调用__destroy函数,这个函数是什么作用呢?
这个函数会先判断迭代器指向对象有没有 trivial destructor()
通过什么去判断呢?通过第三个参数trival_destructor()去判断对象有没有trival_destructor,
如果有的话 这个值就成为__true_type 然后通过函数模板机制调用__destroy_aux的下一个,在里面什么也没做
如果没有的话 这个值就成为__false_type,然后通过函数模板机制调用__destroy_aux的上一个,在里面会调用对象自带的析构函数

为什么要绕这么多弯子,不直接用对象自带的析构函数?  
在c 的类中如果只有基本的数据类型,也就不需要写显式的析构函数,即用默认析构函数就够用了,也就是直接用trival_destructor, 这个时候不必要写自己的析构函数就会调用默认的trival_destructor,,
但是如果类中有个指向其他类的指针,并且在构造时候分配了新的空间,则在析构函数中必须显式释放这块空间,否则会产生内存泄露.
正是因为在这一点上有效率提升的空间,所以在allocaor中才绕了这么多弯子,先判断对象中有trival_destructor,,有的话说明对象没有必要调用它自己的析构函数,直接在函数里不作操作就行了,因为对象自己会自动调用trival_destructor,
.反之,如果没有trival_destructor,,说明对象有自己的析构函数,这个时候就得调用它自己的析构函数了.

2.2----内存的配置与释放

2.2.0 概述

  为了解决小型区块所可能造成的内存破碎问题,sgi设计了双层级配置器,

当申请的内存大小大于128byte时,就启动第一级分配器通过malloc直接向系统的堆空间分配,如果申请的内存大小小于128byte时,就启动第二级分配器,从一个预先分配好的内存池中取一块内存交付给用户,这个内存池由16个不同大小(8的倍数,8~128byte)的空闲列表组成,allocator会根据申请内存的大小(将这个大小round up成8的倍数)从对应的空闲块列表取表头块给用户。

这种做法有两个优点:

1)小对象的快速分配。小对象是从内存池分配的,这个内存池是系统调用一次malloc分配一块足够大的区域给程序备用,当内存池耗尽时再向系统申请一块新的区域,整个过程类似于批发和零售,起先是由allocator向总经商批发一定量的货物,然后零售给用户,与每次都总经商要一个货物再零售给用户的过程相比,显然是快捷了。当然,这里的一个问题时,内存池会带来一些内存的浪费,比如当只需分配一个小对象时,为了这个小对象可能要申请一大块的内存池,但这个浪费还是值得的,况且这种情况在实际应用中也并不多见。

2)避免了内存碎片的生成。程序中的小对象的分配极易造成内存碎片,给操作系统的内存管理带来了很大压力,系统中碎片的增多不但会影响内存分配的速度,而且会极大地降低内存的利用率。以内存池组织小对象的内存,从系统的角度看,只是一大块内存池,看不到小对象内存的分配和释放。

 

2.2.1 一级配置器:__malloc_alloc_template

 上面说过, sgi stl中, 如果申请的内存区域大于128b的时候,就会调用一级适配器,

而一级适配器的调用也是非常简单的, 直接用malloc申请内存,用free释放内存。

简单的对malloc和free的封装

可也看下如下的代码:

class __malloc_alloc_template {
private:
  // oom = out of memroy,当内存不足的时候,我要用下面这两个函数
  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;
  }
 //模拟c  的 set_new_handler,函数,
 //为什么要模拟,因为现在用的是c的内存管理函数。
  static void (* __set_malloc_handler(void (*__f)()))()
  {
    void (* __old)() = __malloc_alloc_oom_handler;
    __malloc_alloc_oom_handler = __f;
    return(__old);
  }
};

 

2.2.2 二级配置器:__default_alloc_template

接下来我们的主角,二级配置器登场了.

先看它的源码

template 
class __default_alloc_template {
private:
  // really we should use static const int x = n
  // instead of enum { x = n }, but few compilers accept the former.
    enum {_align = 8};//小块区域的上界
    enum {_max_bytes = 128};//小块区域的下降
    enum {_nfreelists = 16}; // _max_bytes/_align,有多少个区域
/*sgi 为了方便内存管理, 把128b 分成16*8 的块*/
//将byte调到8的倍数
  static size_t
  _s_round_up(size_t __bytes) 
    { return (((__bytes)   (size_t) _align-1) & ~((size_t) _align - 1)); }
//管理内存的链表,待会会详细分析这个
  union _obj {
        union _obj* _m_free_list_link;
        char _m_client_data[1];    /* the client sees this.        */
  };
private:
    //声明了16个 free_list, 注意 _s_free_list是成员变量
    static _obj* __stl_volatile _s_free_list[_nfreelists];
 //同了第几个free_list, 即_s_free_list[n],当然这里是更具区域大小来计算的
  static  size_t _s_freelist_index(size_t __bytes) {
        return (((__bytes)   (size_t)_align-1)/(size_t)_align - 1);
  }
  // returns an object of size __n, and optionally adds to size __n free list.
  static void* _s_refill(size_t __n);
  // allocates a chunk for nobjs of size size.  nobjs may be reduced
  // if it is inconvenient to allocate the requested number.
  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:
   //分配内存,容后分析
  /* __n must be > 0      */
  static void* allocate(size_t __n);
   //释放内存,容后分析
  /* __p may not be 0 */
  static void deallocate(void* __p, size_t __n);
  //从新分配内存
  static void* reallocate(void* __p, size_t __old_sz, size_t __new_sz);
 }
  //下面是一些 成员函数的初始值的设定
template 
char* __default_alloc_template<__threads, __inst>::_s_start_free = 0;
template 
char* __default_alloc_template<__threads, __inst>::_s_end_free = 0;
template 
size_t __default_alloc_template<__threads, __inst>::_s_heap_size = 0;
template 
typename __default_alloc_template<__threads, __inst>::_obj* __stl_volatile
__default_alloc_template<__threads, __inst> ::_s_free_list[] = 
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };

咱们从源码中从上往下分析

 为了更好的应对程序的内存请求,
若程序请求区块,大于128bytes,就调用一级配置器
若程序请求区块小于128bytes则用它自己的管理方式
在请求小于128bytes时,为了更好的规划内存,引入了一个数组 链表的结构,数组中存放链表的头指针,
这里的数组有16个元素 ,分别对应了16个链表的头指针,这些链表中的结点就代表了不同内存区块的大小
这16个链表各自结点的内存区块大小分别是8bytes,16bytes,32bytes......128bytes ,以8的倍数递增
然后每个链表一般有十几个这样的结点连在一起,现在给这些管理内存区块的链表起个名字叫 free list
他们的结构如下图

有一个问题
为什么链表结点要用union结构而不直接用类去声明?
因为用类声明要额外的去维护指针,会带来额外的内存开销,
而用union的话, 由于占用的内存区域是重合的,所以就不会带来这个问题.

 举个例子

举个例子

  • 内存的组织结构
    //管理内存的链表结点
    union obj{
        union obj* free_list_link;
        char client_data[1];  
    }
    //free list链表头结点指针数组 默认有16个 即__nfreelists == 16
    static obj * volatile free_list[__nfreelists];
    
  • 内存的分配
       //分配内存
      static void* allocate(size_t __n);
    static void * allocate(size_t n){
      obj * volatile * my_free_list; // 是指向obj*的指针
      obj * result;
      // 如果大于128就调用第一级适配器
      if(n > (size_t) __max_bytes){
          return(malloc_alloc::allocate(n));
      }
      // 找到16个当中合适的free_list
      my_free_list = free_list   freelist_index(n);
      result  = * my_free_list;
      if(result == 0){
        //如果没有找到可用的free_list,准备重新填充free_list
        void *r = refill(round_up(n));
        return r;
      }
      *my_free_list = result -> free_list_link;// 这里是理解的关键,其实result已经指向了第一个可用区块,区块的第一个字节是下一个可用区块也是下一个obj的地址,所以这句话就已经移除了第一个可用区块。
      return result;
    }

    对照源码,总结了下面两张图来说明内存的创建过程

    比较关键的是上面的step3中的refill函数 他负责在free list中没内存的时候向内存池要内存
    那refill()函数是怎么工作的呢?
    先看源码

    //返回一个大小为n的对象,并且有时候会为适当的free list增加节点
    //假设n已经适当的上调至8的倍数
    template 
    void*
    __default_alloc_template<__threads, __inst>::_s_refill(size_t __n)
    {
        int __nobjs = 20;
        //调用_s_chunk_alloc(),尝试取得__nobjs个区块作为free list 的新的节点
        //注意参数__nobjs为引用
        char* __chunk = _s_chunk_alloc(__n, __nobjs);
        _obj* __stl_volatile* __my_free_list;
        _obj* __result;
        _obj* __current_obj;
        _obj* __next_obj;
        int __i;
        //如果只获得一个区块,这个区块就分配给调用者用,free list无新节点
        if (1 == __nobjs) return(__chunk);
        //否则准备调整free list,纳入新节点
        __my_free_list = _s_free_list   _s_freelist_index(__n);
        //在__chunk空间内建立free list
          __result = (_obj*)__chunk; //这一块准备返回给客端
          //以下引导free list指向新配置的空间(取自内存池)
          *__my_free_list = __next_obj = (_obj*)(__chunk   __n); 
          //以下将free list的各节点串接起来
          for (__i = 1; ; __i  ) { //从1开始,因为第0个将返回给客端
            __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);
    }

    一张图搞懂他的工作过程

    由上面的分析得知,其关键作用的是chunk_alloc函数 
    那么他是如何工作的呢
    照例,我们先上源码
     

    //假设__size已经适当上调至8的倍数
    //注意__nobjs为引用类型
    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 {
        //内存池剩余空间连一个区块大小都无法提供
            size_t __bytes_to_get = 
          2 * __total_bytes   _s_round_up(_s_heap_size >> 4);
            // 试着让内存池中的残余零头还有利用价值
            if (__bytes_left > 0) {
                //内存池还有零头,先配给适当的free list
                //首先寻找适当的free list
                _obj* __stl_volatile* __my_free_list =
                            _s_free_list   _s_freelist_index(__bytes_left);
                //调整free list,将内存中的残余内存空间编入
                ((_obj*)_s_start_free) -> _m_free_list_link = *__my_free_list;
                *__my_free_list = (_obj*)_s_start_free;
            }
            //配置heap空间,用来填充内存池
            _s_start_free = (char*)malloc(__bytes_to_get);
            //heap空间不足,malloc()失败
            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) { //free list内尚有未用区块
                        //调整free list以释放出未用区块
                        *__my_free_list = __p -> _m_free_list_link;
                        _s_start_free = (char*)__p;
                        _s_end_free = _s_start_free   __i;
       //递归调用自己,为了修正nobjs                 return(_s_chunk_alloc(__size, __nobjs));
                        // 任何残余零头终将被编入适当的free list中备用
                    }
                }
            _s_end_free = 0;    // 如果出现意外
                //调用第一配置器,看看out-of-memory机制能否尽点力
                _s_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
                //这会导致抛出异常,或内存不足的情况获得改善
            }
            _s_heap_size  = __bytes_to_get;
            _s_end_free = _s_start_free   __bytes_to_get;
            //递归自己,修正nobjs
            return(_s_chunk_alloc(__size, __nobjs));
        }
    }

    下面分析chunk_alloc的工作过程

  • 内存的回收 
    先看源码
    void alloc::deallocate(void* ptr, size_t size) {
        if (size > maxbytes) {
            free(ptr);
        }
        else {
            size_t index = freelist_index(size);
            static_cast(ptr)->next = freelists[index];
            freelists[index] = static_cast(ptr);
        }
    }


    举个例子

c sgi 设计了双层级配置器。

第一级配置器:__malloc_alloc_template

第二级配置器 :__default_alloc_template

第一级配置器直接使用 malloc()和 free()完成内存的分配和回收。第二级配置器则根据需求量的大小选择不同的策略执行。 

对于第二级配置器,如果需求块大小大于 128bytes,则直接转而调用第一级配置器,使用 malloc()分配内存。

如果需求块大小小于 128 bytes,第二级配置器中维护了 16 个自由链表,负责 16 种小型区块的次配置能力。

即当有小于 128bytes 的需求块要求时,首先查看所需需求块大小所对应的链表中是否有空闲空间,如果有则直接返回,如果没有,则向内存池中申请所需需求块大小的内存空间,如果申请成功,则将其加入到自由链表中。

如果内存池中没有空间,则使用 malloc() 从堆中进行申请,且申请到的大小是需求量的二倍(或二倍+n 附加量),一倍放在自由空间中,一倍(或一倍+n)放入内存池中。

如果 malloc()也失败,则会遍历自由空间链表,四处寻找“尚有未用区块,且区块够大”的 freelist,找到一 块就挖出一块交出。

如果还是没有,仍交由 malloc()处理,因为 malloc()有 out-of-memory 处理机制或许有机会释放其他的内存拿来用,如果可以就成功, 如果不行就报 bad_alloc 异常。

网站地图