STL源码剖析之空间配置器

本文大致对STL中的空间配置器进行一个简单的讲解,由于只是一篇博客类型的文章,无法将源码表现到面面俱到,所以真正感兴趣的码农们可以从源码中或者《STL源码剖析》仔细了解一下。

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

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

⑵ 我们都知道malloc是系统调用,频繁使用会无疑会使我们的程序性能下降许多,所以我们可以通过申请一块大的内存,自己进行管理,那无疑会使得malloc调用的次数降低。-》性能问题

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

 

2,我们知道了原因,那STL是如何设计空间配置器并避免以上问题?

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

template<class T, class Alloc>

class simple_alloc{

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有效的防止了这一点。

 

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

posted @ 2017-07-11 17:40  不想写代码的DBA  阅读(182)  评论(0编辑  收藏  举报