大家好,我是小贺。

点赞再看,养成习惯

文章每周持续更新,可以微信搜索「herongwei」第一时间阅读和催更,本文 GitHub https://github.com/rongweihe/MoreThanCode 已经收录,有一线大厂面试点思维导图,也整理了很多我的文档,欢迎 star 和完善。一起加油,变得更好!

1、前言

天下大事,必作于细。

源码之前,了无秘密。

你清楚下面这几个问题吗?

  • 调用 new 和 delete 时编译器底层到底做了哪些工作?
  • STL 器底层空间配置原理是怎样的?
  • STL 空间配置器到底要考虑什么?
  • 什么是内存的配置和释放?

这篇,我们就来回答这些问题。

2、STL 六大组件

在深入配置器之前,我们有必要了解下 STL 的背景知识:

标准模板库(英文:Standard Template Library,缩写:STL),是一个 C++ 软件库。

STL 的价值在于两个方面,就底层而言,STL 带给我们一套极具实用价值的零部件以及一个整合的组织;除此之外,STL 还带给我们一个高层次的、以泛型思维 (Generic Paradigm) 为基础的、系统化的“软件组件分类学”。

STL 提供六大组件,了解这些为接下来的阅读打下基础。

  • 容器(containers):各种数据结构,如 vector, list, deque, set, map 用来存放数据。从实现的角度来看,STL 容器是一种 class template。

  • 算法(algorithms):各种常用的算法如 sort, search, copy, erase…从实现角度来看,STL 算法是一种 function template。

  • 迭代器(iterators):扮演容器与算法之间的胶合剂,是所谓的“泛型指针”。从实现角度来看,迭代器是一种将 operator *, operator ->, operator++, operator– 等指针相关操作予以重载的class template。

  • 仿函数(functors):行为类似函数,可以作为算法的某种策略。从实现角度来看,仿函数是一种重载了 operator() 的 class 或class template。

  • 适配器(adapters):一种用来修饰容器或仿函数或迭代器接口的东西。例如 STL 提供的 queue 和 stack,虽然看似容器,其实只能算是一种容器适配器,因为它们的底部完全借助 deque,所有操作都由底层的 deque 供应。

  • 配置器(allocator):负责空间配置与管理,从实现角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的 class template。

3、何为空间配置器

3.1 为何需要先了解空间配置器?

从使用 STL 层面而言,空间配置器并不需要介绍 ,因为容器底层都给你包装好了,但若是从 STL 实现角度出发,空间配置器是首要理解的。

作为 STL 设计背后的支撑,空间配置器总是在默默地付出着。为什么你可以使用算法来高效地处理数据,为什么你可以对容器进行各种操作,为什么你用迭代器可以遍历空间,这一切的一切,都有“空间配置器”的功劳。

3.2 SGI STL 专属空间配置器

SGI STL 的空间配置器与众不同,且与 STL 标准规范不同。

其名为 alloc,而非 allocator。

虽然 SGI 也配置了 allocatalor,但是它自己并不使用,也不建议我们使用,原因是效率比较感人,因为它只是在基层进行配置/释放空间而已,而且不接受任何参数。

SGI STL 的每一个容器都已经指定缺省的空间配置器是 alloc。

在 C++ 里,当我们调用 new 和 delete 进行对象的创建和销毁的时候,也同时会有内存配置操作和释放操作:

这其中的 new 和 delete 都包含两阶段操作:

  • 对于 new 来说,编译器会先调用 ::operator new 分配内存;然后调用 Obj::Obj() 构造对象内容。

  • 对于 delete 来说,编译器会先调用 Obj::~Obj() 析构对象;然后调用 ::operator delete 释放空间。

为了精密分工,STL allocator 决定将这两个阶段操作区分开来。

  • 对象构造由 ::construct() 负责;对象释放由 ::destroy() 负责。

  • 内存配置由 alloc::allocate() 负责;内存释放由 alloc::deallocate() 负责;

STL配置器定义在 中,下图直观的描述了这一框架结构

4、构造和析构源码

我们知道,程序内存的申请和释放离不开基本的构造和析构基本工具:construct() 和 destroy() 。

在 STL 里面,construct() 函数接受一个指针 P 和一个初始值 value,该函数的用途就是将初值设定到指针所指的空间上。

destroy() 函数有两个版本,第一个版本接受一个指针,准备将该指针所指之物析构掉。直接调用析构函数即可。

第二个版本接受 first 和 last 两个迭代器,将[first,last)范围内的所有对象析构掉。

其中 destroy() 只截取了部分源码,全部实现还考虑到特化版本,比如判断元素的数值类型 (value type) 是否有 trivial destructor 等限于篇幅,完整代码请参阅《STL 源码剖析》。

再来张图吧,献丑了。

5、内存的配置与释放

