具备次配置力的SGI空间配置器
SGI STL
的配置器与众不同,也与标准规范不同,其名称是alloc
而非allocator
,而且不接受任何参数。换句话说,如果你要在程序中明白采用SGI
配置器,则不能采用标准写法
vector<int, std::allocator<int> > iv;
必须这么写:
vector<int, std::alloc> iv;
SGI STL allocator
未能符合标准规格,这个事实通常不会给我们带来困扰,因为通常我们使用缺省的空间配置器,很少需要自行指定配置器名称,而SGI STL
的每一个容器都已经指定其缺省的空间配置器alloc
。
template <class T, class Alloc = alloc> //缺省使用alloc为配置器
class vector{ ... };
SGI特殊的空间配置器, std::alloc
class Foo { ... };
Foo* pf = new Foo; //配置内存,然后构造对象
delete pf; //将对象析构,然后释放内存
new
内含两阶段操作:(1)调用::operator new
配置内存;(2)调用Foo::Foo()
构造对象内容。
delete
内含两阶段操作:(1)调用Foo::~Foo()
将对象析构,(2)调用::operator delete
释放内存
STL
标准规格告诉我们,配置器定义于<memory>
中,SGI<memory>
内含以下两个文件:
#include <stl_alloc.h> //负责内存空间的配置和释放
#include <stl_construct.h> //负责对象内容的构造和析构
构造和析构基本工具:construct()和destory()
下面是<stl_construct.h>
的部分内容
#include <new.h> //使用placement new
template <class T1, class T2>
inline void construct(T1* p, const T2& value){
new (p) T1(value); //placement new 调用T1::T1(value)
} //将初值设定到指针所指的空间上
//以下是destory()第一版本,接受一个指针
template<class T>
inline void destory(T* pointer){
pointer->~T(); //调用析构函数
}
//以下是destory()的第二版本,接受两个迭代器。此函数设法找出元素的数值型别。
template<class ForwardIterator>
inline void destory(ForwardIterator first, ForwardItertor last){
__destory(first, last, value_type(first)); //value_type(first)获得迭代器所指对象的型别
}
//判断元素的数值型别(value type)是否有trivial destructor
template <class ForwardIterator, class T>
inline void __destory(ForwardIterator first, ForwardIterator last, T*){
typedef typename __type_trait<T>::has_trivial_destructor trivial_destructor;
__destory_aux(first, last, trivial_destructor());
}
//如果元素的数值型别(value type)有non-trivial destructor
template<class ForwardIterator>
inline void
__destory_aux(ForwardIterator first, ForwardIterator last, __false_type){
for(;first < last; ++first)
destory(&*first);
}
//如果元素的数值型别(value type)有trivial destructor
template <class ForwardIterator>
inline void __destory_aux(ForwardIterator, ForwardIterator, __true_type){}
//以下是destory()第二版本针对迭代器为char* 和 wchar_t*的特化版
inline void destory(char*, char*);
inline void destory(wchar_t*, wchar_t*);
空间的配置与释放,std::alloc
对象构造前的空间配置和对象析构后的空间释放,由<stl_alloc.h>
负责
C++内存配置基本操作是::operator new()
,内存释放基本操作是::operator delete()
。这两个全局函数相当于C的malloc()
和free()
函数。SGI
以malloc()
和free()
完成内存的配置和释放。
考虑到小型区块可能造成的内存破碎问题,SGI设计了双层级配置器,第一级配置器直接使用malloc()
和free()
,第二级配置器则视情况采用不同的策略:当配置区块超过128bytes
时,视之为“足够大”,便调用第一级配置器;当配置区块小于128bytes
时,视之为“过小”,为了降低额外负担,便采用复杂的memory pool
整理方式,而不再求助于第一级配置器。 整个设计究竟只开放第一级配置器,或是同时开放第二级配置器,取决于__USE_MALLOC
是否被定义。
#ifdef __USE_MALLOC
...
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc; //令alloc为第一级配置器
#else
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc; //alloc为二级配置器
#endif
其中__malloc_alloc_template
就是第一级配置器,__default_alloc_template
就是二级配置器,alloc
并不接受任何template
型别参数。
无论alloc
被定义为第一级或者第二级配置器,SGI
还为它再包装一个接口如下,使配置器的接口能够符合STL
规格。
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)); }
};
内部4个成员函数其实都是在单纯的转调用,调用传递给配置器(可能使第一级也可能是第二级)的成员函数。这个接口使配置器的单位从bytes
转化为个别元素的大小(sizeof(T))
。SGI STL
容器全都使用这个simple_alloc
接口。
template <class T, class Alloc=alloc> //缺省使用alloc为配置器
class vector{
protected:
typedef simple_alloc<value_type, Alloc> data_allocator;
void deallocate(){
if(...)
data_allocator::deallocate(start, end_of_storage - start);
}
};
第一级配置器__malloc_alloc_template剖析
#if 0
# include <new>
# define __THROW_BAD_ALLOC throw bad_alloc
#elif !defined(__THROW_BAD_ALLOC)
# include <iostream.h>
# define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1)
#endif
//以下是第一级配置器
template<int inst>
class __malloc_alloc_template{
private:
//以下函数用来处理内存不足的情况
//oom:out of memory
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); //第一级配置器直接使用malloc()
//以下无法满足需求时,改用oom_malloc()
if(0 == result)
result = oom_malloc(n);
return result;
}
static void deallocate(void* p, size_t)
{
free(p); //第一级配置器直接使用free()
}
static void* reallocate(void* p, size_t, size_t new_sz)
{
void* result = realloc(p, new_sz); //第一级配置器直接使用realloc()
//以下无法满足需求时,改用oom_realloc()
if(0 == result)
result = oom_realloc(p, new_sz);
return result;
}
//以下仿真C++的set_new_handler(),换句话说,你可以通过它
//指定你自己的out_of_memory handler
static void (* set_malloc_handler(void (*f)()))()
{
void (*old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = f;
return (old);
}
};
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_malloc_handler = __malloc_alloc_oom_handler;
if(0 == my_malloc_handler){ __THROW_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){ __THROW_BAD_ALLOC; }
(*my_malloc_handler)(); //调用处理例程,企图释放内存
result = realloc(n); //再次尝试配置内存
if(result)
return(result);
}
}
第一级配置器以malloc(),free(),realloc()
等C函数执行实际的内存配置,释放,重配置操作,并实现出类似C++ new-handler的机制。是的,它不能直接运用C++ new_handler机制,因为它并没有使用::operator new
来配置内存。
所谓C++ new handler机制就是,你可以要求系统在内存配置需求无法被满足时,调用一个你所指定的函数。换句话说,一旦::operator new
无法完成任务,在丢出std::bad_malloc
异常状态之前,会先调用由客户端指定的处理例程。该处理例程通常被称为new-handler
。new-handler
解决内存不足的做法有特定的模式。
请注意,SGI
第一级配置器的allocate()
和realloc()
都是在调用malloc()
和realloc()
不成功后,改调用oom_malloc()
和oom_realloc()
,后两者都有内循环,不断调用“内存不足处理例程”,期望在某次调用之后,获得足够的内存而圆满完成任务。但如果“内存不足处理例程”并未被客户端设定,oom_malloc()
和oom_realloc()
便调用__THROW_BAD_ALLOC
,丢出bad_alloc
异常信息,或利用exit(1)中止程序。
第二级配置器__default_alloc_template剖析
第二级配置器多了一些机制,避免太多小额区块造成内存的碎片。小额区块带来的其实不仅仅是内存碎片,配置时的额外负担也是一个大问题。
SGI第二级配置器的做法是:如果区块够大,超过128bytes时,就移交第一级配置器处理。当区块小于128bytes时,则以内存池(memory pool)管理,此法又称为次级配置:每次配置一大块内存,并维护对应的自由链表。下次若再有相同大小的内存需求,就直接从free-list中拨出。如果客户端释放小额区块,就由配置器回收到free-lists中。配置器除了负责配置,也负责回收。SGI第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数(例如客户端要求30bytes,就会自动调整到32bytes),并维护16个free-lists,各自管理大小分别为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128bytes的小额区块。
free-lists
节点结构
union obj{
union obj* free_list_link;
char client_data[1];
}
下面是二级配置器的部分实现内容
enum {__ALIGN = 8}; //小型区块的上调边界
enum {__MAX_BYTES = 128 }; //小型区块的上限
enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; //free-lists个数
//以下是第二级配置器
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-list
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 thread, int inst>
char *__default_alloc_template<threads, inst>::start_free = 0;
template<bool thread, int inst>
char *__default_alloc_template<threads, inst>::end_free = 0;
template<bool thread, int inst>
char __default_alloc_template<threads, inst>::heap_size = 0;
template<bool thread, 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()
身为一个配置器,__default_alloc_template
拥有配置器的标准接口函数allocate()
。此函数首先会判断区块大小,大于128bytes
就调用第一级配置器,小于128bytes
就检查对应的free-list
。如果free list
之内有可用的区块,就直接拿来用,如果没有可用区块,就将区块大小上调至8倍数边界,然后调用refill()
,准备为free list
重新填充空间。
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;
};
空间释放函数deallocate()
身为一个配置器,__default_alloc_template
拥有配置器标准接口函数deallocate()
。该函数首先判断区块大小,大于128bytes
就调用第一级配置器,小于128bytes
就找出对应的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;
}
重新填充free lists
先前讨论说过的allocate()
,当它发现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 == nobjs) 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++){
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()
的工作:
//假设size已经适当上调至8的倍数
//注意参数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);
//以下试着让内存池中的残余零头还有利用价值
//把内存池中的残余零头加到free list中
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(p != 0){ //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));
}
}
上述的chunk_alloc()
函数以end_free - start_free
来判断内存池的水量。如果水量充足,就直接调出20个区块返回给free list
。如果水量不足以提供20个区块,但还足够供应一个以上的区块,就拨出这不足20个区块的空间出去。这时候其pass by reference
的nobjs
参数将被修改为实际能够供应的区块数。如果内存池连一个区块空间都无法供应,对客户端显然无法交代,此时需要利用malloc
从heap
中配置内存,为内存池注入源头活水以应付需求。新水量的大小为需求量的两倍,再加上一个随配置次数增加而愈来愈大的附加量。
万一山穷水尽,整个system heap
空间都不够了(以至无法为内存池提供源头活水),malloc()
行动失败,chunk_alloc()
就四处寻找有无“尚有未用区块,且区块够大”的free lists
。找到了就挖一块交出,找不到就调用第一级配置器。第一级配置器也是使用malloc()
来配置内存,但它有out-of-memory
处理机制(类似new-handler
机制),或许有机会释放其他的内存拿来此处使用。如果可以,就成功,否则发出bad_alloc
异常。
SGI
通常以这种方式来使用配置器:
template<class T, class Alloc = alloc> //缺省使用alloc为配置器
class vector{
public:
typedef T value_type;
...
private:
//专属空间配置器,每次配置一个元素的大小
typedef simple_alloc<value_type, Alloc> data_allocator;
...
};
其中第二个template参数所接受的缺省参数alloc,可以是第一级配置器,也可以是第二级配置器。
system heap
空间都不够了(以至无法为内存池提供源头活水),malloc()
行动失败,chunk_alloc()
就四处寻找有无“尚有未用区块,且区块够大”的free lists
。找到了就挖一块交出,找不到就调用第一级配置器。第一级配置器也是使用malloc()
来配置内存,但它有out-of-memory
处理机制(类似new-handler
机制),或许有机会释放其他的内存拿来此处使用。如果可以,就成功,否则发出bad_alloc
异常。
SGI
通常以这种方式来使用配置器:
template<class T, class Alloc = alloc> //缺省使用alloc为配置器
class vector{
public:
typedef T value_type;
...
private:
//专属空间配置器,每次配置一个元素的大小
typedef simple_alloc<value_type, Alloc> data_allocator;
...
};
其中第二个template参数所接受的缺省参数alloc,可以是第一级配置器,也可以是第二级配置器。