STL 空间配置器篇

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_26822029/article/details/82915529

如果你不曾仔细研读STL源码,你是不会发现在STL还会有空间配置器的存在的,因为它是隐藏在一切组件(特别是容器)的背后,默默工作。如果你需要自己实现一个STL,最先设计的就应该是空间配置器,因为它是一切STL的基础。

零、为何STL要单独设计空间配置器?

一开始我有过这样的疑惑:为什么STL不直接使用malloc和free操纵内存即可,为什么还要设计空间配置器呢?这不是多此一举吗?后来在学习之后才明白这样做是为了进一步提高内存的使用率和使用效率。主要是从以下两方面来考虑的:

1.小块内存会带来内存碎片问题

如果任由STL中的容器自行通过malloc分配内存,那么频繁的分配和释放内存会导致堆中有很多的外部碎片。可能堆中的所有空闲空间之和很大,但当申请新的内存的请求到来时,没有足够大的连续内存可以分配,这将导致内存分配失败。因此这样会导致内存浪费。

2.小块内存的频繁申请释放会带来性能问题

开辟空间的时候,分配器需要时间去寻找空闲块,找到空闲块之后才能分配给用户。而如果分配器找不到足够大的空闲块可能还需要考虑处理加碎片现象(释放的小块空间没有合并),这时候需要花时间去合并已经释放了的内存空间块。

而且malloc在开辟内存空间的时候,还会附带附加的额外信息,因为系统需要靠多出来的额外信息管理内存。特别是区块越小,额外负担所占的比例就越大,更加显得浪费。

STL设计者为了解决这些问题,将STL(SGI版)的空间配置器分为两级:一级空间配置器(__malloc_alloc_template)和二级空间配置器(__default_alloc_template)。在STL中如果你申请的内存大于128个字节,那么直接调用一级空间配置器向内存申请内存,如果你申请的内存小于等于128个字节,将被认为是小内存,那么将会调用二级空间配置器直接去内存池中申请。一级配置器和二级配置器的设计将在下面仔细讲解。

一、STL中空间配置器的工作流程

考虑到小型区块可能造成的内存碎片问题,SGI设计了双层配置器,第一级配置器直接使用malloc()和free(),第二级配置器则视情况采用不同的策略:当配置区块超过128字节时,视之为“足够大”,便调用第一级配置器;当配置区块小于128字节时,视之为“过小",为了降低额外负担,便采用复杂的memory pool整理方式,而不再求助于第一级配置器。整个设计究竟只开放第一级配置器,或是同时开放第二级配置器,取决于__USE_MALLOC是否被定义。

                                                                                                                                             ——《STL 源码剖析》 第2章 空间配置器

1.1 STL空间配置器文件组成

STL空间配置器主要分为三个文件实现:

  1. <stl_construct.h> 定义了全局函数construct()和destroy(),负责对象的构造和析构。
  2. <stl_alloc.h> 定义了一二级配置器,配置器统称为alloc而非allocator!
  3. <stl_uninitialized.h> 定义了一些全局函数,用来填充(fill)或者复制(copy)大块内存数据,也隶属于STL标准规范。

1.2 第一级配置器与第二级配置器

关于第一级配置器(__malloc_alloc_template)和第二级配置器(__default_alloc_template)的工作原理与实现代码将在后面两节介绍,本节我们简单看一下第一级配置器和第二级配置器的使用。如在《STL 源码剖析》中所诉:整个空间配置器是只开放第一级配置器还是同时开放第二级配置器取决于是否定义了__USE_MALLOC,然而在SGI STL中并没有定义__USE_MALLOC,因此在SGI STL中同时开放了第一级配置器和第二级配置器。

如上图所示,因为没有定义__USE_MALLOC,SGI将alloc定义为第二级配置器。默认的空间配置器选择第二级配置器,在第二级配置器中判断申请的内存空间是否大于128字节,如果大于128字节直接在第二级配置器中调用第一级配置器分配内存。如下图所示:

二、一级空间配置器设计与实现 

