STL源码剖析之空间配置器

本文大体对STL中的空间配置器进行一个简单的讲解,因为只是一篇博客类型的文章,没法将源码表现到面面俱到,因此真正感兴趣的码农们能够从源码中或者《STL源码剖析》仔细了解一下。数组

1,为何STL要专门写一个空间配置器管理空间的分配和释放,不能直接使用Malloc吗?数据结构

⑴ 咱们若是频繁的进行一些小空间的申请与释放,加入先申请10个字节的空间,而后每隔一个4字节,将其释放,那后面若是要再次申请比4个字节大的空间,那毫无疑问,前面已经被释放的是没法使用的。-》内存碎片问题架构

⑵ 咱们都知道malloc是系统调用,频繁使用会无疑会使咱们的程序性能降低许多,因此咱们能够经过申请一块大的内存,本身进行管理,那无疑会使得malloc调用的次数下降。-》性能问题函数

对于内存碎片的问题若是不太理解,看一下这个图立马就懂(盗图)性能

 

2,咱们知道了缘由,那STL是如何设计空间配置器并避免以上问题?spa

⑴ STL将空间配置器分为1、二级,若是用户申请空间是大于128个字节,则利用一级空间配置器使用系统调用malloc分配空间,若是小于128字节,则利用二级空间配置器进行内存管理分配。(此处应有源码)设计

template<class T, class Alloc>指针

class simple_alloc{blog

pubulic:递归

  static T *allocate(size_t n)

  { return 0 == n? 0 : (T*)Alloc::allocate(n* sizeof(T)); }

  ...

}

这个类中一共实现了4个方法,2个分配,2个释放(参数不一样而已),咱们若是看源码,能够看到STL中申请或释放内存均是使用simple_alloc的方法,其实就是作一个封装,最主要的调用方法仍是类型Alloc,再往底层看Alloc是怎么来的:

template<class T, class Alloc = alloc>

class vector{

typedef simple_alloc<T, Alloc> data_alloctor;

data_alloctor::allocate(n);

...

}

上段代码就已经说明了simple_alloc的使用,那还有一个问题alloc又是怎么来的,咱们怎么控制这里调用的是以及仍是二级呢?来,继续(封装好累啊。。。)

#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

能够经过源码看到咱们是经过__USE_MALLOC控制是否开启二级配置器的功能,STL中__USE_MALLOC的值默认是为FALSE的,因此会优先使用二级配置器,若是申请内存大小在二级配置器管理的List中没有找到,仍是会调用一级配置器的申请方法。

 

⑵ 二级空间配置器的组织架构就是一个16个元素的自由链表管理,每个块下面都挂着各自管理大小的内存(默认是以8为位数的内存块,8,16,24,32...128字节)。每一个节点的结构都是一个共用体:

union obj

{

  union ogj* free_list_link;

  char client_data[1];

}

这里使用共用体的缘由主要是由于管理内存的数据结构是一个链表,指向下一个节点的指针有可能会成为内存的负担,使用共用体后free_list_link既能够指向相同形式的另外一个obj,一样这个指针也能够指向实际的内存块(这句话不是特别理解,有点乱)。下一个图应该就能够了解了:

从图中就能够看到free_list能够经过指针相连,下方须要管理的内存块也能够经过指针相连,此处指针会指向还未被使用的内存地址,以便于管理。具体的存放过程来看看下图:

在空间没有分配出去,当前my_free_list指向的是当前96字节内存管理块最开始的位置,须要时,直接将这块内存分配出去,放出须要的值n,而后my_free_list直接指向下一个节点就好。而后释放是一样的:

只要将my_free_list指针指向当前释放空间,而后将空间与后面的块连起来。(my_free_list整个操做彻底是链表操做,只看图的确像数组,但若是是数组咱们就很难对申请和释放进行快速管理)。

 

3,理论架构讲完,是不是要看一下代码了?

static void * allocate(size_t n)

{

  obj * volatile * my_free_list;

  obj * result;

  //先判断是否大于128字节

  if(n > (size_t) __MAX_BYTES)  // enum {   __MAX_BYTES   =   128};

  {

    return (malloc_alloc::allocate(n));  //调用一级空间配置器

  }

  //根据大小寻找适合的内存块

  my_free_list = free_list + FREELIST_INDEX(n);      //函数实现为 return ((n + __ALIGN - 1) / __ALIGN - 1 );     __ALIGN 是 8

  result = *my_free_list;

  if(result == 0)

  {

    //没有找到可用的内存块,准备从新填充

    void *r = refill(ROUND_UP(n));  //函数实现为 return ((n + __ALIGN - 1) & ~( __ALIGN - 1) );    实际意义是取整,好比是1,2,3,4,5,6,7   均取 8

    return r;

  }

  *my_free_list = result -> free_list_link;  //须要分出去一块,因此指向下一块

  return result;

}