前面所讲都是对象的构造和析构,接下来要讲的是对象构造和析构背后的故事—(内存的分配与释放),这块是才真正的硬核,不要搞混了哦。

5.1 真· alloc 设计奥义

对象构造和析构之后的内存管理诸项事宜,由 <stl_alloc.h> 一律负责。SGI 对此的设计原则如下:

  • 向 system heap 要求空间

  • 考虑多线程 (multi-threads) 状态

  • 考虑内存不足时的应变措施

  • 考虑过多“小型区块”可能造成的内存碎片 (fragment) 问题

考虑到小型区块可能造成的内存破碎问题,SGI 为此设计了双层级配置器。当配置区块超过 128bytes 时,称为足够大,使用第一级配置器,直接使用 malloc() 和 free()。

当配置区块不大于 128bytes 时,为了降低额外负担,直接使用第二级配置器,采用复杂的 memory pool 处理方式。

无论使用第一级配接器(__malloc_alloc_template)或是第二级配接器(__default_alloc_template),alloc 都为其包装了接口,使其能够符合 STL 标准。

其中, __malloc_alloc_template 就是第一级配置器;

__default_alloc_template 就是第二级配置器。

这么一大堆源码看懵了吧,别着急,请看下图。

其中 SGI STL 将配置器多了一层包装使得 Alloc 具备标准接口。

6、alloc 一级配置器源码解读

这里截取部分(精华)解读

(1)第一级配置器以 malloc(), free(), realloc() 等 C 函数执行实际的内存配置、释放和重配置操作,并实现类似 C++ new-handler 的机制(因为它并非使用 ::operator new 来配置内存,所以不能直接使用C++ new-handler 机制)。

(2)SGI 第一级配置器的 allocate() 和 reallocate() 都是在调用malloc() 和 realloc() 不成功后,改调用 oom_malloc() 和oom_realloc()。

(3)oom_malloc() 和 oom_realloc() 都有内循环,不断调用“内存不足处理例程”,期望某次调用后,获得足够的内存而圆满完成任务,哪怕有一丝希望也要全力以赴申请啊,如果用户并没有指定“内存不足处理程序”,这个时候便无力乏天,真的是没内存了,STL 便抛出异常。或调用exit(1) 终止程序。

7、alloc 二级配置器源码解读

照例,还是截取部分(精华)源码解读。看累了嘛,远眺歇会,回来继续看,接下来的这部分,将会更加的让我们为大师的智慧折服!

第二级配置器多了一些机制,专门针对内存碎片。内存碎片化带来的不仅仅是回收时的困难,配置也是一个负担,额外负担永远无法避免,毕竟系统要划出这么多的资源来管理另外的资源,但是区块越小,额外负担率就越高。

7.1 SGI 第二级配置器到底解决了多少问题呢?

简单来说 SGI第二级配置器的做法是:sub-allocation (层次架构):

前面也说过了,SGI STL 的第一级配置器是直接使用 malloc(), free(), realloc() 并配合类似 C++ new-handler 机制实现的。第二级配置器的工作机制要根据区块的大小是否大于 128bytes 来采取不同的策略:

继续跟上节奏,上面提到了 memory pool ,相信程序员朋友们很熟悉这个名词了,没错,这就是二级配置器的精髓所在,如何理解?请看下图:

有了内存池,是不是就可以了,当然没有这么简单。上图中还提到了自由链表,这个又是何方神圣?

我们来一探究竟!

7.2 自由链表自由在哪?又该如何维护呢?

我们知道,一方面,自由链表中有些区块已经分配给了客端使用,所以 free_list 不需要再指向它们;另一方面,为了维护 free-list,每个节点还需要额外的指针指向下一个节点。

那么问题来了,如果每个节点有两个指针?这不就造成了额外负担吗?本来我们设计 STL 容器就是用来保存对象的,这倒好,对象还没保存之前,已经占据了额外的内存空间了。那么,有方法解决吗?当然有!再来感受一次大师的智慧!

(1)在这之前我们先来了解另一个概念——union(联合体/共用体),对 union 已经熟悉的读者可以跳过这一部分的内容;如果忘记了也没关系,趁此来回顾一下:

(a)共用体是一种特殊的类,也是一种构造类型的数据结构。

(b)共用体表示几个变量共用一个内存位置,在不同的时间保存不同的数据类型和不同长度的变量。

(c)所有的共用体成员共用一个空间,并且同一时间只能储存其中一个成员变量的值。例如如下:

一个union 只配置一个足够大的空间以来容纳最大长度的数据成员,以上例而言,最大长度是 double 类型,

所以 ChannelManager 的空间大小就是 double 数据类型的大小。在 C++ 里,union 的成员默认属性页为 public。union 主要用来压缩空间,如果一些数据不可能在同一时间同时被用到,则可以使用 union。

