何为空间配置器
为何需要先了解空间配置器
从使用STL层面而言,空间配置器并不需要介绍,因为容器底层都封装好了,但是如果从STL实现角度出发,空间配置器是必须先了解的。
作为STL设计背后的支撑,空间配置器总是在默默付出着。为什么你可以使用算法来高效的处理数据,为什么你可以对容器进行各种操作,为什么你可以使用的迭代器遍历空间,这一切的一切,都有“空间配置器”的功劳
SGI STL 专属空间配置器
SGI STL 的空间配置器与众不同,且与 STL 标准规范不同。
其名为alloc,而非allocator
虽然SGI也配置了allocator,但是它自己并不使用,也不建议我们使用,原因是效率比较感人,因为它只是在基层进行配置/释放空间而已,而且不接受任何参数。
SGI STL的每一个容器都已经指定缺省的空间配置器alloc
template<class T, class Alloc = alloc> //预设使用alloc配置器
class vector {...};
在C++中1,当我们调用new和delete进行对象的创建和销毁的时候,也同时会有内存配置操作和释放操作。
这其中的new和delete都包含两阶段操作:
- 对于new来说,编译器会先调用
::operator new
分配内存,然后调用Obj::Obj()
构造对象内容 - 对于delete来说,编译器会先调用
Obj::~Obj()
析构对象,然后调用::operator delete
释放空间
为了精密分工,STL allocator决定将这两个阶段操作区分开来:
- 对象构造由
::construct()
负责,对象释放由::destory
负责 - 内存配置由
alloc::allocate()
负责;内存释放由alloc:deallocate()
负责
构造和析构源码
我们知道,程序内存的申请和释放离不开基本的构造和析构基本工具:construct()和destory()
在STL里面,construct()函数接收一个指针P和一个初始值value,该函数的用途就是将初值设定到指针所指的空间上。
destory()函数有两个版本,第一个版本接受一个指针,准备将该指针所指之物析构调。直接调用析构函数即可。
第二个版本接受first和last两个迭代器,将[first,last)范围内的所有对象析构调
#include<new.h> //如果响应使用布局new,就必须先包含此文件
template<class T1, class T2>
inline void construct(T1 *p, const T2& value){
new (p) T1(value); //布局new,调用T1::T1(value)
}
//destory() 第一个版本
template<class T>
inline void destory(T *pointer){
pointer->~T(); //调用dtor ~T();
}
/*destory() 第二个版本,接受两个迭代器,此函数设法找出元素的数值类型,进而利用
* __type_traits<>求取最适当措施
*/
template <class ForwardIterator>
inline void destory(ForwardIterator first, ForwardIterator last){
__destory(first, last, value_type(first));
}
其中 destroy() 只截取了部分源码,全部实现还考虑到特化版本,⽐如判断元素的数值类型 (value type) 是否有trivial destructor 等
内存的配置和释放
对象的构造和析构之后的内存管理的很多事情,都由< stl_alloc.h >负责。SGI对此的设计原则如下:
- 向system heap要求空间
- 考虑多线程状态
- 考虑内存不足时的应变措施
- 考虑过多“小型区块”可能导致的内存碎片问题
考虑到小型区块可能造成的内存碎片问题,SGI为此设计了双层级配置器。当配置区块超过128bytes时,使用第一级配置器,直接使用malloc()和free()
当配置区域小于等于128byytes时,为了降低额外负担,直接使用第二级配置器,采用复杂的memory pool处理方式。
⽆论第⼀级配接器(malloc_alloc_template)或是第⼆级配接器(default_alloc_template),alloc都为其包装了接口,使其能够符合STL标准
其中 SGI STL 将配置器多了⼀层包装使得 Alloc 具备标准接⼝。
alloc ⼀级配置器源码解读
(1)第一级配置器以malloc()、free()、realloc()等C函数执行实际的内存配置、释放和重配置操作,并实现类似C++ new-handler的机制(因为它并⾮使⽤ ::operator new 来配置内存,所以不能直接使⽤C++ new-handler 机制)
(2)SGI的第一级配置器的allocate()和reallocate()都是在调用malloc()和realloc()不成功后,改调用oom_malloc()和oom_realloc()
#include <new>
//注意,alloc不接受“template 型别参数”,所以就算你定义了也用不上
class __malloc_alloc_template
{
private: //这里面都是函数指针,用来处理内存不足的情况
static void *oom_malloc(size_t);
static void *oom_realloc(void *,size_t);
static void (* __malloc_alloc_oom_handler)();
public:
//正常空间配置
static void * allocate(size_t n)
{
void *result = malloc(n);
if( 0 == result)
result = oom_malloc(n);
return result;
}
//正常空间回收
static void deallocate(void *p,size_t /*n*/)
{
free(p);
}
//正常重分配空间
static void * reallocate(void *p,size_t /* old_sz */,size_t new_sz)
{
void * result = realloc(p,new_sz);
if( 0 == result )
result = oom_realloc(p,new_sz);
return result;
}
}
下面为异常情况处理(空间不够)
//malloc-based allocator,通常比后面介绍的default alloc速度慢
//一般而言是thread-safe,对于空间的运用比较高效。
//下面是第一级配置器
// 注意,无“template类型参数”,至于“非类型参数”inst,则完全没有派上用场
template<int inst>
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
template<int inst>
void *__malloc_alloc_template<inst>::oom_malloc(size_t n){
void (*my_malloc_handler)();
void *result;
for(;;){ //不断尝试释放、配置、再释放、再配置
my_alloc_handler = __malloc_alloc_oom_handler;
if(0 == my_alloc_handdler) {
__THREAD_BAD_ALLOC;
}
(*my_malloc_handler)(); //调用处理例程,企图释放内存
result = malloc(n); // 再次尝试配置内存
if(result)
return (result);
}
}
template<int inst>
void *__malloc_alloc_template<inst>::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) {
__THREAD_BAD_ALLOC;
}
(*my_malloc_handler)(); //调用处理例程,企图释放内存
result = realloc(p, n); // 再次尝试配置内存
if(result)
return (result);
}
}
//注意,以下直接将参数inst指定为0
type __malloc_alloc_template<0> malloc_alloc;
alloc二级配置器源码解读
第二季配置器多了一些机制,专门针对内存碎片。内存碎片化带来的不仅仅是回收时的困难,配置也是一个负担,额外负担永远无法避免,毕竟系统要划出这么多的资源来管理另外的资源,但是区块越小,额外负担率就越高。
第二级配置器到底解决了什么问题?
简单来说SGI第二季配置器的做法就是:sub-allocation(分层架构)
SCI STL的第一级配置器是直接使用malloc()、free()、realloc()并配合类似C++ new-handler机制实现的,第二季配置器的工作机制要根据区块的大小是否大于128bytes来采取不同的策略。
那什么是memory pool呢?
自由链表自由在哪?又该如何维护呢?
我们知道,一方面,自由链表中有些区块已经分配给了客端使用,所以free_list不需要再指向它们;另一方面,为了维护free-list,每个节点还需要额外的职责指向下一个节点。
那么问题来了,如果每个节点有两个指针,这不就造成了额外负担了吗?本来我们设计 STL 容器就是⽤来保存对象的,这倒好,对象还没保存之前,已经占据了额外的内存空间了。那么,有⽅法解决吗?当然有!
(1)在这之前我们先来了解另⼀个概念——union(联合体/共⽤体)
- 联合体是一种特殊的类,也是一种构造类型的数据结构
- 联合体表示几个变量公用一个内存位置,在不同的时间保存不同的数据类型和不同长度的变量
- 所有的联合体成员公用一个空间,并且同一时间只能存储其中一个成员变量的值。比如下面
union ChannelManager{
char ch;
int num;
char *str;
double *exp;
};
一个union只配置一个足够大的空间以容纳最大长度的数据成员,比如上面,最大长度是double,所以ChannelManager的空间大小就是double数据类型的大小。再C++中,union的成员默认属性页为public。union主要用来压缩空间,如果一些数据不可能在同一时间同时被用到,则可以使用union
(2)了解了 union 之后,我们可以借助 union 的帮助,先来观察⼀下 free-list 节点的结构
union obj{
union obj * free_list_link;
char client_data[1];
};
来深⼊了解 free_list 的实现技巧,请看下图。
在 union obj 中,定义了两个字段,再结合上图来分析:
- 从第一个字段来看,obj可以看作一个指针,指向链表的下一个节点
- 从第二季字段来看,obj也可以看作一个指针,不过指向的是实际的内存区
⼀物⼆⽤的好处就是不会为了维护链表所必须的指针⽽造成内存的另⼀种浪费,或许这就是所谓的⾃由奥义所在!
第⼆级配置器的部分实现内容
enum {__ALIGN = 8}; //小区块的上调边界
enum {__MAX_BYTES = 128;} //小型区块的上限
enum {__NFREELISTS = __MAX_BYTES/____ALIGN}; //free-list个数
//以下是第二级配置器
//注意,无“template类型参数”,且第二参数完全没派上用场
//第一参数用于多线程环境下。这里不讨论多线程环境
template <bool threads, int inst>
class __default_alloc_template {
private:
//ROUND_UP()将bytes上调至8的倍数
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:
//16个free-lists
static obj *volatile free_list [__NFREELISTS];
//以下函数根据区块大小,决定使用第n号free-list.n从0起算
static size_t FREELIST_INDEX(size_t bytes) {
return (((bytes) + __ALIGN-1)/__ALIGN-1);
}
//返回一个大小为n的对象,并可能加入大小为n的其他区块到free list
static void *refill(size_t n);
//配置一大块空间,可容纳nobjs个大小为“size”的区块
//如果配置nobjs个区块有所不便,nobjs可能会降低
static char *chunk_alloc(size_t size, int &nobjs);
//Chunk allocation state
static char *start_free; //内存池起始位置。只在chunk_alloc()中变化
static char *end_free; //内存池结束位置。只在chunk_alloc()中变化
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);
};
//以下是static data member的定义与初始值设定
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[__NFREELISTS] =
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
空间配置器函数allocate源码解读
从上面可以看出,第二级配置器拥有配置器的标准接口allocate()。此函数首先会判断区块大小,如果大于128bytes->调用第二级配置器;小于128bytes->就检查对应的free_list(如果没有可用区块,就将区块上调至8的倍数的边界,然后调用refill(),为free list重新填充空间
空间申请
调用标准配置接口allocate()
static void *allocate(size_t n)
{
obj *volatile *my_free_list;
obj *result;
//大于128就调用第一级配置器
if (n >(size_t)__MAX_BYTES) {
return (malloc_alloc::allocate(n));
}
//寻找16个free lists中适当的一个
my_free_list = free_list + FREELIST_INDEX(n);
result = *my_free_list;
if (result ==0 ) {
//没有找到可用的free list,准备重新填充free list
void *r = refill(ROUND_UP(n)); //下节详述
return r;
}
//调整free list
*my_free_list = result ->free_list_link;
return (result);
};
NOTE:每次都是从对应的 free_list 的头部取出可⽤的内存块。然后对free_list 进⾏调整,使上⼀步拨出的内存的下⼀个节点变为头结点
空间释放
同样,作为第⼆级配置器拥有配置器标准接⼝函数 deallocate()。该函数⾸先判断区块⼤⼩,⼤于 128bytes 就调⽤第⼀级配置器。⼩于 128 bytes 就找出对应的 free_list,将区块回收。
//p不可以是0
static void deallocate(void *p, size_t n)
{
obj *q = (obj*) p;
obj *volatile *my_free_list;
//大于128就调用第一级配置器
if (n > (size_t)__MAX_BYTES) {
malloc_alloc::deallocate(p,n);
return;
}
//寻找对应的free list
my_free_list = free_list +FREELIST_INDEX(n);
//调整free list,回收区块
q->free_list_link = *my_free_list;
*my_free_list = q;
}
NOTE:通过调整 free_list 链表将区块放⼊ free_list 的头部。
重新填充 free_lists
- 当发现free-list中没有可用空间,就会调用refill()为free-list重新填充空间
- 新的空间取自内存池(由chunk_alloc()完成)
- 缺省取得20个新空间(区块),如果内存池空间不足,获得的节点将小于20
//返回一个大小为n的对象,并且有时候会为适当的free list增加节点
//假设n已经适当上调至8的倍数
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
int nobjs = 20;
//调用chunk_alloc(),尝试取得nobjs个区块作为free list的新节点
//注意参数nobjs是pass by reference
char * chunk = chunk_alloc(n, nobjs); //稍后详述
obj * volatile *my_free_list;
obj *result;
obj *current_obj, *next_obj;
int i;
//如果只获得一个区块,这个区块就分配给调用者用,free list无新节点
if (1 == nonjs ) return(chunk);
//否则准备调用free list,纳入新节点
my_free_list = free_list + FREELIST_INDEX(n);
//以下在chunk空间内建立free list
result = (obj*)chunk; //这一块准备返回客端
//以下导引free list指向新配置的空间(取自内存池)
*my_free_list = next_obj = (obj*)(chunk + n);
//以下将free list的各节点串接起来
for (i = 1; ;i++) { //从1开始,因为第0个将返回给客端
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使用,是chunk_alloc()在工作,它是怎么工作的呢?
我们先来分析 chunk_alloc() 的⼯作机制:
chunk_alloc() 函数以 end_free – start_free 来判断内存池的“⽔量”,具体逻辑如下:
- 当向内存池申请空间时,需要判断该内存池中还有多少“水量”?
- 水量充足———>直接调出20个区块返回给free list(缺省是20)
- 1≤水量<20——>拨出这不足20的区块的空间出去
- 水量=0———–>调用malloc()从heap中配置内存(新水量的大小为需求量的两倍)
- 万一连system heap空间都不够了,malloc()注水失败怎么办呢?此时,chunk_alloc()会寻找有无“尚有未用区块,且区块足够大”的free list?
- 有—–>找到了就挖出一块并交出
- 无—–>调用第一级配置器
- 我们知道第一级配置器其实也是使用malloc()来配置内存的,但它有out-of-memory处理机制(类似new handler机制),或许有机会释放其他的内存拿来此处使用。
- 如果第一级配置器的malloc()也失败了,就发出bad_alloc异常。
//注意参数nobjs是pass by reference
template <bool threads, int inst>
char*
__default_alloc_template<threads, inst>::
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; //如果出现意外(山穷水尽,到处都没内存可用了)
//调用第一级配置器,看看out-of-memory机制能够尽点力
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));
}
}