【转】STL空间配置器
STL空间配置器(allocator)在所有容器内部默默工作,负责空间的配置和回收。STL标准为空间配置器定义了标准接口(可见《STL源码剖析》P43)。而具体实现细节则由各编译器实现版本而不同。下面介绍SGI STL中的allocator(实际叫alloc)配置器。SGI STL源码下载地址:http://download.csdn.net/detail/wudaijun/6404589
一个简单的allocator配置器
首先我们来看一个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();
T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
if (tmp == ) {
cerr << "out of memory" << endl;
exit();
}
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));
}
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(), size_type(/sizeof(T)));
}
size_type max_size() const {
return max(size_type(), 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()
首先来看看相对简单的对象构造和析构工具: 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
接下来,看看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 ( == 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 ( == 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)() = ;
#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 ( == 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 ( == 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[]; //用于用户使用
}
注意节点是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 = };
enum {__MAX_BYTES = };
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};//16 //上调到8的倍数
static size_t ROUND_UP(size_t bytes) {
return (((bytes) + __ALIGN-) & ~(__ALIGN - ));
}
__PRIVATE:
union obj {
union obj * free_list_link;
char client_data[]; /* 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-)/__ALIGN - );
} // 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 == ) {//如果没有节点了
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 = ;
//尝试从内存池分配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 ( == 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 = ; ; i++) {
current_obj = next_obj;
next_obj = (obj *)((char *)next_obj + n);
if (nobjs - == i) {
current_obj -> free_list_link = ;
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 = * total_bytes + ROUND_UP(heap_size >> );
// Try to make use of the left-over piece.
if (bytes_left > ) {//先把剩余的一点内存放入合适的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 ( == 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 ( != 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 = ; // 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异常。
转自:http://blog.csdn.net/wudaijun/article/details/12748471
【转】STL空间配置器的更多相关文章
- stl空间配置器线程安全问题补充
摘要 在上一篇博客<STL空间配置器那点事>简单介绍了空间配置器的基本实现 两级空间配置器处理,一级相关细节问题,同时简单描述了STL各组件之间的关系以及设计到的设计模式等. 在最后,又关 ...
- STL空间配置器
1.什么是空间配置器? 空间配置器负责空间配置与管理.配置器是一个实现了动态空间配置.空间管理.空间释放的class template.以内存池方式实现小块内存管理分配.关于内存池概念可以点击:内存池 ...
- 咬碎STL空间配置器
STL空间配置器 一.开场白: 给我的感觉就是,了解是空间配置器的功能,是那么的明了:在看原理,我还是很开心:接下来是360度大转变: 那么长的变量或者函数命名.那么多的宏.不爽,不过,遇上我这种二货 ...
- STL空间配置器那点事
STL简介 STL(Standard Template Library,标准模板库),从根本上说,STL是一些“容器”的集合,这些“容器”有list,vector,set,map等,STL也是算法和其 ...
- STL空间配置器解析和实现
STL空间配置器的强大和借鉴作用不言而喻,查阅资料,发现了Dawn_sf已经对其有了极其深入和详细的描述,所以决定偷下懒借用其内容,只提供自己实现STL空间配置器的源码,具体解析内容参考:(一)STL ...
- 【陪你系列】5 千字长文+ 30 张图解 | 陪你手撕 STL 空间配置器源码
大家好,我是小贺. 点赞再看,养成习惯 文章每周持续更新,可以微信搜索「herongwei」第一时间阅读和催更,本文 GitHub https://github.com/rongweihe/MoreT ...
- STL——空间配置器(构造和析构基本工具)
以STL的运用角度而言,空间配置器是最不需要介绍的东西,它总是隐藏在一切组件(更具体地说是指容器,container)的背后,默默工作,默默付出.但若以STL的实现角度而言,第一个需要介绍的就是空间配 ...
- STL空间配置器、vector、list、deque、map复习
本文写于2017-03-03,从老账号迁移到本账号,原文地址:https://www.cnblogs.com/huangweiyang/p/6440830.html STL的六大组件:容器.算法.迭代 ...
- STL——空间配置器(SGI-STL)
一. 空间配置器标准接口 参见<STL源码剖析>第二章-2.1.<memory>文件. 二.具备次配置力的SGI空间配置器 1. SGI STL的配置器与众不同,也与标准规范不 ...
随机推荐
- Chp17: Moderate
17.1 swap a number in place.(without temporary variables) a = a ^ b; b = a ^ b; a = a ^ b; 17.3 Writ ...
- java核心技术记录之集合
java库中的具体集合: 集合类型 描述 ArrayList 一种可以动态增长和缩减的索引序列 LinkedList 一种可以在任何位置进行高效地插入和删除操作的有序序列 ArrarDeque 一种用 ...
- C# MessageBox 用法大全(转)
C# MessageBox 用法大全 http://www.cnblogs.com/Tammie/archive/2011/08/05/2128623.html 我们在程序中经常会用到MessageB ...
- hdu 1376 Octal Fractions
刚开始做这题时,用的是0.75[8]=(7/8+5/64)[10]这个,但是总是WA…………无语了…… 后来看别人的解题报告,知道了另外一个就是0.75[8]=((5/8+7)/8)[10],从低位向 ...
- 李洪强iOS开发之最全App上架流程
在上架App之前想要 真机测试的同学 请查看 iOS- 最全的真机测试教程 里面包含怎么让多台电脑同时 上架App和同时真机调试.P12文件的使用详解 准备 开发者账号 完工的项目 上架步骤 一.创建 ...
- 朴素贝叶斯方法(Naive Bayes Method)
朴素贝叶斯是一种很简单的分类方法,之所以称之为朴素,是因为它有着非常强的前提条件-其所有特征都是相互独立的,是一种典型的生成学习算法.所谓生成学习算法,是指由训练数据学习联合概率分布P(X,Y ...
- 【mongoDB高级篇①】聚集运算之group,aggregate
group 语法 db.collection.group({ key:{field:1},//按什么字段进行分组 initial:{count:0},//进行分组前变量初始化,该处声明的变量可以在以下 ...
- Executing Raw SQL Queries using Entity Framework
原文 Executing Raw SQL Queries using Entity Framework While working with Entity Framework developers m ...
- VS2010/MFC编程入门教程之目录和总结
鸡啄米的这套VS2010/MFC编程入门教程到此就全部完成了,虽然有些内容还未涉及到,但帮助大家进行VS2010/MFC的入门学习业已足够.以此教程的知识为基础,学习VS2010/MFC较为深入的内容 ...
- BSD历史
在UNIX问世之后(关于UNIX来历史这里不详述),UNIX拥有者AT&T公司以低廉甚至免费的许可将Unix源码授权给学术机构做研究或教学之用. 加州大学伯克利分校就是其中之一.在得到源码后, ...