系列文章
STL 系列 —— 空间配置器(一)
STL 系列 —— 迭代器与 traits 编程(二)
前言
《STL 源码剖析》,《泛型编程与STL》这两本书都是候捷大佬译的,且在 《STL 源码剖析》最后推荐读物中也出现了 《泛型编程与STL》的身影,机缘巧合就找了一下相关资源,快速翻看了一下,发现两本书的内容是如此相辅相承的,所以这里就放在一起说一说。
虽然天下大作,必作于细,不过目前博主的水平有限,还是在浅浅的一层学习中,所以接下来的系列文章不会深入细节中,否则系列文章难免会变成单纯的读书摘要,又臭又长,也会给阅读的人带来不小的负担。如果对相关细节感兴趣的小伙伴还是可以自行阅读原书籍的。
这也是第二遍读《STL 源码剖析》了,第一遍的时候还在校园时期,更多的是在准备招聘,之后工作以后又重读了一遍,初读时抱着厚厚的一本书,对里面当时看着复杂难记的各种变量名,代码片段搞的很容易看了后面就忘了前面了。第二遍看的时候找了一下书中对应的 STL 源代码,结合代码再看书籍,整本书突然变的很薄。所以也建议大家可以结合着来看,书中的内容会好理解很多。下面是我下载的源码地址:
STL 源码下载
本系列文章的核心内容主要涵盖在:
《STL 源码剖析》: 2 - 6 章
《泛型编程与STL》:2 - 5 章
allocator 总览
当我们使用 new 和 delete 对内存进行操作时,一般 new 包含了两个阶段;
- 使用 ::operator new 配置内存
- 构造对象
使用 delete 的时候也是两个阶段:
- 析构对象
- 使用 ::operator::delete 释放内存
为了精密分工,STL 将这两阶段的操作分开来了,分布在两个头文件中。
对象构造和析构
可能读者会觉得,构造和析构对象不就是直接调用相关的构造函数和析构函数就可以了,还有什么值得说的地方嘛? STL 作为标准库可处处都要精打细算,为了效率做考虑,这部分主要是两点:
- 构造函数:正常调用输入对象的构造函数
- 析构函数:有两个版本,一个版本只接受一个指针,这个直接调用就可以。还有一个版本是接受两个迭代器,需要析构一个范围内的对象,这时需要判断当前类型的析构是不是 trivial 的,若是,析构可以什么都不做直接返回,若不是,才循环访问,依次遍历析构。
具体析构函数的部分源码,结合 __type_traits 的技巧,在 STL 系列 —— 迭代器与 traits 编程(二) 中还会具体提到。
内存分配和释放
考虑到可能造成的内存碎片的问题,STL 设计了两层配置器。
第一级内存配置器
第一级内存配置器直接使用 malloc()
和 free()
函数,其是用来申请内存空间大于 128 bytes 的,当前内存无法满足申请需求时,C++支持用户自己设定情况如何处理的函数接口,如果用户指定了内存不足如何处理,系统会不断尝试释放、配置、再释放、再配置的过程,否则就会直接抛出 bad_alloc 的异常信息。
第二级内存配置器
第二级内存配置器则根据情况来采用内存分配策略。其通过 free-list 及对应内存池的管理方式来对空间加以利用。概述的说,free-list 是一个长 16,即 range(8, 128, 8) 一个数组,其会把需要申请的内存映射到对应的 8 的整倍数。
- 当我们默认申请一块 20 bytes 区域时,标准库会将它对齐到 24 bytes 的区域(#2),程序默认会分配 40 个 24 bytes 即 960 bytes 作为内存池,并且分配出来 20 个 24 bytes大小的区域并将它们依次相连,给到
free-list[2]
去管理这 480 bytes,剩余的 480 bytes 备用,之后若再有相同的需求,直接从free-list[2]
里面拨出就行了。 - 这时我们继续申请一块 64 bytes 的空间,接下来程序查到目前内存池仅够 480 / 64 = 7 个,就会将最后的这 480 bytes 划分成 7 个 64 bytes 大小的区域并依次连接起来,将第一个地址返回给
free-list[7]
管理。 - 如果我们还需要再申请一块 96 bytes 的空间,程序查到内存池已经没有内存可以用了,会再申请 40 + n (附加量) 个 96 bytes 大小的区块作为内存池大小,将 20 * 96 = 1920 大小的区域划分成 20 个连接的 96 bytes 区域,将第一个地址返回,通过
free-list[11]
来管理,剩余的内存留作下次申请时分配。
回收区块则找到对应 free-list
的 index 调整指针位置,将内存块再次放回 free-list
可用区域即可。
内存基本处理工具
这部分主要是分配内存以后可能涉及到的相关数据的拷贝(copy)和填充(fill), STL 也是出于效率考虑,会对输入迭代器进行 type_traits,判定其是不是 POD(Plain Old Data),这种类型必然拥有 trivial 的一系列构造函数和析构函数,这时程序就可以找到针对当前对象最有效率的拷贝/填充方式。
形如以下:
template <class InputIterator, class ForwardIterator, class T>
inline ForwardIterator
__uninitialized_copy(InputIterator first, InputIterator last,
ForwardIterator result, T*) {
// 同样利用萃取器来判断是否是 POD 类型
typedef typename __type_traits<T>::is_POD_type is_POD;
return __uninitialized_copy_aux(first, last, result, is_POD());
}
填充也是类似,这里直接粘贴部分代码,当是 POD 类型,即有 trivial 的构造/析构函数,可以直接调用有效的 STL fill() 函数,否则只能乖乖一个一个构造。
template <class ForwardIterator, class T>
inline void
__uninitialized_fill_aux(ForwardIterator first, ForwardIterator last,
const T& x, __true_type)
{
fill(first, last, x); // 调用 STL 的算法
}
template <class ForwardIterator, class T>
void
__uninitialized_fill_aux(ForwardIterator first, ForwardIterator last,
const T& x, __false_type)
{
ForwardIterator cur = first;
__STL_TRY {
for ( ; cur != last; ++cur)
construct(&*cur, x); // 必须一个一个元素的构造
}
__STL_UNWIND(destroy(first, cur));
}
template <class ForwardIterator, class T, class T1>
inline void __uninitialized_fill(ForwardIterator first, ForwardIterator last,
const T& x, T1*) {
typedef typename __type_traits<T1>::is_POD_type is_POD;
__uninitialized_fill_aux(first, last, x, is_POD());
}
后记
本文主要介绍了空间配置器分为两级,为什么分为两级?因为为了空间更加有效的利用,包括每一级分配内存时的策略。还有在构建和析构(同样也涉及到对象的拷贝和填充相关函数时)时,对象是否有 trivial 的构造/析构函数对效率是有影响的,STL 对这些情况有写了对应的函数做了处理。
本文相关源代码主要在:
std_alloca.h
std_construct.h
std_uninitialized.h
三个头文件中。