本文大致对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有效的防止了这一点。

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

STL源码剖析之空间配置器的更多相关文章

  1. STL源码分析之空间配置器

    前言 SGI STL将new的申请空间和调用构造函数的两个功能分开实现, 如果对new不太清楚的, 可以先去看看这一篇new实现再来看配置器也不迟. 本节是STL分析的第一篇, 主要分析STL各个部分 ...

  2. 《STL源码剖析》环境配置

    首先,去侯捷网站下载相关文档:http://jjhou.boolan.com/jjwbooks-tass.htm. 这本书采用的是Cygnus C++ 2.91 for windows.下载地址:ht ...

  3. STL源码剖析:配接器

    启 配接器就是适配器 STL中的适配器一共三种: 迭代器适配器 是一种观念上的改变,如将赋值操作变成插入,前进变成后退,等 函数适配器 STL中最广泛的配接器群体 可以实现连续配接 配接操作:bind ...

  4. STL源码剖析 — 空间配置器(allocator)

    前言 以STL的实现角度而言,第一个需要介绍的就是空间配置器,因为整个STL的操作对象都存放在容器之中. 你完全可以实现一个直接向硬件存取空间的allocator. 下面介绍的是SGI STL提供的配 ...

  5. STL源码剖析——空间配置器Allocator#1 构造与析构

    以STL的运用角度而言,空间配置器是最不需要介绍的东西,因为它扮演的是幕后的角色,隐藏在一切容器的背后默默工作.但以STL的实现角度而言,最应该首先介绍的就是空间配置器,因为这是这是容器展开一切运作的 ...

  6. STL"源码"剖析-重点知识总结

    STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点略多 :) 1.STL概述 STL提供六大组件,彼此可以组合 ...

  7. 【转载】STL"源码"剖析-重点知识总结

    原文:STL"源码"剖析-重点知识总结 STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点 ...

  8. (原创滴~)STL源码剖析读书总结1——GP和内存管理

    读完侯捷先生的<STL源码剖析>,感觉真如他本人所说的"庖丁解牛,恢恢乎游刃有余",STL底层的实现一览无余,给人一种自己的C++水平又提升了一个level的幻觉,呵呵 ...

  9. STL"源码"剖析

    STL"源码"剖析-重点知识总结   STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点略 ...

随机推荐

  1. kmp学习小结

    KMP 简要说明 \(kmp\)是一个非常神奇的东西.它的\(fail(next)\)数组\(f[i]\)就表示\(1\)~\(i\)这个串的最长公共前缀后缀长度.根据这个\(fail\)数组,在匹配 ...

  2. Python学习-类

    类是对象的模板或蓝图,类是对象的抽象化,对象是类的实例化 在python中,一个对象的特征也称为属性(attribute),它所具有的的行为也称为方法(method) 对象 = 属性(特征)+方法(行 ...

  3. Robot Framework-断言函数

    测试用例的目的是要验证一些操作否符合我们的预期结果,所以在测试用例中,断言函数是必不可少的一项.我们做的每一步操作都会有预期的结果,为了保证操作得到的结果符合预期,我们需要在测试用例中添加断言,来保证 ...

  4. 洛谷 P2828 Switching on the Lights(开关灯)

    传送门 题目大意:n*n的网格,每个网格是一个房间 都关着灯,只有(1,1)开着灯,且(x,y)有着(z,k)房间灯的开关. 问从(1,1)开始走最多点开几盏灯. 题解:搜索+骗分. 劳资的骗分天下无 ...

  5. PAT 1009 说反话 C语言

    给定一句英语,要求你编写程序,将句中所有单词的顺序颠倒输出. 输入格式:测试输入包含一个测试用例,在一行内给出总长度不超过80的字符串.字符串由若干单词和若干空格组成,其中单词是由英文字母(大小写有区 ...

  6. gradle 插件

    1. 系统内置插件的应用 a. 二进制 apply plugin :"pluginname" 比如: java b. 脚本插件 apply from : "version ...

  7. 云原生应用基金会CNCF

    2006 年 8 月 9 日,埃里克·施密特(EricSchmidt)在搜索引擎大会上首次提出了“云计算”(Cloud Computing)的概念.一转眼十年过去了,它的发展势如破竹,不断渗透当代的 ...

  8. 在ng中的select的使用方法的讲解

    项目中我们可能会使用到条件过滤选择框之类的东西,最简单的就是input.select. 关于select的使用我们通常会需要从数据库中返回数据进行动态绑定. 此时我们会有两种方式: 1)使用ng-re ...

  9. redis数据结构对象

    redis的数据结构对象包括 字符串 列表 哈希 集合 有序集合五种数据结构对象,由底层的8种数据结构组成这五种对象,每种对象的实现不同的数据都是不一样的. 结构 typedef struct red ...

  10. (转)winform安装项目、安装包的制作、部署

    本文转载自:http://zhan.renren.com/cxymst?gid=3602888498037535727&from=post&checked=true 1,解决方案—添加 ...