上述为分配空间的函数:先比较大小,大于128字节就是用一级空间配置,不然是用二级配置。而后再管理列表里面找内存,没有可以使用的就须要进行从新填充(后面会说,主要原理就是有内存就再次申请一块,没有就须要调整一些没有用到的大内存块,好比128字节不经常使用,就拉过来用);若是有直接可以使用的直接改变指针的指向,返回当前块就OK。下面再来看看释放空间的函数。

static void deallocate(void *p, size_t n)

{

  obj *q = (obj*) p;

  boj * volatile * my_free_list;

  //一样,大于128字节使用一级控件配置器

  if(n > (size_t)__MAX_BYTES)

  {

    malloc_alloc::deallocate(p, n);

    return;

  }

  my_free_list = free_list + FREELIST_INDEX(n);

  q -> free_list_link = *my_free_list;

   *my_free_list = q;

}

上述就是空间配置器最重要的二级配置器的申请和释放的过程,看完是否是感受很简单,固然,真正运做起来还须要一系列的辅佐函数,好比填充函数refill的实现。

template <bool threads, int inst>

void * __default_alloc_template<threads, inst>::refill(size_t n)

{
  int nobjs = 20;  //默认是填充当前要申请的20个内存块大小的内存

  char * chunk = chunk_alloc(n , nobjs);  //chunk_alloc函数能够将新申请的空间做为free_list的新节点(做用仍是蛮多的)

  obj * volatile * my_free_list;

  obj * result;

  obj * current_obj, * next_boj;

  int i;

 

  if(nobjs  == 1)  return chunk; //我的以为有点多余...,首先nobjs是一个常量,默认20,并且在chunk_alloc函数传进去的参数也不是&传递,因此不会修改。

  my_free_list = free_list + FREELIST_INDEX(n);

  //新开辟的空间须要按照咱们的规则链起来

  result = (obj *)chunk;

  *my_free_list = next_obj = (obj*)(chunk + n);

  for(i = 1; ;i++) //经过一个For循环,将整个大块分红20个小块,而后相互串联起来

  { ... }

  return result;

}

chunk_alloc函数是空间配置器中的内存池函数,主要内容就是先判断内存池中是否有空间,有的话直接将某一段地址空间返回就OK了,若是大小不够提供当前申请的块数(20块),就会有多少给多少。固然若是内存不够不会直接返回,会进一步调用malloc进行申请空间,补充内存池的不足,若是连堆中内存也没有空余的了,那就必须对当前free_list进行碎片整理,好比须要大内存块,就将小内存块进行整合,若是须要小内存块,则会将最大内存块进行分解,这里会采用一个递归调用自身来进行处理,固然若是连自身也没有可用块,那就是真的山区穷水尽了,就会交给一级空间配置器,一级空间配置有响应的new_handler机制,若是申请不到控件,会返回异常。具体源码有点多,也比较麻烦,大体的过程就是上述内容,想要仔细了解的能够参考源码剖析。

上述全部就是STL中空间配置器最重要也是最麻烦的二级空间配置器的源码和总体的工做流程,至于一级空间配置器呢,主要经过系统调用malloc/realloc等函数,详细就不在此说明了,有兴趣能够自行了解。

 

4,STL之每篇一坑?

STL控件配置器有坑吗?那确定是有的:

1,最重要的就是空间回收问题,若是有仔细研究过源码的人,必定发现咱们若是使用二级空间配置器所开辟出来的空间,若是挂在free_list上以后,使用完了调用deallocate函数,仅仅只能讲空间的管理权还给free_list而已,它但是没有还给真正的堆啊,而后再仔细过一下释放空间的流程,发现它彻底没有释放给堆啊。。。也就说当咱们使用了大量内存以后,释放并无真正将空间释放出去,而是挂在了free_list上面,这是否是很坑啊?

2,还有一点较为坑的就是二级空间配置器每块内存都是8的倍数,也就是否是8的倍数时补全是会浪费部分空间的。

但整体来说,STL的一个效率和适用性仍是很强的,它无疑是尽量的减小了内存碎片的产生,相比一个项目,咱们不断的去开辟和释放空间,是很是容易形成碎片产生,而STL有效的防止了这一点。

 

 本章主要就写这么多,网上有看过一些这方面的博客,整体来讲本文已经很详细了。

相关文章
相关标签/搜索