(2)了解了 union 之后,我们可以借助 union 的帮助,先来观察一下 free-list 节点的结构

来深入了解 free_list 的实现技巧,请看下图。

在 union obj 中,定义了两个字段,再结合上图来分析:

从第一个字段看,obj 可以看做一个指针,指向链表中的下一个节点;

从第二个字段看,obj 可以也看做一个指针,不过此时是指向实际的内存区。

一物二用的好处就是不会为了维护链表所必须的指针而造成内存的另一种浪费,或许这就是所谓的自由奥义所在!大师的智慧跃然纸上。

7.3 第二级配置器的部分实现内容

到这里,我们已经基本了解了第二级配置器中 free_list 的工作原理了。附上第二级配置器的部分实现内容源码:

太长了吧,看懵逼了,没关系,请耐心接着往下看。

8、空间配置器函数allocate源码解读

我们知道第二级配置器拥有配置器的标准接口函数 allocate()。此函数首先判断区块的大小,如果大于 128bytes –> 调用第一级配置器;小于128bytes–> 就检查对应的 free_list(如果没有可用区块,就将区块上调至 8 倍数的边界,然后调用 refill(), 为 free list 重新填充空间。

8.1 空间申请

调用标准接口函数 allocate():

NOTE:每次都是从对应的 free_list 的头部取出可用的内存块。然后对free_list 进行调整,使上一步拨出的内存的下一个节点变为头结点。

8.2 空间释放

同样,作为第二级配置器拥有配置器标准接口函数 deallocate()。该函数首先判断区块大小,大于 128bytes 就调用第一级配置器。小于 128 bytes 就找出对应的 free_list,将区块回收。

NOTE:通过调整 free_list 链表将区块放入 free_list 的头部。

区块回收纳入 free_list 的操作,如图所示:

8.3 重新填充 free_lists

(1)当发现 free_list 中没有可用区块时,就会调用 refill() 为free_list 重新填充空间;

(2)新的空间将取自内存池(经由 chunk_alloc() 完成);

(3)缺省取得20个新节点(区块),但万一内存池空间不足,获得的节点数可能小于 20。

8.4 内存池(memory pool)

唔…在前面提到了 memory pool,现在终于轮到这个大 boss 上场。

首先,我们要知道从内存池中取空间给 free_list 使用,是 chunk_alloc() 在工作,它是怎么工作的呢?

我们先来分析 chunk_alloc() 的工作机制:

chunk_alloc() 函数以 end_free – start_free 来判断内存池的“水量”(哈哈,很形象的比喻)。

具体逻辑都在下面的图里了,很形象吧。

如果第一级配置器的 malloc() 也失败了,就发出 bad_alloc 异常。

说了这么多来看一下 STL 的源码吧。



太长了,又看懵逼了吧,没关系,请看下图。

NOTE:上述就是 STL 源码当中实际内存池的操作原理,我们可以看到其实以共用体串联起来共享内存形成了 free_list 的实质组成。

9、本文小结

STL 源码本身博大精深,还有很多精妙的设计等着大家去探索。

小贺本人才疏学浅,在这里也只是在自己掌握的程度下写出自己的理解,不足之处希望对大家多多指出,互相讨论学习。

这篇肝了一个礼拜的文章,在周日的晚上终于写完了,文中所有的图都是自己一个个亲手画的,不画不知道,画完之后真心感觉不容易啊。

第一次写这种图解的文章,不知道读者朋友们喜不喜欢。如果反馈还不错的话,后面有时间会继续更新类型的图解文章。

参考文章:

《STL源码剖析-侯捷》

https://cloud.tencent.com/developer/article/1686585

https://dulishu.top/allocator-of-stl/

10、结尾

如果觉得文章对你有帮助,欢迎分享给你的朋友,一键三连,谢谢各位。

我是 herongwei ,是男人,就对自己狠一点,我们下期见。

文章每周持续更新,可以微信搜索「herongwei」第一时间阅读和催更,本文 GitHub https://github.com/rongweihe/MoreThanCode 已经收录,有一线大厂面试点思维导图,也整理了很多我的文档,欢迎 star 和完善。一起加油,变得更好!

【陪你系列】5 千字长文+ 30 张图解 | 陪你手撕 STL 空间配置器源码的更多相关文章

  1. C#进阶系列——一步一步封装自己的HtmlHelper组件:BootstrapHelper(三:附源码)

    前言:之前的两篇封装了一些基础的表单组件,这篇继续来封装几个基于bootstrap的其他组件.和上篇不同的是,这篇的有几个组件需要某些js文件的支持. 本文原创地址:http://www.cnblog ...

  2. Python解析器源码加密系列之(二):一次使用标准c的FILE*访问内存块的尝试

    摘要:由于近期打算修改Python解释器以实现pyc文件的加密/解密,出于保密的要求,解密之后的数据只能放在内存中,不能写入到文件中.但是后续的解析pyc文件的代码又只能接受FILE*作为入参,所以就 ...

  3. 一步步实现windows版ijkplayer系列文章之三——Ijkplayer播放器源码分析之音视频输出——音频篇

    一步步实现windows版ijkplayer系列文章之一--Windows10平台编译ffmpeg 4.0.2,生成ffplay 一步步实现windows版ijkplayer系列文章之二--Ijkpl ...

  4. 一步步实现windows版ijkplayer系列文章之二——Ijkplayer播放器源码分析之音视频输出——视频篇

    一步步实现windows版ijkplayer系列文章之一--Windows10平台编译ffmpeg 4.0.2,生成ffplay 一步步实现windows版ijkplayer系列文章之二--Ijkpl ...

  5. 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入

    使用react全家桶制作博客后台管理系统   前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...

  6. Java日期时间API系列8-----Jdk8中java.time包中的新的日期时间API类的LocalDate源码分析

    目录 0.前言 1.TemporalAccessor源码 2.Temporal源码 3.TemporalAdjuster源码 4.ChronoLocalDate源码 5.LocalDate源码 6.总 ...

  7. es 修改拼音分词器源码实现汉字/拼音/简拼混合搜索时同音字不匹配

    [版权声明]:本文章由danvid发布于http://danvid.cnblogs.com/,如需转载或部分使用请注明出处 在业务中经常会用到拼音匹配查询,大家都会用到拼音分词器,但是拼音分词器匹配的 ...

  8. 【面试必备】硬核!30 张图解 HTTP 常见的面试题

    每日一句英语学习,每天进步一点点: 前言 在面试过程中,HTTP 被提问的概率还是比较高的.小林我搜集了 5 大类 HTTP 面试常问的题目,同时这 5 大类题跟 HTTP 的发展和演变关联性是比较大 ...

  9. Java技术开发专题系列之【Guava RateLimiter】针对于限流器的入门到精通(针对于源码分析介绍)

    Guava包中限流实现分析 RateLimiter 之前的文章中已经介绍了常用的限流算法,而google在Java领域中使用Guava包中的限流工具进行服务限流. 回顾使用案例 Google开源工具包 ...

随机推荐

  1. Mysql主从同步机制

    1.1 主从同步介绍和优点 *在多台数据服务器中,分为主服务器和从服务器.一台主服务器对应多台从服务器. *主服务器只负责写入数据,从服务器只负责同步主服务器的数据,并让外部程序读取数据 *主服务器写 ...

  2. 使用django的用户表进行登录管理

    改写用户基本表 ... AUTH_USER_MODEL = 'appjwt.User' ... setting.py from django.db import models from django. ...

  3. Tomcat 知识点总结

    Tomcat 学习笔记.本文相关配置均为 tomcat8 下,其他版本可能略有不同.如有错误请多包涵. 架构 首先,看一下整个架构图 接下来简单解释一下. Server:服务器.Tomcat 就是一个 ...

  4. BJWC2011 禁忌

    题目链接 题解 多模式匹配首先建 AC 自动机,看到 \(len \le 10^9\) 想到矩阵乘法优化. 朴素 DP 关于分割的最大值,可以贪心,只要走到一个能匹配串的点立刻返回根继续匹配就行,一定 ...

  5. 算法——模拟LRU机制

    运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 . 实现 LRUCache 类: LRUCache(int capacity) 以正整数作为容量 capacity 初始化 ...

  6. Vulnhub实战靶场:ME AND MY GIRLFRIEND: 1

    一.环境搭建 1.官网下载连接:https://www.vulnhub.com/entry/me-and-my-girlfriend-1,409/ 2.下载之后,使用Oracle VM Virtual ...

  7. GaussDB(DWS)磁盘维护:vacuum full执行慢怎么办?

    摘要:在数据库中用于维护数据库磁盘空间的工具是VACUUM,其重要的作用是删除那些已经标示为删除的数据并释放空间. vacuum的功能 回收空间 数据库总是不断地在执行删除,更新等操作.良好的空间管理 ...

  8. undo表空间丢失、损坏

    1.模拟误删undo表空间 rm -rf undotbs01.dbf 2.解决步骤 SQL> shutdown immediateORA-01116: error in opening data ...

  9. IDEA创建maven项目很慢的问题解决方式

    问题现象:刚转IDEA开发,发现创建maven项目,非常慢. 解决方式: 构建maven项目添加参数,要不然非常慢,会卡住-DarchetypeCatalog=internal 自动导入包,codin ...

  10. Web服务器-并发服务器-单进程单线程非堵塞方式(3.4.3)

    @ 目录 1.分析 2.代码 关于作者 1.分析 当socket去监听的时候,是堵塞的状态 通过tcp_sever_socket.setblocking(False)去设置不堵塞 当socket发现没 ...