废话不多说,读侯捷的SGI STL源码分析目的有三个:
1,接触c++不久就开始跟STL打交道,一直有个好奇心,这么强大的库到底是谁、咋实现的?;
2,不熟悉实现就用不好STL,所以想更好的应用STL,就有必要一探其底层驱动;
3,引用林语堂先生的一句话:“只用一样东西,不明白它的道理,实在不高明”;
目录
1,如何使用空间适配器
2,一个标准的空间配置器
3,SGI STL 空间配置器架构
4,构造和析构的基本工具:construct()和destroy()
5,空间的配置与释放,alloc
5.1 第一级配置器 __malloc_alloc_template
5.2 第二级配置器 __default_alloc_template
5.2.1 freelist
5.2.2 二级配置器中的空间配置函数allocator()
5.2.3 空间释放函数deallocate()
5.2.4 内存池(mem pool)
5.2.5 第二级配置器总的流程框图
6 内存基本处理工具
7 小结
其实以运用STL的角度来看,完全可以忽略空间适配器,因为每个容器都是通过默认参数指定好了allocator,通过查看vector的声明可以看出:
template >
class vector
{
//...
}
如下代码中的vector没有指定allocator,默认的allocator会自动根据你传入的元素,调整内存空间:
#include
void main()
{
std::vector vecTemp;
for (int i = 0;i<10;i++)
{
vecTemp.push_back(i);
}
getchar();
}
其实,完整的vecTemp声明应该是 vetor
假如我们自定义了将内存分配指向磁盘或者其他存储介质空间的allocator,那么只要在声明时传入设计好的allocator,不再使用默认的allocator就行了。
那么问题来了,怎么样才能设计一个allocator呢?继续看~
首先,设计一个空间配置器需要包含什么接口呢?我们从如下的例子引入:
class Foo{...};
Foo* pFoo = new Foo;//< 第一阶段,干了俩事:1,配置内存 2,在配置好的内存上构造对象
delete pFoo; //< 第二阶段,也干了俩事:1,析构对象 2,释放内存
所以,一个allocator至少要包含四个功能:申请内存、构造对象、析构对象、释放内存。
其次,我可以很负责人的告诉你,如果你的allocator只包含上述四个功能,肯定无法再STL中运用^_^。因为STL对allocator的组成已经规定好了,即STL规范。那么STL中的allocator相关的规范是啥呢?我们通过一个符合STL标准的allocator(主要参考书中的JJ::allocator,略有修改)来说明:
namespace JJ
{
template
class allocator
{
public:
//< 七个typedef主要是为了迭代器的类型萃取,迭代器章节会提到
typedef T value_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
//< 成员模板 rebind
//< 定义了一个associated type other,other也是一个allocator的实例,但是负责管理的对象类型与T不同
//< 具体可以参考https://blog.csdn.net/qq100440110/article/details/50198789
template
struct rebind
{
typedef allocator other;
};
//内存申请 直接使用new
pointer allocate(size_type n, const void* hint = 0)
{
T* tmp = (T*)(::operator new((size_t)(n * sizeof(T))));
if (tmp == 0)
cerr << "out of memory" << endl;
return tmp;
}
//构造函数 使用placement_new 在p处构造T1
void construct(pointer p, const T& value)
{
new(p) T1(value);
}
//析构函数
void destroy(pointer p)
{
p->~T();
}
//释放内存 直接使用delete
void deallocate(pointer p)
{
::operator delete(p);
}
//取地址
pointer address(reference x)
{
return (pointer)&x;
}
//返回const对象的地址
const_pointer const_address(const_reference x)
{
return (const_pointer)&x;
}
//可成功配置的最大量
size_type max_size() const
{
return size_type(UINT_MAX / sizeof(T));
}
};
}// NAMESPACE_JJ_END
这样,我们设计的第一个allocator完成了,就可以在实际中使用了:
int ia[5] = { 1,2,3,4,5 };
vector > vec(ia, ia + 5);
有人可能会想,既然设计一个空间配置器这么简单,STL的多个毛啊,为啥它的这么NB。其实,STL的空间配置器不只多个毛,是多很多毛,不是NB,而是很NB,从这就能看出来王者与青铜的差别了,膜拜之~
由于一个内存配置与释放操作通常分两个阶段(见2中的例子),为了精密分工,STL allocator将这两个阶段的操作区分开来:
1,内存配置由alloc::allocate()负责,内存释放由alloc::deallocate()负责;
2,对象构造由::construct()负责,对象析构由::destroy()负责。
其实,对于内存配置和释放还有个allocator::allocate()和allocator::deallocate(),这是SGI定义的符合部分STL标准的配置器,但由于效率不佳,不推荐使用。其实它就是对::operator new和::operator delete做了一层薄薄的封装。
思维导图如下:
书中原图:
毋庸置疑,
先上一张书中的construt()和destroy()示意图,对照着图就很容易理解了:
首先,对于construct()来说很简单了,就是接受一个指针p和一个初值value,用途就是将初值设定到指针所致的空间上,可以通过placement new来完成。
template
void construct(T1 *p, const T2 &value)
{
new(p) T1(value);
}
其次,从图中可以看出destroy()有两个版本:
第一个版本:接受一个指针(图中的第四个),准备将所指之物析构掉。这很简单,直接调用析构函数即可。
template
void destroy(T *ptr)
{
ptr->~T();
}
第二个版本:接受一个迭代器区间,准备将这个范围内的对象析构掉。
再讲这个版本的destroy()之前,讲一下trivial destructor:如果不定义析构函数,而是使用系统自带的,也就是析构函数没什么作用,那么这个析构函数称为trivial destructor。
这里提现了STL作者的设计亮点,他不是直接调用每个对象的析构,而是首先确定每个对象是否有non_rivial destructor(即自己定义了析构函数)。如果有,则调用对象析构,如果没有,就什么也不做结束。
反正思路就是上面写的,具体可以看一下书上的代码,至于每个对象是否有non_rivial destructor的判断,则用到了_type_traits
图中的第二个和第三个是第二个版本的char*和wchar*的特化。
以上就是关于construt()和destroy()的所有内容,其实还是挺简单的。
这一节我觉得是整个SGI STL空间配置器的核心。
设计者设计了两个配置器,准确的说是两级配置器,两个配置器相辅相成,相互配合最终完成空间的配置。
第一级配置器直接使用malloc()和free(),第二级则视情况采取不同的策略。而分界点是配置的内存是否大于128B,大于就用第一级,小于等于则通过第二级访问复杂的memory pool整理方式。
通过是否定义_USE_MALLOC宏,来设定是只打开第一级还是同时打开第一级与第二级。SGI STL没定义那个宏,也就是同时开放一、二级。
先说一下整体的思路。就像上图所说的,这个配置器中的allocator()直接调用C中的malloc(),reallocator()直接调用C中的realloc()。如果配置成功,则返回指针,如果不成功则调用out of memory处理;deallocator()直接调用free()。
out of memory主要调用用户设置的__malloc_alloc_oom_handler,这个可以通过模拟C++中的set_new_handler()的set_malloc_handler()来设定。如果用户指定了,则循环调用这个handler,直到分配到内存,如果没定义,则抛bad_alloc异常。
具体代码如下:
#if 0
#include
#define __THROW_BAD_ALLOC throw bad_alloc
#elif !defined(__THROW_BAD_ALLOC)
//#include
#define __THROW_BAD_ALLOC cout<<"Out Of Memory."<
class __malloc_alloc_template
{
private:
//以下用来处理内存不足的情况;oom:out of memory
static void * oom_malloc(size_t n);
static void * oom_realloc(void *p, size_t n);
static void(*__malloc_alloc_oom_handler)();
public:
static void* allocate(size_t n)
{
void *result = malloc(n); //< 直接调用malloc()
if (result == 0)
result = oom_malloc(n); //< 分配失败调用oom_malloc()
return result;
}
static void deallocate(void *p, size_t)
{
free(p); //< 直接调用free()
}
static void* reallocate(void *p, size_t old_sz, size_t new_sz)
{
void *result = realloc(p, new_sz); //< 直接调用C中的realloc()
if (0 == result)
result = oom_realloc(p, new_sz); //< 分配失败调用oom_realloc
return result;
}
//模拟C++中的set_new_handler(),也就是通过这个函数指针来指定自己的out-of-memory操作
static void(* set_malloc_handler(void(*f)()))()
};
// 初值为0,客户端指定
template
void(*__malloc_alloc_template::__malloc_alloc_oom_handler)() = 0;
//如果指定了 __malloc_alloc_oom_handler,则循环调用,直到分配到内存,否则抛异常
template
void* __malloc_alloc_template::oom_malloc(size_t n)
{
void(*my_malloc_handler)();
void *result;
for (;;)
{
my_malloc_handler = __malloc_alloc_oom_handler;
if (0 == my_malloc_handler)
{
__THROW_BAD_ALLOC;
}
(*my_malloc_handler)();
result = malloc(n);
if (result)
return result;
}
}
//如果指定了 __malloc_alloc_oom_handler,则循环调用,直到分配到内存,否则抛异常
template
void* __malloc_alloc_template::oom_realloc(void *p, size_t n)
{
void(*my_malloc_handler)();
void *result;
for (;;)
{
my_malloc_handler = __malloc_alloc_oom_handler;
if (0 == my_malloc_handler)
{
__THROW_BAD_ALLOC;
}
(*my_malloc_handler)();
result = realloc(p, n);
if (result)
return result;
}
}
所谓的C++ new handler机制是指,你可以要求系统在内存配置需求无法被满足时,调用一个你指定的函数。之所以要模拟这种机制,因为它并不是使用::operator new来配置内存的。
其实第一级配置器可以说用户是通过new和free直接与系统内存打交道的,而第二级配置器相对比较复杂,大概分为3块内存,简要的沟通机制可参考下图:
三块空间分别为freelist、mempool、系统内存。
各实现的伪代码可以参考博客:https://blog.csdn.net/qq973177663/article/details/50815055?locationNum=9
总是通过freelist来获得内存,freelist如果没有内存了,则调用refill()向mempoor获得内存,mempoor如果也不够,则调用trunk_alloc()向内存申请,内存都没有调用第一级配置器,看看out of memory机制能够起作用。
整个第二级配置器无非就是对上述freelist空间、mempoor空间的创建、内存申请、内存回收、以及之间的通信。源码如下:
enum { __ALIGN = 8 }; //< 小型区块的上调边界
enum { __MAX_BYTES = 128 }; //< 小型区块的上限
enum { __NFREELISTS = __MAX_BYTES / __ALIGN }; //< freelist个数:16个
template
class __default_alloc_template
{
public:
//< 三个接口
static void *allocate(size_t n);
static void deallocate(void *p, size_t n);
static void* reallocate(void *p, size_t old_sz, size_t new_sz);
private:
// 将申请的size上调至__ALIGN的倍数
static size_t ROUND_UP(size_t bytes)/
{
return (bytes + __ALIGN - 1) & ~(__ALIGN - 1);//
}
// freelist节点结构
union obj
{
union obj * free_list_link;
char client_data[1];
};
// 根据要申请的区块大小,决定使用第n号freelist,n从0算起
static size_t FREELIST_INDEX(size_t bytes)
{
return (bytes + __ALIGN - 1) / __ALIGN - 1;
}
// 返回一个大小为n的区块对象,并可能(通常)加入大小为n的其他区块到freelist
static void* refill(size_t n);
// 配置一大块空间,可容纳nobjs个大小为size的区块
// 注意此处nobjs是引用,如果配置有所不便(内存不够),nobjs会降低
static char* chunk_alloc(size_t size, int &nobjs);
static obj * free_list[__NFREELISTS]; //< 16个freelist
static char *start_free;//< 内存池其实位置
static char *end_free; //< 内存池结束位置
static size_t heap_size; //< 配置内存的附加量
};
// 赋初值
template
char* __default_alloc_template::start_free = 0;
template
char* __default_alloc_template::end_free = 0;
template
size_t __default_alloc_template::heap_size = 0;
template
__default_alloc_template::obj*
__default_alloc_template::free_list[__NFREELISTS] =
{ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };
简单说,就是16个8byte倍数但小于128byte的链表,链表的节点如下:
union obj
{
union obj* free_list_link;
char client_data[1];
};
其实我对这个节点的定义还是有点疑问的,具体请看我的另一篇博客:
https://blog.csdn.net/u012481976/article/details/82724916
一共16个freelist,每个freelist就是一个链表,16个的区别就是链表节点所占空间大小不一样,:
对于每个freelist的空间申请与释放,其实就是一些链表的操作。
以下代码描述了如何利用二级配置器中的allocator()配置空间,以及freelist空间如何与mempool之间通信,代码如下:
static void* allocate(size_t n)
{
obj* volatile* my_free_list;
void* result = 0;
//如果大于128B, 直接调用一级配置器
if (n > (size_t)_MAX_BYTES)
{
return (malloc_alloc::allocate(n));
}
//寻找 16个free-list 中的一个
my_free_list = free_list + FREELIST_INDEX(n);
result = *__my_free_list;
if (result == 0)
{
//如果freelist上没有可用空间,则将空间调整至8的倍数
//调用refill,向mempool申请内存,重新填充该freelist
result = refill(ROUND_UP(n));
return result;
}
else
{
*my_free_list = result->_M_free_list_link;
}
return result;
};
其中freelist与mempool之间的通信函数refill(),源码如下:
template
void* __default_alloc_template::refill(size_t n)
{
//默认取20个新节点连接到freelist上(其实是19个,第一个返回给用户)
int nobjs = 20;
//调用chunk_alloc(),尝试取得nobjs个区块作为freelist的新节点
//注意此处参数nobjs是通过引用传入,有可能变小
char* chunk = chunk_alloc(n, nobjs);
obj* volatile* my_free_list;
obj * result;
obj * current_obj, *next_obj;
int i;
//如果只获得一个区块,则将这个区块直接反馈,freelist无新节点
if (1 == nobjs)
{
return chunk;
}
//找到需要填充的链表的位置
my_free_list = freeList + FREELIST_INDEX(n);
result = (obj*)chunk;//第一块返回给客户端
//引导freelist指向新的空间
*my_free_list = next_obj = (obj*)(chunk + n);//这里把第二块先挂到指针数组对应位置下 //注意这里的n在传参数时已经调整到8的倍数
for (i = 1;; i++) {//从1开始,0返回给客户端
cur_obj = next_obj;
next_obj = (obj*)((chat*)next_obj + n);
if (nobjs - 1 == i) { //因为第一次从内存池取下的空间在物理上是连续的 尾插方便用 以后用完还回自由链表的就不是了
cur_obj->free_list_link = NULL;//这里没有添加节点
break;
}
else {
cur_obj->free_list_link = next_obj;//nobjs - 2是最后一次添加节点
}
}
return result;
}
如果释放的空间大于128b则调用第一级配置器,如果小于128b,则将要释放的空间链接到对应的freelist上,也就是一个在链表头插入节点的过程:
static void deallocate(void* p, size_t n)
{
obj* volatile* my_free_list;
obj* q = (obj*)p;
//如果大于128,调用第一级配置器
if (n > (size_t)_MAX_BYTES)
{
malloc_alloc::deallocate(p, n);
return;
}
//寻找对应的freelist
my_free_list = _S_free_list + _S_freelist_index(n);
//回收该区块
q->free_list_link = *my_free_list;
*my_free_list = q;
}
chunk_alloc()是负责mem pool与系统内存打交道的,源码如下:
template
void* __default_alloc_template::chunk_alloc(size_t size, int& nobjs)
{
char * result;
size_t total_bytes = size * nobjs;
size_t bytes_left = end_free - start_free;
// 内存池剩余空间完全满足需求量
if (bytes_left >= total_bytes)
{
result = start_free;
start_free += total_bytes;
return result;
}
else if (bytes_left >= size)
{
// 内存池剩余空间不能完全满足需求量,但足够供应一个(含)以上的区块
nobjs = bytes_left / size;
total_bytes = size * nobjs;
result = start_free;
start_free += total_bytes;
return result;
}
else
{
// 内存池剩余空间连一个区块的大小都无法提供
size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
// 以下试着让内存池中的残余零头还有利用价值
if (bytes_left > 0)
{
// 内存池内还有一些零头,先配给适当的free list
// 首先寻找适当的free list
obj * volatile * my_free_list = free_list + FREELIST_INDEX(bytes_left);
// 调整free list,将内存池中的残余空间编入
((obj *)start_free)->free_list_link = *my_free_list;
*my_free_list = (obj *)start_free;
}
// 配置heap空间,用来补充内存池
start_free = (char *)malloc(bytes_to_get);
if (0 == start_free)
{
// heap空间不足,malloc失败
int i;
obj * volatile * my_free_list, *p;
// 试着检视我们手上拥有的东西,这不会造成伤害。我们不打算尝试配置
// 较小的区块,因为那在多进程机器上容器导致灾难
// 以下搜寻适当的free list
// 所谓适当是指“尚未用区块,且区块够大”的free list
for (i = size; i <= __MAX_BYTES; i += __ALIGN)
{
my_free_list = free_list + FREELIST_INDEX(i);
p = *my_free_list;
if (0 != p)
{ // free list内尚有未用块
// 调整free list以释放未用区块
*my_free_list = p->free_list_link;
start_free = (char *)p;
end_free = start_free + i;
// 递归调用自己,为了修正nobjs
return chunk_alloc(size, nobjs);
// 注意,任何残余零头终将被编入适当的free list中备用
}
}
end_free = 0; // 如果出现意外,调用第一级配置器,看看oom机制能否尽力
start_free = (char *)malloc_alloc::allocate(bytes_to_get);
// 这会抛出异常 或 内存不足的情况得到改善
}
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
// 递归调用自己,为了修正nobjs
return chunk_alloc(size, nobjs);
}
}
我觉得书上举的例子对这段代码的解释再合适不过了,非常透彻:
自己懒得画了摘了一个:
提供的三个工具uninitialized_copy()、uninitialized_fill()、uninitialized_fill_n(),用于将内存的配置与对象的构造分别开来。如何分开的呢?我们先看一下对于一个全区间的构造函数,如何构造对象的:
1,配置内存区块,足以包含范围内的所有元素;
2,在内存上构造对象;
那么这三个函数是如何发挥作用的呢?这里用到了is_POD_type的概念。POD意指Plain Old Data,也就是标量型别或传统的C struct型别。POD必然拥有trivial ctor/dtor/copy/assignment函数,因此我们可以:
对POD型别采取最有效的初值填写法,如:
int a;
a = 5;
而对non-POD型别采取最保险的安全做法:
char* p = new char;
new(p) char(5);
至于怎么判断一个迭代器所指对象的型别,那就是利用__type_trait了,后续再说。
如果is_POD_type是__true_type,那么这几个工具就调用相应的算法copy()、fill()、fill_n()。如果是__false_type则调用第4节提到的construct()。
花了三天晚上看书,加上一个周末的下午+晚上串联思想与写这篇博客,总体感觉收获还是蛮多的,对于STL的内存配置以及泛型变成都有了一定得了解,还可以~