一级空间配置器只是简单的封装了一下malloc和free实现的。在allocate函数中如果通过malloc申请内存失败(失败返回0)就改用oom_malloc(size_t n)函数尝试分配内存,如果oom发现没有指定new-handler函数的话,那就直接调用__THROW_BAD_ALLOC,丢出bad_alloc或是直接通过exit(1)中止程序。

我们来看看allocate函数,这个函数执行的流程就如上面所诉:

static void * allocate(size_t n)
{
    void *result = malloc(n);//直接使用第一级分配器,直接使用malloc
    if (0 == result) result = oom_malloc(n);//第一级分配器失效了,那就使 用第二级分配器,oom(out of memeory)
    return result;//将分配的空间以void*的方式返回,用户可以随意转化为需要的类型
}

再来看看oom_malloc函数是如何实现的,很显然这里的内存不足处理函数是需要客户端来实现的,默认情况下为0,也即默认情况下将会直接执行__THROW_BAD_ALLOC。内存不足处理例程是客端的责任,设定内存不足处理例程也是客端的责任。如果你已经设置了out-of-memory handler,那么系统会调用你设定的处理程序,企图释放内存来给程序用。会一直循环直到成功分配到内存才结束。所以这个函数设计不好的话会出现死循环!这也是为什么STL里面默认没有设置处理机制。

// malloc_alloc out-of-memory handling
// 初值为0.有待客端设定
template <int inst>
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;

template <int inst>
void * __malloc_alloc_template<inst>::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);//如果内存已经得到满足的话,那么就可以返回了,如果不满足,那么
        //就继续释放,分配....
    }
}

这个过程盗用博主午饭要阳光的一幅图,我觉得特别直观:

三、二级空间配置器设计与实现

SGI STL空间配置器的第二级配置器才是值得讨论的。

二级空间配置器为了避免小块内存带来的碎片化问题,使用内存池+free_list的形式管理内存。第二级空间配置器的代码如下所示。若申请的内存大于128个字节则直接调用一级空间配置器,如果不大于就将任何小额区块的内存__n上调至8的倍数(即使你请求1个字节,那也会申请8个字节)。这是因为在二级空间配置器中内存是通过free_list管理的。

*注意这里出现了关键字voliate:确保本条指令不会因为编译器的优化而省略,而且要求每次从内存中直接读取值

static void* allocate(size_t __n)
{
    void* __ret = 0;
    // 如果大于128 bytes,则使用第一级空间配置器
    if (__n > (size_t) _MAX_BYTES) {
      __ret = malloc_alloc::allocate(__n);
    }
    else {
      // 通过大小取得free-list数组下标,随后取得对应节点的指针
      // 相当于&_S_free_list[_S_freelist_index(__n)]
      _Obj* __STL_VOLATILE* __my_free_list
          = _S_free_list + _S_freelist_index(__n);
      _Obj* __RESTRICT __result = *__my_free_list;
      // 如果没有可用的节点,则通过_S_refill分配新的节点
      if (__result == 0)
        __ret = _S_refill(_S_round_up(__n));
      else {
        // 将当前节点移除,并当做结果返回给用户使用
        *__my_free_list = __result -> _M_free_list_link;
        __ret = __result;
      }
    }

    return __ret;
}

free_list如下图所示,为了便于管理,二级空间配置器在分配的时候都是以8的倍数对齐。因此free_list含有16个结点,分别管理大小为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128字节大小的内存。

继续分析上面的二级分配器分配内存过程:将程序需要申请的内存大小__n上调至8的整数倍后,就去自由链表free_list中相应的结点下面找,如果该结点下挂有未使用的内存,则直切返回这块空间的地址,并且更新指向该块内存地址的指针。如果该结点下没有可用的内存,就需要调用refill(size_t n)函数去内存池中申请。申请的流程再次盗用良心示意图:

》》》此处未完待续《《《 

四、本篇参考资料

《STL源码剖析》 侯捷 著

揭秘——STL空间配置器  https://blog.csdn.net/lf_2016/article/details/53511648

《STL源码剖析》学习笔记-第2章 空间配置器 https://blog.csdn.net/will130/article/details/51315647

STL空间配置器(一)  https://blog.csdn.net/hujian_/article/details/51063935

猜你喜欢

转载自blog.csdn.net/qq_26822029/article/details/82915529
今日推荐