STL源码剖析——空间配置器Allocator#2 一/二级空间配置器

时间:2023-03-09 20:01:45
STL源码剖析——空间配置器Allocator#2 一/二级空间配置器

  上节学习了内存配置后的对象构造行为和内存释放前的对象析构行为,在这一节来学习内存的配置与释放。

  C++的内存配置基本操作是::operator new(),而释放基本操作是::operator delete()。这两个全局函数相当于C的malloc() 和free() 函数。而SGI正是以malloc() 和free() 完成内存的配置与释放。

  考虑到小型区块可能造成的内存破碎问题,SGI设计了两级的空间配置器。第一级直接使用malloc() 和free() ,而第二级则视情况采用不同的策略:当配置区块超过128bytes时,视为足够大,便调用第一级配置器;当配置区块小于128bytes时,视为过小,采用复杂的内存池(memery pool)分配方式,而不再求助于第一级配置器。我的理解是,容器在分配内存时,无论所需区块多大,它都是调用第二级配置器,而在第二级配置器内再做判断是否求助于第一级配置器。我们可以从这段代码中看到端倪,其中第一级配置器类为__malloc_alloc_template,第二级配置器类为 __default_alloc_template:

typedef __malloc_alloc_template<> malloc_alloc;
...
#ifdef __USE_MALLOC
...
typedef malloc_alloc alloc; // 令 alloc 為第一級配置器
...
#else
...
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, > alloc; // 令 alloc 為第二級配置器
...
#endif

  通过测试可知,SGI STL并未定义__USE_MALLOC,所以都是调用第二级配置器。

  不过无论alloc被定义为第一级亦或是第二级配置器,SGI 还为它再包装一个接口如下,使配置器的接口能够符合STL规格:

 template<class T, class Alloc>
class simple_alloc {
public:
static T *allocate(size_t n) { return == n? : (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 ( != n) Alloc::deallocate(p, n * sizeof (T)); }
static void deallocate(T *p) { Alloc::deallocate(p, sizeof (T)); }
};

  其内部的四个成员函数其实都是单纯的转调用,调用泛型Alloc类的成员函数allocate亦或是deallocate来配置与释放内存。而Alloc一般在容器类的定义时就已经指派好了,例如在Vector类定义的开头:

template <class T, class Alloc = alloc>  // 預設使用 alloc 為配置器
class vector {
...
protected:
// 專屬之空間配置器,每次配置一個元素大小
typedef simple_alloc<value_type, Alloc> data_allocator;
...
}

STL源码剖析——空间配置器Allocator#2 一/二级空间配置器

第一级配置器 __malloc_alloc_template

