空间适配器(Allocator)
前言
侯捷在《STL源码剖析》一书讲到:
这是STL学习的第一部分,空间适配器,所谓空间适配器,就是用来管理内存的一个器具。对于STL来说,空间适配器是它可以正常工作的基础,也为它可以高效工作提供了动力。对于使用STL来说,它是不和用户直接打交道的,而是隐藏在一切STL组建之后,默默为各种内存申请提供支持的。
主要思想
对象构造前的空间配置和对象析构后的空间释放,由stl_alloc.h负责,SGI对此的设计哲学如下:
(1) 向system heap 要求空间;
(2) 考虑多线程(multi-threads)状态;
(3) 考虑内存不足时的应变措施;
(4) 考虑过多“小型区块”可能造成的内存碎片(fragment)问题。
C++的内存配置基本操作是::operator new(),内存释放基本操作是::operator delete()。这两个全局函数相当于C的malloc() 和 free() 函数。SGI正是以malloc()和free() 完成内存的配置和释放。考虑到小型区块所可能造成的内存破碎问题,SGI 设计了双层级配置器,第一级配置器直接使用malloc() 和 free() ,第二级配置器则是情况采用不同的策略:以配置128bytes区块为界,大于则调用第一级配置器,小于则采用复杂的memory pool整理方式,同时也取决是否定义了_USE_MALLOC。
具备次配置力的SGI空间配置器
SGI有一个标准空间适配器,同时还有一个特殊空间适配器:
标准空间适配器为: std::allocator,这个适配器只是对new和delete的浅层包装,所以没有什么技术含量,所以在SGI中从没使用过这个标准适配器。
特殊空间适配器是: std:alloc,这是一个具有次分配能力的特殊空间适配器,它具有一级和二级适配器,它们协调工作。
Std::alloc的主要思想是:定义一个空间大小阈值,128bytes,如果申请的空间大于128bytes,那么就调用第一级空间适配器来完成分配工作,如果小于128bytes,那么就调用第二级空间适配器来完成。
需要注意的是,SGI版STL提供了一层更高级的封装,定义了一个simple_alloc类,无论是用哪一级都以模板参数alloc传给simple_alloc,这样对外体现都是只是simple_alloc。
而它的代码实现比较简单,仅仅是调用一级或者二级配置器的接口 allocator.h
#ifdef _ALLOCATOR_H_
#define _ALLOCATOR_H_
/*
*为alloc类封装接口
*/
#include"alloc.h"
#include"construct.h"
#include<cassert>
#include<new>
namespace EasySTL
{
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));
}
};
}
#endif
第一级配置器
对于第一级适配器:直接调用malloc和free来完成分配与释放内存的工作(没有调用new和delete),最为重要的是,第一级适配器具有new-handle机制,用户可以指定当出现out-of-memory时的处理函数,在SGI里面,当第一级alloc失败时,会接着调用oom_alloc函数来尝试分配内存,如果oom发现没有指定new-handler函数的话,那就无能为力了!会抛出__THROW_BAD_ALLOC这个异常,下面是第一级适配器的主要流程(对于alloc来讲,其他如realloc一样):
1、空间分配器的“分线器”
static void * allocate(size_t n)
{
void *result = malloc(n);//直接使用第一级分配器,直接使用malloc
if (0 == result) result = oom_malloc(n);//第一级分配器失效了,那就使 用第二级分配器,oom(out of memeory)
return result;//将分配的空间以void*的方式返回,用户可以随意转化为需要的类型
}
n为我们想要申请的内存,如果malloc可以满足要求的话,直接返回,否则交由oom_malloc()来处理。
2、具有new-handler的oom_malloc
template <int inst>
void * __malloc_alloc_template<inst>::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就是由用户设定的new-handler,我们可以通过下面的函数来设定这个句柄:
static void (* set_malloc_handler(void (*f)()))()
{
void (* old)() = __malloc_alloc_oom_handler;//old handler of outing of memory
__malloc_alloc_oom_handler = f;//new handler for hander the out of memory
return(old);
}
是不是看起来oom_malloc也没做什么事情,就是一直在循环申请内存?在一个循环里oom_handler->malloc….就等着某一个时刻成功申请到内存了,就可以返回交差了!!
第二级配置器
我们重点来看看第二级配置器,这才是SGI的经典(个人),我们需要再次知道第二级分配器是怎么“被”工作的,当用户申请的内存大小小于128bytes时,SGI配置器“分线器”就会将这个工作交由第二级分配器来完成。此时,第二级分配器就要开始工作了。第二级分配器的原理较为简单,就是向内存池中申请一大块内存空间,然后按照大小分为16组,(8,16…..128),每一个大小都对应于一个free_list链表,这个链表上面的节点就是可以使用的内存空间,需要注意的是,配置器只能分配8的倍数的内存,如果用户申请的内存大小不足8的倍数,配置器将自作主张的为用户上调到8的倍数,所以有时候你明明超出边界了但是系统却没有阻止你的行为的时候,你应该知道是SGI空间配置器救了你。
当然,第二级配置器的原理远没有这么简单,上面我们说到第二级配置器如何管理内存,现在,我们要开始为用户分配内存和回收内存了。
二级配置器分配过程:当用户申请一个内存后,第二级配置器首先将这个空间大小上调到8的倍数,然后找到对应的free_list链表,如果链表尚有空闲节点,那么就直接取下一个节点分配给用户,如果对应链表为空,那么就需要重新申请这个链表的节点,默认为20个此大小的节点。如果内存池已经不足以支付20个此大小的节点,但是足以支付一个或者更多的该节点大小的内存时,返回可完成的节点个数。如果已经没有办法满足该大小的一个节点时,就需要重新申请内存池了!所申请的内存池大小为:2*total_bytes+ROUND_UP(heap_size>>4),total_bytes是所申请的内存大小,SGI将申请2倍还要多的内存。为了避免内存碎片问题,需要将原来内存池中剩余的内存分配给free_list链表。如果内存池申请内存失败了,也就是heap_size不足以支付要求时,SGI的次级配置器将使用最后的绝招–>查看free_list数组,查看是否有足够大的没有被使用的内存,如果这些办法都没有办法满足要求时,只能调用第一级配置器了,我们需要注意,第一级配置器虽然是用malloc来分配内存,但是有new-handler机制(out-of-memory),如果无法成功,只能抛出bad_alloc异常而结束分配。
上面说到的配置器“分线器”,其实就是次级配置器的入口,如果次级配置器发现所需要分配的内存大于128bytes时,就会交由第一级配置器去完成,否则由自己完成。
下面,我们就来看看这个次级配置器是怎么实现的:
enum {__ALIGN = 8};//小型区域的上调边界,用户如果申请30bytes大小内存,系统将返回32bytes给用户
enum {__MAX_BYTES = 128};//小型区域的上限,超过这个大小将直接由第一级配置器直接配置内存
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};//free-list链表的个数,从8一直到128,总共需要16个链表维护每个级别大小的链表
我相信不需要说什么就可以明白,上面的代码是在定义最小河最大的空间大小,和需要的链表的个数,为什么要定义这个呢?
也就是说,如果你觉得SGI STL的配置器过于频繁的调用次级配置器,那么你可以修改进入次级配置器的条件,比如可以修改成16-128,或者32-256等等。我们需要明白的事情是,虽然次级配置器解决了内存碎片的问题,但是给内存管理带来了额外的负担,有可能需要不断去调整free_list,甚至去反过来调用第一级配置器,所以如果你觉得进入配置器的区间不够合理的话,可以自己调整!(但别玩死了!)。
下面这个函数返回离bytes最近的可以被8整除的整数,上取整。
static size_t ROUND_UP(size_t bytes)
{
return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
}
下面我们看看free_list的节点的数据结构:
union obj
{
union obj * free_list_link;
char client_data[1]; /* The client sees this. */
};
如此巧妙地运用union来管理节点,如果还没有被分配,那么free_list_link有效,如果已经分配给用户,那么client_data[1]有效。不会造成多余的浪费!
下面这个定义就是我们所说的free_list链表数组,每一个数组元素都将对应一个链表:
static obj * __VOLATILE free_list[];
下面这个函数将找到所申请的内存所对应的空闲链表,注意,SGI将用户的内存申请大小上调至8的倍数:
static size_t FREELIST_INDEX(size_t bytes)
{
return (((bytes) + __ALIGN-1)/__ALIGN - 1);
}
次级配置器将会使用内存池,所以下面给出内存池的区间。
static char *start_free; //内存池的开始位置
static char *end_free; //内存池的结束位置
下面就是激动人心的内存分配函数了,该函数将首先判断区间大小,如果大于128bytes,就调用第一级配置器,小于128bytes就寻找对应的free_list链表,如果有可用的,直接拿,如果没有,则调用函数refill来重新分配节点(20个)。
static void * allocate(size_t n){...}
有申请就需要回收内存,下面就是次级配置器的空间释放函数,该函数将判断区间大小,大于128bytes直接调用第一级配置器来回收内存,小于128bytes则回收到相应的free_list链表中,较为简单。
static void deallocate(void *p, size_t n){...}
下面我们来看看refill函数,这个函数完成重新给链表分配节点的工作。新的空间将从内存池中取到,(内存池由chunk_alloc管理),缺省的话只取20个新节点,但有可能小于20个节点。
void *alloc::refill(size_t bytes)
下面就是最为重要的也是最难的内存池了。当然,下面这个函数只是从内存池中取出空间给free_list用而已。不过整个过程很曲折,很经典,需要仔细品味,很棒的设计!
char *alloc::chunk_alloc(size_t bytes,size_t& nobjs){...}
配置器的流程
讲完了上面的内容,我们现在应该很清楚次级配置器是怎么工作的了,其实,我们可以看到第一级配置器就是为了配合第二级配置器而存在的,重点在于次级配置器的设计原理。最后我们来走一遍配置器的流程:
Alloc_bytes is the ask memory.
If(Alloc_bytes>128)
First_alloc work start...
Else
Second_alloc work start....
In Second_alloc:
If(free_list!=NULL)
Get an node of this free list and return it
Else
Refill function start to work...
Chunk_alloc function start to work..
In chunk_alloc:
If memory pool’s size bigger wanted,just assign
Else if the memory pool’s size can reach more than 1 nodes
Then alloc...
Else
First allocator start to work...
...call itself ....
源码
alloc.h
#ifndef _ALLOC_H_
#define _ALLOC_H_
/*
* 简单的空间适配器
*/
#include<cstdlib>
namespace EasySTL
{
/*
* 空间是配器
*/
class alloc
{
private:
enum{_ALIGN=8}; //小型区域块上调边界
enum{_MAX_BYTES=128}; //小型区块的上限
enum{_NFREELIST=_MAX_BYTES/_ALIGN}; //freelist的个数16
enum{_NOBJS=20}; //每次增加节点个数
union obj //freelist节点
{
union obj* free_list_next;
char client[1];
};
static obj* volatile free_list[_NFREELIST];
static char* start_free; //内存池开始位置,只在chunk_alloc()中变化
static char* end_free; //内存池结束位置
static size_t heap_size;
//根据需要的区块大小,选择freelist编号,从1开始
static size_t FREELIST_INDEX(size_t bytes)
{
return ((bytes+_ALIGN-1)/_ALIGN-1);
}
//将bytes上调至8的倍数,如果8n<byte<8(n+1),得到8(n+1)
static size_t ROUND_UP(size_t bytes)
{
return ((bytes+_ALIGN-1)&~(_ALIGN-1));
}
//返回一个大小为n的对象,并可能加入大小为n的其他区域到freelist
static void *refill(size_t bytes);
//配置一大块空间,可容纳_NOBJS个大小为size的区块
//如果配置_NOBJS个区块有所不便,_NOBJS可能会降低
static char *chunk_alloc(size_t bytes,size_t& nobjs);
public:
static void *allocate(size_t n);
static void deallocate(void *p,size_t bytes);
static void *reallocate(void *p,size_t old_sz,size_t new_sz);
};
}
#endif
alloc.cpp
#include"alloc.h"
namespace EasySTL
{
char *alloc::start_free=0;
char *alloc::end_free=0;
size_t alloc::heap_size=0;
alloc::obj* volatile alloc::free_list[alloc::_NFREELIST]=
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
//空间配置
void *alloc::allocate(size_t bytes)
{
if(bytes>_MAX_BYTES) //调用一级配置器
{
return malloc(bytes);
}
//选择freelist编号
size_t index=FREELIST_INDEX(bytes);
obj *my_list=free_list[index];
if(my_list==0) //没有可用的freelist,准备重新填充freelist
{
void *r=refill(ROUND_UP(bytes));
return r;
}
//调整freelist,将list后面的空间前移,返回list所指的空间
free_list[index]=my_list->free_list_next;
return my_list;
}
//空间释放
void alloc::deallocate(void *ptr,size_t bytes)
{
if(bytes>_MAX_BYTES)
{
free(ptr);
return;
}
size_t index=FREELIST_INDEX(bytes);
obj* node=static_cast<obj *>(ptr);
node->free_list_next=free_list[index];
free_list[index]=node;
}
//重新分配内存空间
void *alloc::reallocate(void *ptr,size_t old_sz,size_t new_sz)
{
deallocate(ptr,old_sz);
ptr=allocate(new_sz);
return ptr;
}
//重新填充
//返回一个大小为n的对象,并且有时候会为适当的freelist增加节点
//假设bytes已经上调为8的倍数
void *alloc::refill(size_t bytes)
{
//记录获取的区块数目
size_t nobjs=_NOBJS;
//从内存池中取nobjs个区块作为freelist的新节点
char* chunk=chunk_alloc(bytes,nobjs);
obj* volatile *my_list=0;
obj* result;
obj *current_obj=0, *next_obj=0;
//如果只获得一个区块,就分配给调用者,freelist无新节点
if(nobjs==1)
{
return chunk;
}
//准备调整freelist,纳入新节点
my_list=free_list+FREELIST_INDEX(bytes);
//在chunk中建立freelist
result=(obj *)(chunk); //这一块准备返回给客服端
//引导freelist指向新配置的空间(取自内存池)
*my_list=next_obj=(obj *)(chunk+bytes);
//将freelist中的节点串联起来
for(int i=1;;++i) //从1开始,因为第0个返回给客户端
{
current_obj=next_obj;
next_obj=(obj *)((char *)next_obj+bytes);
if(nobjs-1==i)
{
current_obj->free_list_next=0;
break;
}
current_obj->free_list_next=next_obj;
}
return result;
}
//内存池(一大块空闲空间)取空间给freelist使用,bytes已经上调为8的倍数
char *alloc::chunk_alloc(size_t bytes,size_t& nobjs)
{
char *result;
size_t bytes_need=bytes * nobjs; //需求字节数
size_t bytes_left=end_free-start_free; //内存池剩余空间
if(bytes_left>bytes_need) //能满足需求量
{
result=start_free;
start_free+=bytes_need;
return result;
}
else if(bytes_left>=bytes) //不能完全满足需求,能满足一个及以上的区块
{
nobjs=bytes_left/bytes;
bytes_need=nobjs*bytes;
result=start_free;
start_free+=bytes_need;
return result;
}
else //一个区块也无法提供
{
//每次申请两倍的新内存
size_t bytes_to_get=2*bytes_need+ROUND_UP(heap_size>>4);
//试着让内存池中的残余零头还有利用价值
if(bytes_left>0)
{
obj * volatile *my_list=free_list+FREELIST_INDEX(bytes_left);
((obj *)start_free)->free_list_next=*my_list;
*my_list=(obj *)start_free;
}
//调整heap空间,用来补充内存池
start_free=(char *)malloc(bytes_to_get);
if(start_free==0)
{
//heap空间内存不足,malloc分配失败
obj * volatile * my_list=0,*p=0;
//在freelist上寻找未使用的大区域块
for(int i=bytes;i<_MAX_BYTES;i+=_ALIGN)
{
my_list=free_list+FREELIST_INDEX(i);
p=*my_list;
if(p!=0) //freelist中尚有未用区块
{
//调整freelist释放出未用区块
*my_list=p->free_list_next;
start_free=(char *)p;
end_free=start_free+i;
//递归调用自己,为了修正nobjs 任何残余零头都将被编入十大那该的freelist备用
return chunk_alloc(bytes,nobjs);
}
}
end_free=0;
}
heap_size+=bytes_to_get;
end_free=start_free+bytes_to_get;
return chunk_alloc(bytes,nobjs);
}
}
}
allocator.h
#ifdef _ALLOCATOR_H_
#define _ALLOCATOR_H_
/*
*为alloc类封装接口
*/
#include"alloc.h"
#include"construct.h"
#include<cassert>
#include<new>
namespace EasySTL
{
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));
}
};
}
#endif
End