标准的空间配置器:allocator
SGI也定义有一个符合部分标准,名为allocator的配置器,但是从未使用过它,也不建议使用,原因是效率不佳,只是对new,delete的一层简单包装。
特殊的空间配置器:alloc
先考虑一般而言,我们习惯的C++申请内存和释放内存的流程,
class Foo{ ... };
Foo* pf = new Foo;
delete pf;
new包含了两部分操作:申请空间,构造对象内容,delete也包含了两部分操作:析构,释放内存。
为了精密分工,allocator决定将这两阶段区分开来,
- 内存配置操作由alloc:allocate负责,内存释放操作由alloc:deallocate负责,
- 对象构造由::construct负责,对象析构由::destroy负责。
这两部分内容被定义在 < memory >中,其中包含的两个头文件,stl_alloc.h和stl_construct.h有上述内容。从属关系如下所示:
对象构造和析构行为
construct()接收一个指针p和一个初值value,作用是把初值设定到指针所指的空间上。
destroy()有两个版本,一种支持指针(指定析构),一种支持迭代器(范围析构)。
空间配置和释放行为
设计哲学:
- 向system heap要求空间
- 考虑多线程情况
- 考虑内存不足的应变措施
- 考虑过多“小型区块”可能造成的内存碎片问题
针对上述设计哲学,
- 空间上,SGI采用的是malloc和free来完成内存的配置和释放
- 内存碎片上,采用的是双级配置器
对于双级配置器的表述,
- 第一级直接使用malloc和free (其实加装了一层外壳,使其符合规格)
- 第二级视情况采取不同策略:
- 配置区块超过128bytes,视为足够大,用一级
- 配置区块小于128bytes,视之过小,用memory pool整理方式(二级)
一二级配置器的关系图示如下:
针对上图中的new handler机制,是在内存配置需求无法满足时可以调用一个你指定的函数(在调用bad alloc之前)。
配置器
一级配置器剖析:
为什么不使用new?
- 可能是历史因素
- c++未提供realloc的内存配置操作,导致不能直接使用set_new_handler,需要仿真一个。
二级配置器剖析:
前面提到了二级配置器是为了防止内存碎片问题,不仅是碎片问题,配置时的额外负担也是问题(系统在管理空间是需要缓存空间来记录内存大小)。二级配置器的做法是:
- 足够大就交给一级
- 小于128bytes用内存池管理(次级配置)
次级配置器的具体做法:
- 每次配置一大块内存,并维护对应的自由链表,下次再有相同大小需求直接从链表中拨出
- 如果是释还空间,就由配置器回收到自由链表中
- 为了方便管理,任何小额区块的内存需求量上调至8的倍数,维护的是16个自由链表,8-128
配置器与函数的对应及逻辑:
申请过程allocate
- 判断区块大小决定调用几级配置器。
- 在二级中如果有可用就直接拨出,没有就将区块大小上调至8的倍数边界进行refill
释放过程deallocate
- 判断区块大小决定调用几级配置器。
- 大于128一级回收,小于二级找对应的free list回收
重新填充过程refill
- free list没有可用时调用,去内存池取,缺省取20个,内存池不够就小于20
- 大于128一级回收,小于二级找对应的free list回收
内存池申请过程chunk_alloc
- 水量充足取20,不足少于20
- 连一个区块空间都不够就去malloc,新水量为需求量的两倍,再加上一个随配置次数增加的附加量
- 如果system heap空间也不够了,就去free list四处寻找并挖出,再找不到就去一级配置器
- 或许一级配置器有机会,计算没有机会还可以调用out-of-memory处理机制
内存池的实际操作过程如下所示:
内存基本处理工具
STL定义了五个全局函数,作用于未初始化空间上,分别是construct(),destroy(),uninitialized_copy(),uninitialized_fill()和uninitialized_filln()。这部分没看太明白,整理的比较简略。
uninitialized_copy()
将内存的配置和对象的构造行为分离,目前能记住的致使这个函数对实现一个容器有帮助,因为容器的全区间构造函数通常需要下述两个步骤完成:
- 配置内存区块,足以包含范围内的所有元素
- 使用该函数在该内存区块上构造元素
uninitialized_fill()
也能将内存的配置和对象的构造行为分离,
uninitialized_filln()
也能将内存的配置和对象的构造行为分离,会为指定范围内的所有元素设定相同的初值。