导言
在深入探索今天的内容前,你可以在心中思考思考下列问题:
- 当你调用new与delete时编译器底层的做了什么事情?
- STL 各大容器底层空间配置器是什么样子的?
- STL 的到底需要做什么样的实现?
- 什么是内存池及它的实现是怎样的?
- …
STL六大组件
在深入空间配置器前,我觉得我们还是有必要先了解一下STL的基础知识
STL仅仅是一个标准,内部实现是没有任何要求的。因此导致STL存在许多的实现版本,PJ STL(被Visual C++)所采用。我们所学习的SGI STL版本注释丰富、结构清晰、可读性最强,所以最后被GCC所采用,同时也是最流行的版本。STL主要有六大组件来支撑着,它们是:
- 配置器(allocator): 为容器提供空间配置与释放,对象的构造与析构的服务,也是一个class template。
- 容器(containers): 常用的数据结构,大致分为两种序列容器与关联容器。在实现上是类模板。
- 迭代器(iterator): 一套访问容器的接口,行为类似于指针。它为不同的算法提供相对统一的容器访问方式,便利设计算法时无需过多关心数据。
- 算法(algorithms): 提供一套常用的算法,如
sort、search、copy、erase ...
。在实现上,可以认为是一种函数模板。 - 仿函数(functor): 作为函数使用的对象,用于泛化算法中的操作。
- 配接器(adapter): 将一种容器修饰为不同的另一种容器。
大致关系如下图所示
空间配置器
从STL使用的角度来说,空间配置器是最不需要介绍的东西。因为它们早已被容器封装好了,在一切组件后默默付出。但是从实用角度来说,若是不了解STL的空间配置器,无异于空中楼阁,在阅读其它STL实现时都会遇到困难。
专属配置器
对于SGI STL来说,很多的项目早已脱离了STL标准规格。因此,SGI STL使用了一个专属的、拥有次层配置(sub-allocation)能力的、效率卓越的特殊配置器的alloc
。当然SGI STL也提供了一个默认的内存配置器为allocator
,只不过在大部分情况下,我们很少使用这个效率不佳且仅做内存分配与释放的配置器。
alloc不同于allocator,它不接受任何参数,以vector
为例,它的的标准写法如下:
1 2
| template <class T, class Alloc = alloc> class vector {...};
|
对象与内存
回到上面的问题,对于C++中new/delete的操作是怎样的?在日常使用中,我们所习惯的操作大致如下:
1 2 3
| class Foo {...}; Foo* pf = new Foo; delete pf;
|
对于这里的new/delete算式都内含了两步的操作
new :调用::operator new 配置内存;调用Foo::Foo() 构造对象内容。
delete:调用对象Foo::~Foo()将对象析构;调用::operator delete 释放内存
为了精密分工,STL allocator 决定将这两个阶段分开
对于内存:分配时使用 alloc::allocate();释放时使用 alloc::deallocate()。
对于对象:构造时使用::construct();析构时使用::destroy();
大家可以通过下图来帮助理解与记忆
构造与析构
通过上图的理解,知道这两个函数是如何完成对象的构造与析构。接下来我们通过对其源码的剖析,对其作用加深理解。
源码阅读
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include <new.h>
template <class t1, class t2> inline void construct(t1* p,const t2&value) { new (p) t1(value); }
template <class t> inline void destroy(t* pointer) { pointer->~t(); }
template<class forwarditerator> inline void destory(forwarditerator first,forwarditerator last) { __destroy(first,last,value_type(first)); }
template <class forwarditerator,class t> inline void __destroy(forwarditerator first, forwarditerator last,t*) { typedef typename __type_traits::has_trival_destructor trivial_destructor; __destroy_aux(first,last,trivial_destructor()); }
template <class forwarditerator> inline void __destroy_aux(forwarditerator first,forwarditerator last,__false_type) { for (; first < last; ++first) destroy(&*first); }
template <class forwarditerator> inline void __destroy_aux(forwarditerator,forwarditerator,__true_type) {}
inline void destroy(char* ,char**) {} inline void destroy(wchar_t*,wchar_t*) {}
|
源码分析
在STL源码中,construct()
仅接受一个指针p与一个初值value。它的使用是将初值设定到指针所指向空间上。在C++中placement new运算子也可完成这一操作。
对于析构destroy
则存在两个版本。第一个版本仅接受一个指针,准备将该指针所指向之物析构掉;第二个版本则是接受[first,last)两个迭代器,将范围内的对象析构掉。为了效率的考虑,调用类型萃取防止无效析构拖慢性能。在检查结束后,就会调用第一版本的destroy来析构对象。
空间配置核心
剖析完对象构造与析构的源码后,接下来我们一起深入内存的配置与释放之旅,了解空间配置器核心————alloc。
前言
对象构造前的空间配置与对象析构后的空间释放,全由<stl_alloc.h>负责,SGI对此的设计哲学如下:
- 向system heap要求空间
- 考虑多线程(multi-threads)状态
- 考虑内存不足的应变措施
- 考虑过多”小型区块”可能造成的内存碎片(fragment)问题
考虑到小型区块可能造成的内存破碎问题,SGI设计了双层级配置器。当配置区块>128bytes时,则采用一级配置器。当配置区块<128bytes时,则调用二级配置的memory pool来处理。
之前提及过,alloc这个次层配置、效率卓越的特殊配置器逸脱了STL标准。SGI为其包装了一个接口从而符合STL规格。下面是它们的定义与包装后的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| # ifdef __USER_MALLOC ... typedef __malloc_alloc_template<0> malloc_alloc; typedef malloc_alloc alloc; # else ...
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc; #endif
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(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));} };
|
一二级配置器关系
一二级配置器的包装接口与运用方式
一级配置器剖析
结合上图,让我们看看一级配置器是如何仿真C++ set_malloc_handler
的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
template <int inst> class __malloc_alloc_template { private: static void *oom_malloc(size_t); static void *oom_realloc(void* ,size_t); static void (* __malloc_alloc_oom_bandler) (); public: static void *allocate(size_t n) { void *result = malloc(n) if (0 == result) result = com_malloc(n); return result; }
static void deallocate(void *p, size_t) { free(p) }
static void *reallocate(void *p,size_t ,size_t new_sz) { void *result = reallocate(p, new_sz); if (0 == result) result = oom_realloc(p, new_sz); return result; }
static void (* set_malloc_handler(void (*f)())) () { void (* old)() = __malloc_alloc_oom_bandler; __malloc_alloc_oom_bandler = f; return (old); } }
|
通过观察一级配置器可以得知,一级配置器是以malloc()、free()、realloc()等C函数执行实际的内存的配置、释放、重配置等操作,并实现出类似C++ new handler
的操作。
SGI一级配置器的顺序是先使用allocate()
与realloc()
(allocate 调用malloc,realloc调用realloc())。若是分配没有成功,就会改而调用oom_malloc()
与oom_realloc()
。它们会不断的循环调用内存不足处理例程,去解决问题。倘若没有设定内存不足处理例程,编程器则会调 _ _ THROW_BAD_ALLOC,丢出bac_alloc异常,或是直接调用exit(1)强制中止程序。
一级配置器的malloc-based allocator 相对于二级配置器的default alloc 会慢一些。但是对于线程安全和空间的运用上来说,malloc-base allocator会显得更加高效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
template <int inst> class __malloc_alloc_template { private: static void *oom_malloc(size_t); static void *oom_realloc(void* ,size_t); static void (* __malloc_alloc_oom_bandler) (); public: static void *allocate(size_t n) { void *result = malloc(n) if (0 == result) result = com_malloc(n); return result; }
static void deallocate(void *p, size_t) { free(p) }
static void *reallocate(void *p,size_t ,size_t new_sz) { void *result = reallocate(p, new_sz); if (0 == result) result = oom_realloc(p, new_sz); return result; }
static void (* set_malloc_handler(void (*f)())) () { void (* old)() = __malloc_alloc_oom_bandler; __malloc_alloc_oom_bandler = f; return (old); } }
|
二级配置器剖析
二级配置器对于一级来说又多了一些机制,专门针对于内存碎片的处理。内存的碎片化不仅是增加了回收的难度,对于配置来说也是一个负担。并且这种负担是无法避免的,因为不管任何程序向system heap申请内存,都必须缴税。这个税也就是用来记录内存大小的信息,系统也正是依赖这些税才可以更好的管理内存。
那么对于这些浩如烟海的税,我们的系统又是如何去管理的呢?二级配置器提供了一个很好的方法即使用自由链表去管理。大家肯定好奇自由链表为何是自由的?首先,自由链表每次都是直接配置一大块内存,客户端需要时直接分配给它,也就是从free-list中拔出。若客户端释还小额区块,则依旧还给free-list即重新插入free-list中。
既然使用链表,那我们又该如何去维护它呢?在这里大师提供了一个绝妙的方法,在了解这个方法之前,我们先了解一个概念————union(共用体)。如果了解过了可跳过这部分,不了解的同学可以学习回顾一下。
- 共用体是一种特殊的类,也是一种构造类型的数据结构
- 共用体表示几个变量共用一个内存位置,在不同的时间保存不同的数据类型与不同长度的变量。
- 所有的共用借机成员共用一个空间,并且同一时间只能存放一个成员变量的值
1 2 3 4 5 6 7
| union test { size_t a; int b; double c; long d; char e; }
|
对于test来说,这里的数据大小就是long的大小。在C++中union主要用来压缩空间的。
在了解union之后,我们就可以来学习自由链表节点设计的精妙了
1 2 3 4
| union obj { union obj* free_list_link; char client_data[1]; }
|
为了防止需要额外指针去指向下一个,因而union的第一个字段可以当做一个指针指向下一个。对于第二个区块,也可作为一个指针,不过此时指向的是实际的内存区。这样的一物两用,既实现了维护的目标又达到了避免浪费的目的。
底下是二级配置器中的free_list的部分实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| enum {__ALIGN = 8}; enum {__MAX_BYTES = 128}; enum {__NFRELISTS = __MAX_BYTES / __ALIGN};
template <bool threads,int inst> class __default_alloc_template { private: static size_t ROUND_UP (size_t bytes) { return (((bytes)+__ALIGN-1) & ~(__ALIGN-1)); } private: union obj { union obj * free_list_link; char client_data[1]; } private: static obj * volatile free_list[__NFRELISTS]; static size_t FREELIST_INDEX(size_t bytes) { return (((bytes)+__ALIGN-1) & ~(__ALIGN-1)); }
static void *refill(size_t n);
static char* chunk_alloc(size_t size,int &nobjs);
static char* start_free; static char* end_free; static size_t heap_size;
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); };
template <bool threads,int inst> char *__default_alloc_template<threads,inst>::start_free = 0;
template<bool threads, int inst> char *__default_alloc_template<threads, inst>::end_free = 0;
template<bool threads,int inst> size_t __default_alloc_template<threads, inst>::heap_size = 0;
template<bool threads, int inst> __default_alloc_template<threads,inst>::obj * volatile __default_alloc_template<threads, inst>::free_list[__NFRELISTS]= {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
|
allocate源码剖析
在了解free-list这样一个二级配置器的密钥后,我们就带着这个钥匙打开allocate()的大门。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| static void *allocate(size_t n) { obj * volatile * my_free_list; obj * result;
if (n > (size_t) __MAX_BYTES) { return (malloc_alloc::allocate(n)); }
my_free_list = free_list + FREELIST_INDEX(n); result = * my_free_list; if (result == 0) { void *r = refill(ROUND_UP(n)); return r; } *my_free_list = result -> free_list_link; return (result); };
|
作为二级配置器,( _ _ default_alloc_templat)同样也拥有配置的标准接口函数allocate()。函数的作用:首先判断区块大小,超过128就调用一级配置器。小于就检查对应的free-list,如果free list有可用区块,就直接拿来使用。如果没有可用的,就将区块大小上调至8的倍数,然后调用refill(),为free list重新填充空间。
allocate函数实现示意图
deallocate()源码剖析
在了解完alloate的空间配置,我们同时也应该明白,空间的释放是如何进行的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| static void deallocate(void *p,size_t n) { obj *q = (obj*) p; obj * volatile * my_free_list;
if (n > (size_t) __MAX_BYTES) { malloc_alloc::deallocate(p, n); return; }
my_free_list = free_list + FREELIST_INDEX(n); q->free_list_link = *my_free_list; *my_free_list = 0; }
|
作为二级配置器的标准接口函数deallocate()。它的使用是:首先判断区块大小,大于128调用一级,小于128就找到对应的free list,将其回收。
deallocate函数作用示意图
refill()源码剖析
作为品尝内存池的餐前小点,你确定不看看吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
template <bool threads, int inst> void *__default_alloc_template<threads, inst>::refill(size_t n) { int nobjs = 20; char* chunk = chunk_alloc(n, nobjs); obj* volatile * my_free_list;
obj * result; obj * current_obj, * next_obj; int i;
if (i == nobjs) return (chunk); my_free_list = free_lis + FREELIST_INDEX(n);
result = (obj*) chunk; *my_free_list = next_obj = (*obj *)(chunk + n); for (i = 1; ;i++) { current_obj = next_obj; next_obj = (obj*)((char *)next_obj + n); if (nobjs - 1 == i) { current_obj -> free_list_link = 0; break; } else { current_obj -> free_list_link = next_obj; } } return (result); }
|
当发现free list中没有可用区块时,就会调用refill()为free list注入新空间。新的空间皆是取自内存池(经由chunk_alloc()完成)。当内存池水够时,缺省会获得20个新区块。如果水不够,获得的节点可能少于20。
形象一点的说法就是:refill()就相当于水杯,free list里没有水了,它就会从内存池子里面舀水并灌到free list中。当内存池水不够了,那么杯子就是能舀多少舀多少水灌到free list中。
二级配置器核心——内存池
在有了前面的基础下,我们终于来到memory pool这里了。其实内存池的意思很简单,就像下图表示的一样
让我们再通过源码来深入了解了解内存池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
|
template <bool threads, inst> char* __default_alloc_template<threads,inst>:: chunk_alloc(size_t size,int& nobjs) { char * result; size_t total_types = size * nobjs; size_t bytes_left = end_free - start_free; if ( bytes_left >= total_types) { result = start_free; start_free += total_types; return(result); } else if (bytes_left >= size) { nobjs = bytes_left /size; total_types = size * nobjs; result = start_free; start_free += total_types; return (result); } else { size_t types_to_get = 2 * total_types + ROUND_UP(heap_size >> 4); if (bytes_left > 0) { obj * volatile * my_free_list = free_list + FREELIST_INDEX(bytes_left); ((*obj)start_free)-> free_list_link = *my_free_list; *my_free_list = (obj*)start_free; }
start_free = (char*)malloc(bytes_to_get); if (0 == start_free) { int i; obj * volatile * my_free_list *p; for (i = size; i< __MAX_BYTES; i += __ALIGN) { my_free_list = free_list + FREELIST_INDEX(i); p = *my_free_list; if (0 != p) { *my_free_list = p->free_list_link; start_free = (char*) p; end_free = start_free + i; return (chunk_alloc(size, nobjs)); } }
end_free = 0 start_free = (char*) malloc_alloc::allocate(bytes_to_get); } heap_size += bytes_to_get; end_free = start_free + bytes_to_get; return (chunk_alloc(size,nobjs)); } }
|
结合书上的例子与下图,对内存池理解进一步加深
总结
空间配置器作为STL六大组件的基础,我们之后学习新的内容时。要时常的复习这部分,要结合新的内容,进行全面开拓。