本文大致对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. 二次剩余-Cipolla

    二次剩余 \(Cipolla\) 算法 概述 大概就是在模 \(p\) 意义下开根号,如求解方程\(x^2\equiv n(mod\ p)\). 这里只考虑 \(p\) 为素数的情况.若 \(p=2\ ...

  2. UWP 中的 LaunchUriAsync,使用默认浏览器或其他应用打开链接

    古老的 Win32 应用启动其他程序太过方便,以至于一部分开发者都已经不记得 Windows 能通过关联协议的方式通过统一资源定位符(URI)来启动应用程序了. 转到 UWP 后,使用 URI 启动应 ...

  3. Python3 shutil模块

    平时我们总会用到复制文件的命令,Python中自带了相应模块,那就是shutil模块,下面是shutil模块的分析及用法. 1.copyfileobj(fsrc, fdst, length=16*10 ...

  4. HTML5的28个常用特性

    1. 新的Doctype 尽管使用<!DOCTYPE html>,即使浏览器不懂这句话也会按照标准模式去渲染 2. Figure元素 用<figure>和<figcapt ...

  5. 【java基础】java集合之HashTable,HashSet,HashMap

    [一]HashSet (1)HashSet内部维护的是一个HashMap,具体原理见java集合之HashMap [二]HashTable (1)HashTable内部维护的是一个Entry的数组.E ...

  6. sqlopsstudio 微软开源跨平台sql server 连接工具

    sqlopsstudio  微软开源的sql server 连接工具,跨平台,基于vscode 分之代码. 项目地址: https://github.com/Microsoft/sqlopsstudi ...

  7. js各种效果

    1.JavaScript 仿LightBox内容显示效果 2.固定高度的div,竖直方向出现滚动条,水平方向固定 http://www.jb51.net/css/109928.html <!do ...

  8. WinForm界面布局控件WeifenLuo.WinFormsUI.Docking"的使用 (一)

    WinForm界面布局控件WeifenLuo.WinFormsUI.Docking"的使用 (一) 编写人:CC阿爸 2015-1-28 在伍华聪的博客中,看到布局控件"Weife ...

  9. C++直接初始化和复制初始化1

    这篇文章主要介绍了C++直接初始化与复制初始化的区别深入解析,是很多C++初学者需要深入了解的重要概念,需要的朋友可以参考下   C++中直接初始化与复制初始化是很多初学者容易混淆的概念,本文就以实例 ...

  10. 安装ecb

    mac emacs上安装ecb,通过elpa折腾得要死,死活无法使用. 解决办法:下载https://github.com/alexott/ecb,添加路径,(require 'ecb),直接ok.