 // 以下是第一級配置器。
// 注意,無「template 型別參數」。「非型別參數」inst 完全沒派上用場。
template <int inst>
class __malloc_alloc_template {
private:
static void *oom_malloc(size_t);
static void *oom_realloc(void *, size_t); #ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
static void (* __malloc_alloc_oom_handler)();
#endif public:
static void * allocate(size_t n)
{
void *result = malloc(n); // 第一級配置器直接使用 malloc()
if ( == result) result = oom_malloc(n);
return result;
}
static void deallocate(void *p, size_t /* n */)
{
free(p); // 第一級配置器直接使用 free()
}
static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)
{
void * result = realloc(p, new_sz); // 第一級配置器直接使用 realloc()
if ( == result) result = oom_realloc(p, new_sz);
return result;
}
// 以下類似 C++ 的 set_new_handler().
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 (;;) { // 不斷嘗試釋放、配置、再釋放、再配置…
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>
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()来分配内存,使用free()释放内存,使用realloc()来重分配内存,并实现了类似C++ new-handler的机制,因为它并非使用::operator new来分配内存,所以不能直接运用C++ new-handler机制。而C++ new-handler机制,大概就是要求系统在内存配置需求无法被满足时,调用一个你所指定的函数。一旦::operator new无法完成任务,在丢出std::bad_alloc异常状态之前,会先调用由客端指定的处理例程。该例程通常就被称为new-handler。

  理解这段源码的难点就在于处理内存不足的情况,在allocate()(realloc()同理)调用malloc()不成功后,会改调用oom_malloc()函数,该函数会尝试不断尝试调用__malloc_alloc_oom_handler,期待在某次调用后能够获得足够的内存而完成任务。但我们可以看到__malloc_alloc_oom_handler默认值是设为0的(41行),就是说如果用户没通过set_malloc_handler(31行)来为__malloc_alloc_oom_handler指定一个处理内存不足的函数,那么在oom_malloc()函数内便会直接调用__THROW_BAD_ALLOC(50行),丢出bad_alloc异常。这是我对上述源码的愚见,如有错误请指出。那么现在剩下的问题只有一个了,new-handler在解决内存不足的问题上有特定的模式,那么如何设计一个new-handler函数呢?这个问题留在日后解决......

  

第二级配置器 __default_alloc_template

  第二级配置器多了一些机制,避免太多小额区块造成内存的碎片,且还会在配置时带来额外的负担,区块越小,额外负担所占的比例就越大:

STL源码剖析——空间配置器Allocator#2 一/二级空间配置器

  SGI第二级配置器的做法是,如果区块够大,超过128bytes时,就移交给第一级配置器处理。否则,情况就有些复杂,需要一一道来:

  首先先介绍第二级配置器用到一个内存管理结构——*链表(free-list),在第二级配置器里会维护16个*链表,每一个*链表管理不同大小的区块,分别为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128bytes,且第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数(例如用户要求30bytes,就自动调整为32bytes,这决定了使用第几号*链表)。STL源码剖析——空间配置器Allocator#2 一/二级空间配置器

  就一个单独的*链表而言,其节点用一个union表示:

  union obj {
  union obj * free_list_link;
  char client_data[]; /* The client sees this. */
};

  里面的联合体指针就是指向下一个节点的指针,那个char是指向实际内存块的指针,采用联合体可以减少内存的消耗,不必专门维护一个指向下一个节点的指针。

  当节点所指的内存块是空闲块时,obj被看做一个指针,指向下一个节点,当节点已经被分配了内存之后,被视为一个指针,指向实际区块(free_list_link自动失效)。

  在一开始,*链表数组全部被初始化为0,当用户需要某大小的区块(例如94bytes)时,寻找16个*链表中适当的一个(则为11号),发现其11号*链表为空,需要分配内存(一般分配40*96bytes大小的内存),在分配内存后会将第一块给用户,剩余一部分(19*96bytes)放进11号*链表,另一部分(20*16bytes)留在内存池。如果是在*链表不为空的情况下,例如当用户再需要一块94bytes大小的区块时,则直接把11号链表的表头取出,取出的表头的值(为free_list_link,指向链表第一块空内存块)便会成为用户申请地址返回给用户给用户,然后把表头的值指向的下一块空块作为新的表头。

 enum {__ALIGN = }; // 小型區塊的上調邊界
enum {__MAX_BYTES = }; // 小型區塊的上限
enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; // free-lists 個數
#endif
// 以下是第二級配置器。
// 注意,無「template 型別參數」,且第二參數完全沒派上用場。
template <bool threads, int inst>
class __default_alloc_template {
private:
//负责将用户需求的内存大小调整为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:
# 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
//以下函数根据区块大小,决定使用第n号free-list。n从0起:
static size_t FREELIST_INDEX(size_t bytes) {
  return (((bytes) + __ALIGN-)/__ALIGN - );
} 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 void *refill(size_t n);  //重新填充链表,下节详述 static char *chunk_alloc(size_t size, int &nobjs);  //填充时用于分配内存,下节详述 // 内存池指针
static char *start_free;
static char *end_free;
static size_t heap_size; template <bool threads, int inst>char *__default_alloc_template<threads, inst>::start_free = ;
template <bool threads, int inst>char *__default_alloc_template<threads, inst>::end_free = ;
template <bool threads, int inst>size_t __default_alloc_template<threads, inst>::heap_size = ;
} template <bool threads, int inst>__default_alloc_template<threads, inst>::obj * __VOLATILE__
default_alloc_template<threads, inst> ::free_list[__NFREELISTS] = {, , , , , , , , , , , , , , , , };

空间配置函数allocate()

 static void * allocate(size_t n)
{
obj * __VOLATILE * my_free_list;
obj * __RESTRICT result;
//大于128就调用第一级配置器
if (n > (size_t) __MAX_BYTES) {
return(malloc_alloc::allocate(n));
}
//寻找16个*链表中适当的那个表头
my_free_list = free_list + FREELIST_INDEX(n);
//表头的值作为该内存块的地址返回给用户
result = *my_free_list;
//倘若*链表为空,准备分配内存填充链表
if (result == ) {
  void *r = refill(ROUND_UP(n)); //将n调整为8的倍数再进行内存分配
  return r;
}
//调整*链表为表头的下一区块
*my_free_list = result -> free_list_link;
return (result);
};

  STL源码剖析——空间配置器Allocator#2 一/二级空间配置器

空间释放函数deallocate():

 static void deallocate(void *p, size_t n)
{
obj *q = (obj *)p; //将要归还的区块转换为联合体obj
obj * __VOLATILE * my_free_list;
if (n > (size_t) __MAX_BYTES) { //大于128bytes就调用第一级配置器
malloc_alloc::deallocate(p, n);
return;
}
//寻找对应的*链表
my_free_list = free_list + FREELIST_INDEX(n);
//将要归还的区块放在表头前,并把它设为表头
q -> free_list_link = *my_free_list;
*my_free_list = q;
}

STL源码剖析——空间配置器Allocator#2 一/二级空间配置器