STL空间配置器(allocator)在所有容器内部默默工作,负责空间的配置和回收。STL标准为空间配置器定义了标准接口(可见《STL源码剖析》P43)。而具体实现细节则由各编译器实现版本而不同。下面介绍SGI STL中的allocator(实际叫alloc)配置器。SGI STL源码下载地址:http://download.csdn.net/detail/wudaijun/6404589
首先我们来看一个SGI STL中符合标准,名为allocator的空间配置器:
//位于cygwin-b20\include\g++\defalloc.h
#ifndef DEFALLOC_H
#define DEFALLOC_H
#include <new.h>
#include <stddef.h>
#include <stdlib.h>
#include <limits.h>
#include <iostream .h>
#include <algobase.h>
template <class T>
inline T* allocate(ptrdiff_t size, T*) {
set_new_handler(0);
T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
if (tmp == 0) {
cerr << "out of memory" << endl;
exit(1);
}
return tmp;
}
template <class T>
inline void deallocate(T* buffer) {
::operator delete(buffer);
}
template <class T>
class allocator {
public:
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;
pointer allocate(size_type n) {
return ::allocate((difference_type)n, (pointer)0);
}
void deallocate(pointer p) { ::deallocate(p); }
pointer address(reference x) { return (pointer)&x; }
const_pointer const_address(const_reference x) {
return (const_pointer)&x;
}
size_type init_page_size() {
return max(size_type(1), size_type(4096/sizeof(T)));
}
size_type max_size() const {
return max(size_type(1), size_type(UINT_MAX/sizeof(T)));
}
};
class allocator<void> {
public:
typedef void* pointer;
};
这份allocator很简单,只是对::operator new和::operator delete的简单封装而已。上面的allocator完成对内存的分配与回收,但是并没有对象的构造,对象的构造是在defalloc同目录下的stl_construct.h中的函数完成的,之所以将对象的分配和构造(析构和回收)分开,是为了提高效率,避免不必要的构造和对对象构造析构的优化,在使用上面的allocator配置器时,内存的分配和回收分别由allocator::allocate()和allocator::deallocate()负责,而对象的构造和析构则由::construct()和destroy()负责(位于stl_construct.h),这种分工使整个空间配置更灵活高效。
但是SGI STL并未使用上面的allocator,因为它效率不高,保留它是为了与HP STL风格兼容。SGI STL真正使用的是一个叫alloc的配置器,它在很多方面都与STL规范不同,但是性能卓越。将alloc和stl_construct中的construct(),destroy()结合,成为了SGI STL的独门利器。
首先来看看相对简单的对象构造和析构工具: construct()和destroy()
//cygwin-b20\include\g++\stl_construct.h
#ifndef __SGI_STL_INTERNAL_CONSTRUCT_H
#define __SGI_STL_INTERNAL_CONSTRUCT_H
#include <new.h>
__STL_BEGIN_NAMESPACE
template <class T>//析构单个元素
inline void destroy(T* pointer) {
pointer->~T();
}
template <class T1, class T2>
inline void construct(T1* p, const T2& value) {
new (p) T1(value); //布局new(placement new) 在p地址处调用T1构造函数构造对象
}
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) {}
template <class ForwardIterator, class T>
inline void __destroy(ForwardIterator first, ForwardIterator last, T*) {
//通过元素型别来判断析构函数是否无关紧要(trivial) 并调用对应的函数进行析构
typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
__destroy_aux(first, last, trivial_destructor());
}
template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last) {
__destroy(first, last, value_type(first));//通过泛型的类型识别技术来得到元素类型
}
inline void destroy(char*, char*) {}
inline void destroy(wchar_t*, wchar_t*) {}
__STL_END_NAMESPACE
#endif /* __SGI_STL_INTERNAL_CONSTRUCT_H */
这里面的构造函数construct()只是调用placement new,关于placement new和::operator new以及new运算符的关系和区别我前面专门有博客阐述。
而析构函数destroy就比较巧妙了。它有两个重载,一个销毁指定元素,这个只需要调用其析构函数即可,另一个接受first和last两个迭代器,用于销毁迭代器内的所有元素。这时候destroy并不是盲目地对这个范围内所有元素依次调用析构函数,为了效率起见,它先通过泛型的类型解析从而在_destory()中得到元素类型,再通过元素类型的_type_traits<T>::has_trivial_destructor trivial_destructor来判断元素类型的析构函数是否是无关紧要(trivial)的,如果是,那么trivial_destructor()值为true_type,否则为false_type,注意,这里的true_type,false_type是一种类型而不是值,因此再通过一个_destroy_aux()的重载即可对两种情况分别处理。如果是false_type,也就是元素类型的析构函数是必要的,那么就老老实实依次调用每个元素的析构函数,否则,什么也不做。这对于销毁大范围的元素来说,如果析构函数无关痛痒,那么效率上将会有很大提高。
接下来,看看std::alloc又是如何以高效率淘汰前面的allocator的。
简单来说,alloc主要在如下方面超越了allocator
1.通过内存池技术提升了分配效率:
2.对小内存频繁分配所可能造成的内存碎片问题的处理
3.对内存不足时的处理
alloc采用了两级配置器,其中一级配置器直接从堆中获取和释放内存(通过malloc),效率和前面的allocator相当。二级适配器采用内存池技术,对用户的小区块申请进行了优化,当用户申请大区块时,它将其交予一级配置器。当用户申请小区块时,将于内存池打交道,内存池通过自由链表来管理小区块,当内存池不足时,会一次性向堆中申请足够大的空间。用户可以通过宏来控制使用哪一级配置器(默认为二级配置器)。
在前面的allocator中,内存的分配和释放都使用的是::operator new()和::operator delete()函数,它们的内部其实也是调用C语言的malloc和free来实现的,在alloc中,内存的分配和释放直接使用malloc()和free()两个函数。当用户请求的内存大于128bytes时,则通过malloc和free来配置内存(一级配置器),当请求内存小于128bytes时,则通过内存池来管理内存(二级配置器)。用户可以通过一个_USE_MALLOC宏来控制是否使用二级配置器(即不管请求内存大小,均从堆中分配)。当_USE_MALLOC被定义时,将不使用二级配置器。
用《STL源码剖析》中的两幅图来概览一下两级配置器:
cygwin-b20\include\g++\stl_alloc.h
template <int inst>
class __malloc_alloc_template {
private:
static void *oom_malloc(size_t);// malloc内存不足处理例程
static void *oom_realloc(void *, size_t);// realloc内存不足处理例程
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
static void (* __malloc_alloc_oom_handler)();//函数指针,保存用户定义的内存不足处理函数
#endif
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内存不足处理例程
static void (* set_malloc_handler(void (*f)()))()
{
void (* old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = f;
return(old);
}
};
// malloc_alloc out-of-memory handling
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
template <int inst>
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
#endif
template <int inst>
void * __malloc_alloc_template<inst>::oom_malloc(size_t n)
{
void (* my_malloc_handler)();
void *result;
for (;;) {//反复调用用户定义(通过set_malloc_hander函数)的内存不足函数
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>//和oom_malloc类似
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(p, n);
if (result) return(result);
}
}
第一层配置器直接通过malloc来分配内存,并在此之上建立内存不足处理例程。当allocate()通过malloc分配内存失败时,它会调用内存不足处理例程oom_malloc(),oom_malloc会不断调用用户定义的处理函数(由_malloc_alloc_oom_handler保存,通过set_malloc_handler()设置),并且尝试分配内存。如果用户未定义处理函数,则抛出异常。在这个过程中,设计和设置内存不足处理函数都是用户的责任。
第二层配置器通过内存池和自由链表(free_list)使得用户申请<128bytes的内存更快捷并造成更少的内存碎片。它每次配置一块大的内存交给自由链表维护,用户每次申请的内存都从链表中获取,并且在释放时交还与自由链表。SGI STL将用户申请的128bytes以内的内存自动上调到8的倍数,并维护16个free_list,各free_list负责的大小分别为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128bytes。比如用户申请10bytes内存,将被上调到16bytes,并且从负责管理16bytes内存的free_list中取出一个节点(也就是一块内存),如果free_list中当前没有节点,则从内存池中分配足够内存,并且填充到free_list。
Free_list的节点结构如下:
union obj
{
union obj* free_list_link; //用于维护空闲内存,指向下一个空闲节点
char client_data[1]; //用于用户使用
}
注意节点是union类型的,当节点空闲(未被分配时),节点使用第一字段指向下一个空闲节点,当节点被分配后,用户可以直接使用第二字段,这样自由链表就不会因为free_list_link指针而造成内存的浪费(当节点被分配出去后,free_list_link指针就没有意义了)。如下图
下面是主要代码:
template <bool threads, int inst>
class __default_alloc_template {
private:
// Really we should use static const int x = N
// instead of enum { x = N }, but few compilers accept the former.
enum {__ALIGN = 8};
enum {__MAX_BYTES = 128};
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};//16
//上调到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]; /* The client sees this. */
};
private:
//自由链表数组 各自管理不同大小的内存节点
static obj * __VOLATILE free_list[__NFREELISTS];
//找到bytes所属的free_list
static size_t FREELIST_INDEX(size_t bytes) {
return (((bytes) + __ALIGN-1)/__ALIGN - 1);
}
// Returns an object of size n, and optionally adds to size n free list.
static void *refill(size_t n);
// Allocates a chunk for nobjs of size "size". nobjs may be reduced
// if it is inconvenient to allocate the requested number.
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:
/* n must be > 0 */
static void * allocate(size_t n)
{
obj * __VOLATILE * my_free_list;
obj * __RESTRICT result;
//如果>128bytes 就使用一级配置器
if (n > (size_t) __MAX_BYTES) {
return(malloc_alloc::allocate(n));
}
my_free_list = free_list + FREELIST_INDEX(n);//找到对应free_list
result = *my_free_list;//直接使用最前面那个节点
if (result == 0) {//如果没有节点了
void *r = refill(ROUND_UP(n));//重新填充free_list
return r;
}
//调整free_list
*my_free_list = result -> free_list_link;
return (result);
};
/* p may not be 0 */
static void deallocate(void *p, size_t n)
{
obj *q = (obj *)p;
obj * __VOLATILE * my_free_list;
//如果>128bytes 使用以及配置器回收
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;
}
static void * reallocate(void *p, size_t old_sz, size_t new_sz);
} ;
上面是部分源码,删掉了多线程的那部分代码。主要函数有allocate()和deallocate()(STL标准规定空间配置器必须有allocate()和deallocate()接口)。在allocate()中,先判断用户申请的内存大小,如果大于128bytes,则交由一级配置器。否则自由链表中找到对应大小的free_list,然后取出第一个节点并返回。如果对应free_list没有节点,那么通过refill()函数从内存池分配足够节点填充free_list并返回给客户端。refill()代码如下:
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
int nobjs = 20;
//尝试从内存池分配nobjs个大小为n的区块,如果内存池不够,
//实际分配个数由nobjs指出 注意:nobjs参数类型为引用
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);
my_free_list = free_list + FREELIST_INDEX(n);
//将返回区块的第一块返回给客户端 其余的填充到free_list
/* Build free list in chunk */
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);
}
可以看到,与内存池打交道的任务实际是chunk_alloc()完成的:
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);
// Try to make use of the left-over piece.
if (bytes_left > 0) {//先把剩余的一点内存放入合适的free_list
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;
// Try to make do with what we have. That can't
// hurt. We do not try smaller requests, since that tends
// to result in disaster on multi-process machines.
//从比请求的区块更大的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) {
*my_free_list = p -> free_list_link;
start_free = (char *)p;
end_free = start_free + i;
return(chunk_alloc(size, nobjs));
// Any leftover piece will eventually make it to the
// right free list.
}
}
end_free = 0; // In case of exception.
//尝试通过一级配置器获得内存(借助内存不足处理例程)
start_free = (char *)malloc_alloc::allocate(bytes_to_get);
// This should either throw an
// exception or remedy the situation. Thus we assume it
// succeeded.
}
//修正内存池水位
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
//修正nobjs
return(chunk_alloc(size, nobjs));
}
}
最后随便看看realloc()的代码:
__default_alloc_template<threads, inst>::reallocate(void *p,
size_t old_sz,
size_t new_sz)
{
void * result;
size_t copy_sz;
//如果分配区块大小>128bytes 交予一级配置器
if (old_sz > (size_t) __MAX_BYTES && new_sz > (size_t) __MAX_BYTES) {
return(realloc(p, new_sz));
}
//否则用二级配置器的allocate分配
if (ROUND_UP(old_sz) == ROUND_UP(new_sz)) return(p);
result = allocate(new_sz);
copy_sz = new_sz > old_sz? old_sz : new_sz;
memcpy(result, p, copy_sz);
deallocate(p, old_sz);
return(result);
}
到此,整个二级配置器就完工了。现在从客户端申请内存开始来理一下流程:
当客户端第一次申请30bytes内存时,首先被上调到32bytes,在allocate()中,找到管理32bytes块的free_list[3],发现它为空,因此调用chunk_alloc(32,20)从内存池中取出内存,但是此时内存池也是空的,因此在chunk_alloc()中,调用malloc()分配了20*2=40个32bytes区块,其中1个交给客户端,19个填充到free_list,剩余20*32bytes内存留给内存池。当用户再申请60bytes时,上调到64bytes,然后找到free_list[7]发现也是空的,然后调用chunk_alloc(64,20),但是此时内存池只有32*20bytes空间,只够10个64bytes区块,因此内存池返回这10个区块,在refill()中,1个返回给客户端,剩余9个交给free_list[7]维护。如果用户再申请90bytes内存,此时仍然会调用chunk_alloc(96,20),此时内存池又空了,会通过malloc()申请(20*2+n)*96bytes的空间(其中n是一个附加量,内存池malloc()次数越多,该值越大),之后1个交给客户端,19个交给free_list[11],剩余(20+n)*96bytes内存留给内存池。。。。
如果内存池中malloc()失败,即系统堆也不够了,那么先尝试从free_list中找到一个比申请区块更大的空闲区块,如果有,则放入内存池,然后递归调用自身。如果没有找到,最后尝试一级配置器,或许内存不足处理例程能做些什么,再否则就抛出bad_alloc异常。