【《STL源码剖析》提炼总结】 第1节:空间配置器 allocator
文章目录
- 一. 什么是空间配置器
- 二. STL allocator的四个操作: allocate,deallocate,construct,destroy
- `construct()`
- `destroy()`
- 三. SGI STL独有的空间配置器alloc
- 1. 各个版本的allocate
- 2. alloc空间配置器的独特结构
- 双层配置器:
- 一级配置器
- 二级配置器
- 什么是内存池
- 为什么要使用内存池
- 二级配置器的内存池的具体实现
- 3. alloc用于适配STL标准的通用接口simple_alloc
- 4. SGI 容器对于alloc的匹配
- 四. 关于allocate变化的思考
- 五. 内存基本处理工具
- `construct`
- `destroy`
- `uninitialized_fill_n`
- `uninitialized_copy`
- `uninitialized_fill`
- 程序如何进行类型选择
- uninitialized_xxx 系列函数与其对应的高阶函数的区别是什么
一. 什么是空间配置器
负责空间配置与管理。从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template
——《STL源码剖析》
这段话指出了空间配置器的几个特征:
- 主要功能:负责空间的配置与管理,其将空间方面的操作抽象出来,从而使其他组间更好地面向其他方向
- 具体功能:动态空间配置、空间管理、空间释放
- 类型:为一个类模板(class template),实际上,6大组件除了算法组件是函数模板(function template),其他均为类模板。
实际上,因为在使用容器时默认指定了allocate,对大部分使用者来说空间配置器是透明的
二. STL allocator的四个操作: allocate,deallocate,construct,destroy
假如要进行C++的内存配置和释放操作,我们会进行如下操作:
class Foo{ ... }
Foo *pf = new Foo;
delete pf;
这个过程中new
有两个操作:
- 调用
::operator new
配置内存, - 调用
::operator Foo::Foo()
构造对象函数
delete
也有两段操作
- 调用
Foo::~Foo()
将对象析构 - 调用
::operator delete
释放内存
综上,虽然只有new和delete两个操作,但是实际上有4个操作:内存的分配和回收、对象的构造和析构。因此STL allocator将其分为4个部分:
alloc:allocate()
内存配置alloc:deallocate()
内存释放::construct()
对象构造::destroy()
对象析构
关于allocate()和deallocate() 将于后面介绍,这里简单介绍一下构造和析构函数的思想原理
construct()
构造函数其实比较简单,因为已经抽象出了内存配置功能,因此只要实现构造即可。这里直接使用使用了 placement new
一般来说,使用new申请空间时,是从系统的“堆”(heap)中分配空间。申请所得的空间的位置是根据当时的内存的实际使用情况决定的。但是,在某些特殊情况下,可能需要在已分配的特定内存创建对象,这就是所谓的“定位放置new”(placement new)操作。
by placement new机制 - AlexNoBug的文章 - 知乎
destroy()
相比于简单调用placement new的construct()
,destroy()
略有不同,它进行了如下分类
- 假如是对一个指针执行析构函数,则直接调用该对象的析构函数即可
- 假如是对[first,last) 的范围执行析构函数,那需要进行一次判断:假如这个范围内的对象是"trivial"(不重要的),那鉴于循环调用析构函数的开销不小,没有调用析构函数的必要;反之,若为"non-trivial",那说明有调用的必要,因此会遍历调用析构函数
PS: 实际上,对于是否为trivial,有一个很好判断的依据:这个对象是否有用new在堆中创建内存,假如有的话,那必然为non-trivial,因为不执行析构函数会造成内存泄漏,这也是判断是否是trivial的依据——假如对象没有分配额外内存,那等着deallocate进行内存释放即可
三. SGI STL独有的空间配置器alloc
1. 各个版本的allocate
- VC和BC5版本的allocate,本质上就是对new和delete的调用
- GNU 2.9版本的allocate,采用的是其独特的空间配置器alloc (虽然有标准接口allocate,但是其没有调用,这个标准接口的存在纯粹是为了与旧版本的代码适配)
- GNU 4.9时,allocate又变回了对new和delete的简单封装,原有的那个版本被改名为 __poll_alloc (内存池,也是很贴合它的本质)
- 我查看自己的编译器 gcc 11.2.0 (mingw版) 同样容器适配的是标准接口的allocate(面向类型),同时底层也是使用delete和new实现的
2. alloc空间配置器的独特结构
双层配置器:
alloc独特的设计
- 一级空间配置器:直接使用malloc与free来进行空间配置(没有使用new和delete)
- 二级空间配置器:使用内存池技术,对于过小的内存块,使用内存池进行统一管理
一级配置器
一级配置器与其他版本的配置器比较相似,是直接分配一块内存,略有区别的是其使用的是malloc与free来操作内存。
《STL源码剖析》的猜测为: 一为历史因素,二是因为C++并未提供相应与realloc()的内存分配操作
二级配置器
这是其独特的地方,其使用了内存池来进行统一管理
什么是内存池
简单来说,就是申请一块较大的内存,然后使用其他程序来对其分块,每次要使用的话就从中取出一块
为什么要使用内存池
对于每块申请的内存,其都有一块cookie来记录其大小——这就是额外的开销,当内存块太小时,大量的额外开销会带来很大的浪费,因此采用内存池是一个降低开销的好办法——它一次只申请一块较大的内存,额外开销很小。
同时我觉得还有一个方面,就是内存的申请需要一定的时间,而内存池调用空闲内存只要在空闲的小内存块中选择即可,释放内存只需要将其归为空闲内存块即可,这方面的开销较低。
二级配置器的内存池的具体实现
如果内存足够大,超过128B时,就将其移交给一级配置器处理。
内存池中相同大小的内存区块是以链表形式连接的——链表的指针域会有一定的开销,但是将链表的数据域作为分配的内存移交出去的时候,节点的指针域是不需要使用的——而将内存放入链表中时,数据域也没有作用。因此可以使用共用体来处理
union obj{union obj * free_list_link; // 指向下一个空间节点 指针域char client_data[1];// 作为一个数据域的开头
}
这个char client_data[1]很有迷惑性,看起来空间只有1字节那么大,实际上只是指明起点而已,具体大小要具体分析
内存池中有16个相关的链表(free-lists)(因为是一大块静态内存,也可以说是静态链表),对应的区块大小均为8的倍数,即 8,16,24,32,40,…,128 B,因此在进行取区块的时候,程序会对需要的内存数上取整为8的倍数(会带来微不足道的额外内存分配)
- 因此调用二级配置器时,只需要从对应的链表中取出空闲区块(头指针指向的第一个节点),取下之后,将头结点指向其next
- 要归还区块(deallocate)时,使用头插法将区块插入作为链表的第一个节点即可
- 假如对应链表已经没有空闲区块(头指针指向NULL),那需要重新分配内存产生新的节点放入free-lists——使用malloc分配或者从节点内存更大的链表处取,将其初始化的思路也就是我将其称为静态链表的原因——通过这个大区块的地址,第 n 块的地址位为起始地址 +(n-1)*小区块大小 即可推出
3. alloc用于适配STL标准的通用接口simple_alloc
因为alloc不符合STL标准:alloc是面向字节分配的,而STL规定是为class template,需要面向类型
因此对其有一个简单的封装——通过sizeof
来调用alloc
,使配置器的配置单位从bytes转为面向元素的大小
template<class T,class Alloc>
class simple_alloc{public:static T*allocate(size_t n){ return 0==n? 0 : (T*)Alloc::allocate(n*sizeof(T));}static T*allocate(void){ return (T*)Alloc::allocate(n*sizeof(T));}static void deallocate(T *p,size_t n){ if(0!=n) Alloc::deallocate(p,n*sizeof(T));}static void deallocate(T *p){ Alloc::deallocate(p, sizeof(T));}
};
4. SGI 容器对于alloc的匹配
默认配置器为alloc
,在内部会定义对应的simple_alloc
,然后使用simple_alloc
来进行操作
四. 关于allocate变化的思考
侯捷老师在课上和书内均没有提到为什么GNU要改回简单封装的new和delete,我经过查阅资料,现在的malloc本身已经实现了内存池技术,因此allocate就不需要自己来手动操作了。
好像这样一来旧版的alloc就不适用了。其实不然,这个架构依然是有很大的学习价值的,并不会因为现在版本的更新而消退。
五. 内存基本处理工具
STL定义有5个全局函数作用于未初始化的空间上
construct
前面已经介绍过
destroy
前面同样介绍过
uninitialized_fill_n
接受三个参数:
- 迭代器first指向初始化空间的起始处
- n 表示初始化元素的数量
- x表示初值
简单来说就是将[first,first+n)范围内的元素全部赋值为 x
在进行操作的时候需要判断元素是否为POD类型
POD意指Plain Old Data,也就是标量类型或传统的C struct类型,POD的构造函数/析构函数/拷贝赋值函数等必然为trivial类型
因此面对POD类型可以采用最有效率的初值填写(也就是直接赋值)手法,面对non-POD类型采用更保险的做法(非trivial的构造函数可能有额外操作,因此不能用初值填写的方式):
- 面对POD类型,直接调用高阶函数
fill_n
- 面对非POD类型,进行遍历然后逐个调用
construct
函数进行构造
uninitialized_copy
接受三个参数
- first 指向输入端的起始位置
- last 指向输入端的结束位置
- result指向输出端的起始处
简单来说就是将[first,last)的元素拷贝到[result,result+(last-first) 的位置
与上面同样,进行POD类型判断
- POD类型 直接调用高阶函数
copy
- 非POD类型逐个调用
construct
uninitialized_fill
接受三个参数
- first 指向输入端的起始位置
- last 指向输入端的结束位置
- x 表示初值
简单来说就是将[first,last)的元素赋值为x
与上面同样,进行POD类型判断
- POD类型 直接调用高阶函数
fill
- 非POD类型逐个调用
construct
程序如何进行类型选择
入口函数使用类型萃取(type traits)获取其POD类型__type_traits<T1>::is_POD_type
,将结果作为一个参数调用多路选择函数(针对POD萃取结果进行了函数重载)
uninitialized_xxx 系列函数与其对应的高阶函数的区别是什么
其作用于未初始化的空间,调用的是construct,(假如是POD类型则没有区别)
【《STL源码剖析》提炼总结】 第1节:空间配置器 allocator相关推荐
- STL源码剖析 内存基本处理工具 初始化空间的五个函数
初始化空间的五个函数 构造函数 construct() 析构函数 destroy() 剩余三个底层函数 和 高层函数之间的对应关系如下 uninitialized_copy() 对应 copy() ...
- STL源码剖析学习七:stack和queue
STL源码剖析学习七:stack和queue stack是一种先进后出的数据结构,只有一个出口. 允许新增.删除.获取最顶端的元素,没有任何办法可以存取其他元素,不允许有遍历行为. 缺省情况下用deq ...
- 《STL源码剖析》学习-- 1.9-- 可能令你困惑的C++语法1
最近在看侯捷的<STL源码剖析>,虽然感觉自己c++看得比较深一点,还是感觉还多东西不是那么明白,这里将一些细小的东西或者概念记录一下. 有些东西是根据<C++编程思想>理解的 ...
- 《STL源码剖析》学习--6章--_rotate算法分析
最近在看侯捷的<STL源码剖析>,其中有许多不太明白之处,后经分析或查找资料有了些理解,现记录一下. <STL源码剖析>学习--6章--random access ite ...
- 《STL源码剖析》学习--6章--power算法分析
最近在看侯捷的<STL源码剖析>,其中有许多不太明白之处,后经分析或查找资料有了些理解,现记录一下. 6章--power算法分析 书本中的算法如下所示: template <clas ...
- STL源码剖析——P142关于list::sort函数
在list容器中,由于容器自身组织数据的特殊性,所以list提供了自己的排序函数list::sort, 并且实现得相当巧妙,不过<STL源码剖析>的原文中,我有些许疑问,对于该排序算法,侯 ...
- STL源码剖析---红黑树原理详解下
转载请标明出处,原文地址:http://blog.csdn.net/hackbuteer1/article/details/7760584 算法导论书上给出的红黑树的性质如下,跟STL源码 ...
- STL源码剖析面试问题
当vector的内存用完了,它是如何动态扩展内存的?它是怎么释放内存的?用clear可以释放掉内存吗?是不是线程安全的? vector内存用完了,会以当前size大小重新申请2* size的内存,然后 ...
- STL源码剖析学习二:空间配置器(allocator)
STL源码剖析学习二:空间配置器(allocator) 标准接口: vlaue_type pointer const_pointer reference const_reference size_ty ...
最新文章
- 使用tcpwrapper实现访问控制功能
- java 使用new新建一个对象时的操作过程
- 收藏长文|Java 代码精简之道
- 【Leetcode】Palindrome Number
- Element-UI分页组件超详细使用示例
- 在Angular里使用rxjs的异步API - Observable
- JS 获取浏览器信息,给出友情提示,避免部分兼容性问题
- 肿瘤坏死因子(TNF)阻断剂治疗幼年型银屑病关节炎: 有效吗
- 晨哥真有料丨盘点追女生的作死行为!
- 利用opencv中的级联分类器进行人脸检測-opencv学习(1)
- 【激光雷达】激光雷达点云数据的技术流程和点云预处理的方法
- [na]思科产品选型pdf
- Java访问用户名密码验证的url
- vbs返回结果给java_返回vbs脚本
- 5个小众视频素材网站,你知道吗?
- python爬虫爬取糗百成人图片单线程版本
- 【组合数学】多项式定理 ( 多项式定理 | 多项式定理证明 | 多项式定理推论 1 项数是非负整数解个数 | 多项式定理推论 2 每项系数之和 )
- 虚拟机vm介绍及虚拟机常见操作
- 干货!基于深度空间一致性的鲁棒点云配准算法
- 阿里云短信服务Java实现