STL_Allocator
¶1. 文件结构
Allocator 文件结构以及关键函数如上图所示,分为了两个部分,simpleAlloc,subAllocation。
¶1.1 simpleAlloc
内部包含 1 个头文件:simpleAlloc.h
¶1.1.1 simpleAlloc.h
simpleAlloc.h
这是一个简单的空间配置器的例子,实现了部分的接口和功能。
内部调用 ::operator new、 ::operator delete
《STL源码剖析》 – P44 - P47
¶1.2 subAllocation
内部包含 5 个头文件:
allocator.h
memory.h
alloc.h
construct.h
uninitialized.h
¶1.2.1 allocator.h
与 simpleAlloc.h
相类似。它由 SGI 定义的一个符合部分标准,名为 allocator 的配置器,但是 SGI 从未使用过它,也不建议我们使用。主要的原因是效率不佳,只是把 C++ 的 ::operator new 和 ::operator delete 做一层薄薄的包装而已。
《STL源码剖析》 – P48 - P49
但是重要的是 SGI STL 把一级二级配置器封装在 simple_alloc 接口中,所有的 SGI STL 容器都是用这个接口。这个接口定义在 allocator.h
文件中。一二级配置器的关系,接口包装,实际运用方式见下图。
《STL源码剖析》 – P53 - P56
¶1.2.2 memory.h
内部包含了 3 个头文件:
1 |
alloc.h 定义了一、二级配置器,彼此合作。配置器名为 alloc。利用 allocate()
分配内存,利用 deallocate()
释放内存。
construct.h 定义了全局函数 construct()
和 destroy()
,负责对象的构造和析构。
uninitialized.h 定义了一些全局函数,用来填充或复制大块内存数据,这些函数虽然不属于配置器的范畴,但与对象初值设置有关。对于容器的大规模元素初值设置很有帮助。这些函数对于效率都有面面俱到的考虑,最差情况下会调用 construct()
,最佳情况则会使用 C 标准函数 memmove()
直接进行内存数据的移动。
《STL源码剖析》 – P50
¶2. 关键代码
¶2.1 construct.h
¶2.1.1 头文件
1 |
《STL源码剖析》 – P51
¶2.1.2 构造
构造函数只有一种,可以使用 placement new
1 | template <class T1, class T2> |
《STL源码剖析》 – P51
¶2.1.3 析构
析构函数有四个重载函数,版本一就是简单调用对象的析构函数,如下:
1 | template <class T> |
版本二接受两个迭代器,并根据不同的元素型别选择调用不同的接口函数。destroy()
内部调用不同型别对应的接口函数 _destroy_aux()
。
1 | // 设法利用traits批量析构对象 |
版本三、四是对应 char* 和 wchar_t* 的特化版本。
1 | // 因为 char* 和 wchar_t* 归于 POD 类型,属于 trivial 类型,--> _true_type |
可以参考 C++ trivial、non-trivial及POD类型
“typeTraits.h” 头文件中有详细的分类
《STL源码剖析》 – P51
¶2.2 alloc.h
alloc.h
为 sub-Allocation 的主要实现,sub-Allocation 理解为具有次配置的空间配置器,分为两级配置。
- 一级配置器 _malloc_alloc,分配内存 $> 128\ bytes$
- 二级配置器 _default_alloc,分配内存 $\leq 128\ bytes$
《STL源码剖析》 – P53 - P56
¶2.2.1 头文件
1 |
《STL源码剖析》 – P56
¶2.2.2 一级配置器
一级配置器,主要有两个主要功能:
- 分配和释放内存
- 处理内存不足的状况
¶2.2.2.1 分配和释放内存
allocate()
直接使用 malloc()
;
1 | static void *allocate(size_t n) { |
deallocate()
直接使用 free()
;
1 | // deallocate 释放内存,内部调用 free |
reallocate()
直接使用 reallocate()
;
1 | // reallocate 修改分配内存,内部调用 realloc |
可以发现,三个函数皆是直接使用库函数来实现,但是进行了内存分配失败的处理 oom_malloc(n)
,oom_realloc(n)
。
《STL源码剖析》 – P57
¶2.2.2.2 处理内存不足的状况
1 | using malloc_handler = void (*)(); |
为了模拟 set_new_handler 行为,需要写一个类似标准版的set_malloc_handler()
1 | // 模拟 C++ 的 set_new_handler(), 可以通过它来指定自己的 out-of-memory handler |
1 | // __malloc_alloc_oom_handler的类外定义,函数指针为 nullptr |
《Effective C++ 3rd》 – 条款49:了解 new-handler 的行为 (P243)
《STL源码剖析》 – P57
oom_malloc()
用来自定义处理 malloc 分配失败的情况。
1 | void *__malloc_alloc::oom_malloc(size_t n) { |
oom_realloc()
用来自定义处理 remalloc 分配失败的情况。与上文代码相同,仅仅 行: 8
不同。
1 | void *__malloc_alloc::oom_realloc(void *p, size_t n) { |
上述的处理函数步骤如下:
用户调用
set_malloc_handler()
自定义自己的处理函数,如果未定义,_malloc_alloc_oom_handler 被默认定义为 nullptr,是一个空指针。如果出现内存分配失败情况,进入相应的处理函数,
oom_malloc()
或者oom_realloc()
- 首先定义处理函数 new_alloc_handler 并将 _malloc_alloc_oom_handler 赋给它;
- 如果用户未定义相应的处理函数,直接抛出异常即可;否则调用用户定义的处理函数,试图解决内存不足的情况;
- 再次重新调用
malloc()
分配内存;
注意:
设计和设定“内存不足处理函数”是客端的责任。
其中
for (;;)
的使用可以使得处理函数不断尝试处理内存不足情况。
《STL源码剖析》 – P58 - P59
《Effective C++ 3rd》 – 条款51:编写 new 和 delete 时需固守的常规 (P253)
¶2.2.3 二级配置器
二级配置器多了一些机制,避免太多小额外区块造成内存的碎片。小额区块带来的其实不仅时内存碎片,配置时的额外负担也是一个大问题。额外负担永远无法避免,毕竟系统要靠这多出来的空间来管理内存,如下图所示。但是块区越小,额外负担所占的比例就越大,越显得浪费。
SGI 第二级配置器的做法是,如果区块够大,超过 128 $bytes$ 时,就移交给第一级配置器处理。当区块小于 128 $bytes$ 时,则以内存池(memory pool)管理,此法又称为次层配置(sub-allocation):每次配置一大块内存,并维护对应之自由链表(free-list),下次若再有相同大小的内存需求,就直接从 free-list 中拔出。如果客端释放小额区块,就由配置器收回到 free-list 中。
¶2.2.3.1 自由链表
free-list 节点结构如下,实现技巧如下图 4 所示。
1 | // free_list节点 |
为了方便管理,SGI 二级配置器会主动使用 ROUND_UP()
函数将任何小额区块的内存需求量上调至 8 的倍数,并维护 16 个 free-list 。从 ${8,16,…128}$。使用 FREELIST_INDEX()
函数确定区块编号。
1 | // freelist参数设定 |
关于 _freelist_setting 的相关操作
1 | // 将bytes上调至8的倍数 |
《STL源码剖析》 – P59 - P61
《Effective C++ 3rd》 – 条款2:尽量用 const,enum,inline 替代 #define (P15)
¶2.2.3.2 空间配置函数 allocate()
空间配置函数为 allocate()
,如果需要分配的内存大于 128 则采用一级配置器,否则从自由链表中拔出内存使用,如果自由链表没有没存就需要调用 refill()
来填充自由链表。
1 | void *__default_alloc::allocate(size_t n) { /*n > 0*/ |
《STL源码剖析》 – P62 - P63
¶2.2.3.3 空间释放函数 deallocate()
同样需要判断区块大小,大于128 字节,使用一级配置器,小于128 字节就找到对应的 free-list,将区块回收。
分为四个步骤
- 强制类型转换,将 p 转化为 obj* 节点类型 q ,使用 reinterpret_cast 将 void* -> obj*
- 根据内存的大小确定区块,调用
FREELIST_INDEX()
- 将 q 的 next 区块指向当前 my_free_list 指向的内存
- 更新 my_free_list 为 q
1 | void __default_alloc::deallocate(void *p, size_t n) { |
《STL源码剖析》 – P64 - P65
¶2.2.3.4 空间重配置函数 reallocate()
根据新旧内存需求的大小关系可以分为五种情况,分别对应这三种处理方式。
- ① old_sz > 128, new_sz > 128 ,直接使用
realloc()
; - ②
ROUND_UP(old_sz)
==ROUND_UP(new_sz)
<= 128:直接返回原内存指针即可; ROUND_UP(old_sz)
!=ROUND_UP(new_sz)
,统一调用allocate()
函数,只是内部根据 new_sz 的大小使用不同的配置器。- ③ old_sz > 128, new_sz <= 128 :则会使用二级配置器;
- ④ old_sz <= 128, new_sz > 128 :则会使用一级配置器;
- ⑤ old_sz <= 128, new_sz <= 128 :则会使用二级配置器;
注意:情况 ① 中包含了 ROUND_UP(old_sz)
== ROUND_UP(new_sz)
> 128
1 | void *__default_alloc::reallocate(void *p, size_t old_sz, size_t new_sz) { |
MiniSTL / Allocator / subAllocation / alloc.h :
reallocate()
内存池技术(三) 最后
mem_realloc()
讲解《STL源码剖析》 – P62
¶2.2.3.5 重新填充 refill()
refill()
的工作就是,从内存池中获取内存。主要通过 chunk_alloc()
函数来获取内存。默认获取 20 个区块内存,由于内存不足,可能会小于这个数字,会根据获得内存的不同进行不同的处理。
1 | // 当free_list无可用区块时,重新填充空间 |
《STL源码剖析》 – P65 - P66
¶2.2.3.6 内存池
从内存池中取空间给 free-list 使用,是 chunk_alloc()
的工作。在 chunk_alloc()
之前需要明确内存池的结构。内存池的结构定义如下,图示如图 7 所示。
1 | // chunk allocation state |
chunk_alloc()
是内存池的关键函数。
1 | char *__default_alloc::chunk_alloc(size_t size, int &nobjs) { // size为 8 的整数倍 |
根据内存池的剩余空间与客端需求的关系,有三个处理路径:
- 所需要分配的内存足够
- 容量至少满足一个区块的需求
- 内存池一个区块都无法满足提供
1. 所需要分配的内存足够
1 | // 1. 容量满足需求 |
2. 容量至少满足一个区块的需求
1 | // 2. 容量至少满足一个区块需求 |
3. 内存池一个区块都无法满足提供
行:5
此时扩大需求量,新需求量是原需求量的二倍与现有内存池大小的十六分之一之和。
行:7-12
并做碎片资源回收操作,将内存池中碎片资源加入到对应的自由链表中。
行:14
利用 malloc 函数向 heap 中申请所需内存
由于 malloc 的使用,会存在两种情况
- heap 空间充足
- heap 空间不够
前者 直接更新部分参数,并递归调用自身以修正 nobjs,最终返回所需内存,结束调用。(递归操作必定会进入if_else 语句中,下同)
后者
- 会在大于 size 的自由链表中,尝试分配所需的区块,如果成功找到,则回收至内存池,并递归调用自身以修正 nobjs,最终返回所需内存,结束调用。
- 如果自由链表中也没有内存,则会调用一级配置器,尝试调用一级配置器观察其能否分配内存,或抛出异常。
1 | // 3. 内存池一个区块都无法提供 |
《STL源码剖析》 – P66 - P69
¶2.3 内存基本处理工具 uninitialized.h
¶2.3.1 头文件
1 |
关键的三个函数:
uninitialized_copy()
uninitialized_fill()
uninitialized_fill_n()
¶2.3.2 uninitialized_copy()
该函数有三个重载版本,分别是一个泛化版本和两个特化版本
两个特化版本:直接使用 memmove()
移动内存
1 | //针对char*、wchar_t*存在特化版本 memmove直接移动内存 |
泛化版本:需要使用萃取机获得型别,后通过不同的型别判断使用何种接口函数。(又是两个特化)
型别:_true_type, _false_type
1 | template <class InputIterator, class ForwardIterator> |
接口函数:__uninitialized_copy_aux()
1 | // _true_type 说明 trivial 类型,直接调用 STL 中 copy 函数即可 |
注意:(下同)
is_POD_type 存在两种状态,_true_type, false_type,由萃取机获得。
_true_type 说明 trivial
如果这个类都是 trivial ctor / dtor / copy / assignment 函数
我们对这个类进行构造、析构、拷贝和赋值时可以采用最有效率的方法,不调用无所事事正真的那些 ctor / dtor等,而直接采用内存操作如 *malloc()、memcpy(), memmove()*等提高性能。这也是SGI STL内部干的事情。
_false_type 也就是 non trivial
那么上面的四种函数有(ctor / dtor / copy / assignment) 是 non - trivial 函数,比如 non-trivial ctor、non - trivial copy ,也就是说有意义的函数,里面有一下必要的操作,比如类成员的初始化,释放内存等。这时候只能调用对应的 ctor,copy…
可以参考 C++ trivial、non-trivial及POD类型
“typeTraits.h” 头文件中有详细的分类
《STL源码剖析》 – P70, P73 - P75
¶2.3.3 uninitialized_fill()
该函数是一个泛化版本,但是利用萃取机又可以分为两个特化的接口函数。
泛化版本:需要使用萃取机获得型别,后通过不同的型别判断使用何种接口函数。(两个特化)
型别:_true_type, _false_type
1 | template <class ForwardIterator, class T> |
接口函数: __uninitialized_fill_aux()
1 | // _true_type 说明 trivial 类型,直接调用 STL 中 fill 函数即可 |
《STL源码剖析》 – P71, P75 - P76
¶2.3.4 uninitialized_fill_n()
该函数是一个泛化版本,但是利用萃取机又可以分为两个特化的接口函数。
泛化版本:需要使用萃取机获得型别,后通过不同的型别判断使用何种接口函数。(两个特化)
型别:_true_type, _false_type
1 | template <class ForwardIterator, class Size, class T> |
接口函数: __uninitialized_fill_n_aux()
1 | // _true_type 说明 trivial 类型,直接调用 STL 中 fill 函数即可 |
《STL源码剖析》 – P71 - P73
¶2.3.5 SGI_STL的扩展
SGI_STL的扩展:
uninitializd_copy_copy()
uninitialized_copy_fill()
uninitialized_fill_copy()
uninitializd_copy_copy()
1 | // __uninitialized_copy_copy |
uninitialized_copy_fill()
1 | // uninitialized_fill_copy |
uninitialized_fill_copy()
1 | // __uninitialized_copy_fill |
MiniSTL / Allocator / subAllocation / alloc.h :
uninitializd_copy_copy()
MiniSTL / Allocator / subAllocation / alloc.h :
uninitializd_fill_copy()
MiniSTL / Allocator / subAllocation / alloc.h :
uninitializd_copy_fill()
¶3. 参考资料
《STL源码剖析》
《Effective C++ 3rd》
内存池技术(三) 最后
mem_realloc()
讲解MiniSTL / Allocator 部分