本文讲解SGI STL空间配置器的第二级配置器。
相比第一级配置器,第二级配置器多了一些机制,避免小额区块造成内存的碎片。不仅仅是碎片的问题,配置时的额外负担也是一个大问题。因为区块越小,额外负担所占的比例就越大。
额外负担是指动态分配内存块的时候,位于其头部的额外信息,包括记录内存块大小的信息以及内存保护区(判断是否越界)。要想了解详细信息,请参考MSVC或者其他malloc实现。
图1 所求任何一块内存,都得有一些税要缴给系统
SGI STL第二级配置器具体实现思想 |
如下:
下面详细节介绍内存池管理技术。
在第二级配置器中,小额区块内存需求大小都被上调至8的倍数,比如需要分配的大小是30bytes,就自动调整为32bytes。系统中总共维护16个free-lists,各自管理大小为8,16,...,128bytes的小额区块。
为了维护链表,需要额外的指针,为了避免造成另外一种额外的负担,这里采用了一种技术:用union表示链表节点结构:
union obj {
union obj * free_list_link;//指向下一个节点
char client_data[1]; /* The client sees this. */
};
union能够实现一物二用的效果,当节点所指的内存块是空闲块时,obj被视为一个指针,指向另一个节点。当节点已被分配时,被视为一个指针,指向实际区块。
以下是第二级配置器总体实现代码概览:
template <bool threads, int inst>
class __default_alloc_template {
private:
// 实际上我们应该使用 static const int x = N
// 来取代 enum { x = N }, 但目前支持该性质的编译器还不多。
# ifndef __SUNPRO_CC
enum {__ALIGN = 8};
enum {__MAX_BYTES = 128};
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};
# endif
//注意此处的位运算技巧
static size_t ROUND_UP(size_t bytes) {
return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
}
__PRIVATE:
//注意此处的union技巧
union obj {
union obj * free_list_link;
char client_data[1]; /* The client sees this. */
};
private:
# ifdef __SUNPRO_CC
static obj * __VOLATILE free_list[];
// Specifying a size results in duplicate def for 4.1
# else
static obj * __VOLATILE free_list[__NFREELISTS];
# endif
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;
static char *end_free;
static size_t heap_size;
/* n must be > 0 */
static void * allocate(size_t n){...}
/* p may not be 0 */
static void deallocate(void *p, size_t n){...}
static void * reallocate(void *p, size_t old_sz, size_t new_sz);
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[
# ifdef __SUNPRO_CC
__NFREELISTS
# else
__default_alloc_template<threads, inst>::__NFREELISTS
# endif
] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
空间配置函数allocate() |
具体实现如下:
代码如下:
/* 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));
}
//否则,从free list中分配
my_free_list = free_list + FREELIST_INDEX(n);
// Acquire the lock here with a constructor call.
// This ensures that it is released in exit or during stack
// unwinding.
# ifndef _NOTHREADS
/*REFERENCED*/
lock lock_instance;
# endif
result = *my_free_list;
//对应的空闲链表上没有客空间,调用refill函数
if (result == 0) {
void *r = refill(ROUND_UP(n));
return r;
}
*my_free_list = result -> free_list_link;
return (result);
};
这里需要注意的是,每次都是从对应的free list的头部取出可用的内存块。
图示如下:
图一 从freelist取出空闲区块示意图
refill()-为free list填充空间 |
当发现对应的free list没有可用的空闲区块时,就需要调用此函数重新填充空间。新的空间将取自于内存池。内存池的管理后面会讲到。
缺省状况下取得20个新区块,但是如果内存池空间不够,取得的节点数就有可能小于20.下面是SGI STL中的源代码:
/* Returns an object of size n, and optionally adds to size n free list.*/
/* We assume that n is properly aligned. */
/* We hold the allocation lock. */
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
int nobjs = 20;
char * chunk = chunk_alloc(n, nobjs);
obj * __VOLATILE * my_free_list;
obj * result;
obj * current_obj, * next_obj;
int i;
if (1 == nobjs) return(chunk);
my_free_list = free_list + FREELIST_INDEX(n);
/* Build free list in chunk */
result = (obj *)chunk;
*my_free_list = next_obj = (obj *)(chunk + n);
for (i = 1; ; i++) {//将各节点串接起来(注意,索引为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);
}
chunk_alloc-从内存池中取空间供free list使用 |
具体实现思想如下:
源代码如下:
/* We allocate memory in large chunks in order to avoid fragmenting */
/* the malloc heap too much. */
/* We assume that size is properly aligned. */
/* We hold the allocation lock. */
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;
//内存池剩余内存满足20个区块的需求量,直接取出
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.回收零头空间,编入free list 头部
if (bytes_left > 0) {
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;
}
//调用malloc分配空间补充内存池
start_free = (char *)malloc(bytes_to_get);
if (0 == start_free) { //malloc分配失败,搜寻freelist尝试获取空间编入内存池
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.
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.
}
//malloc分配空间成功
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
return(chunk_alloc(size, nobjs));
}
}
注意:从heap中配置内存时,配置的大小为需求量的两倍再加上一个随配置次数逐渐增加的附加量。
内存池实例演示:
图二 内存池实例演示
程序一开始,客户调用chunk_alloc(32,20),因为此时内存池和freelist空间均不够,于是调用malloc从heap配置40个32bytes区块,其中一个供使用,另一个交给free_list[3]维护。剩余的20个留给内存池。接下来调用chunk_alloc(64,20), 此 时 free_list[7]空空如也,必须向内存池要求支持。内存池只够供应 (32*20)/64=10 个64bytes区块,就把这 10 个区块传回,第 1 个交给客端,余 9个由 free_list[7] 维护。此时内存池全空。接下来再呼叫chunk_alloc(96, 20),此时 free_list[11] 空空如也,必须向内存池要求支持,而记忆池此时也是空的,于是以malloc()配 置 40+n(附加量)个96bytes 区块,其中第 1 个交出,另 19 个交给 free_list[11] 维护,余20+n(附加量)个区块留给记忆池……。
万一山穷水尽,整个system heap 空间都不够了(以至无法为记忆池注入活水源 头),alloc()行动失败,chunk_alloc()就到处寻找有无可用区块,且区块够大之freelists。找到的话就挖一块交出,找不到的话就调用第一级配 置器。第一级配置器其实也是使用malloc()来配置内存,但它有 out-of-memory 处理机制(类似new-handler 机制),或许有机会释放其它的内存拿来此处使用。 如果可以,就成功,否则发出bad_alloc异常。
deallocate()-空间释放函数 |
SGI STL源码:
/* p may not be 0 */
static void deallocate(void *p, size_t n)
{
obj *q = (obj *)p;
obj * __VOLATILE * my_free_list;
if (n > (size_t) __MAX_BYTES) {
malloc_alloc::deallocate(p, n);
return;
}
my_free_list = free_list + FREELIST_INDEX(n);
// acquire lock
# ifndef _NOTHREADS
/*REFERENCED*/
lock lock_instance;
# endif /* _NOTHREADS */
q -> free_list_link = *my_free_list;
*my_free_list = q;
// lock is released here
}
STL源码分析