空间配置器,听起来高大上,那它到底是什么东西呢?

1.什么是空间配置器?

空间配置器是STL源码中实现的一个小灶,用来应对STL容器频繁申请小块内存空间的问题。他算是一个小型的内存池,以提升STL容器在空间申请方面的效率

image-20221018151529901

2.了解空间配置器

STL以128个字节为分界线,将空间配置器分为了一级和二级

2.1 一级

一级空间配置器中,allocate/deallocate函数实际上就是对malloc/free做了一个简单的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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);
}

//这个函数是malloc失败的时候才会调用的
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);
}
}

当malloc失败的时候,一级空间配置器会抛异常

2.2 二级

在二级空间配置器中,会先判断带申请空间大小是否大于128个字节,如果超过了,则会去调用一级空间配置器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# ifndef __SUNPRO_CC
enum {__ALIGN = 8};
enum {__MAX_BYTES = 128};
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};
# endif
/* n must be > 0 */
static void * allocate(size_t n)
{
obj * __VOLATILE * my_free_list;
obj * __RESTRICT result;

if (n > (size_t) __MAX_BYTES) {
return(malloc_alloc::allocate(n));
}
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;
if (result == 0) {
void *r = refill(ROUND_UP(n));
return r;
}
*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;

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
}

二级空间配置器中,主要的几个成员变量如下

1
2
3
4
5
static obj * __VOLATILE free_list[__NFREELISTS]; //哈希桶
// Chunk allocation state.
static char *start_free;
static char *end_free;
static size_t heap_size;

这里是一个内存池+ 一个哈希桶,内存池的长度为heap_size

image-20221018152617677

哈希桶以8字节对齐,分为了16个桶。最开始的时候,free_list哈希桶是空的

image-20221018155632729

当我们需要小于128个字节空间的时候(以16字节为例),二级空间配置器会直接去上面的内存池当中申请20个16字节的空间,链接到16字节对应的哈希桶的位置(1号桶)

在下面的refill函数中可以看到这一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
int nobjs = 20;//一次性申请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++) {
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);
}

8字节对齐的妙处

假设我们的list单个节点需要12个字节,set单个字节需要16个字节

当list需要空间的时候,二级空间配置器也会去申请16个字节的空间,而不会去申请12个字节。这时候,当list将用完的空间还回来之后,就能拿过去给set用了!

关于哈希桶的特殊处理

当我们用完了申请的空间,准备“释放”的时候,二级空间配置器会将释放回来的空间插入进哈希桶(头插)

这里有一个特殊处理,当用完的空间,或者说是刚申请的空间插入进哈希桶的时候,二及空间配置器会将其强转为一个obj类型,这个类型的前4/8个字节会用来存放一个指针,指向下一个空间,以此构成链表

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

image-20221018160755549

当我们再次需要这部分大小的空间的时候,只需要将哈希桶头指针的指向直接修改,就能链接上下一个空间,并将之前头指针指向的空间拿去用

用的时候也不用管之前在此处存放的指针,毕竟下一次放回来的时候,二级空间配置器会来处理它的


内存池空间不够用了咋办?

之前提到了,二级空间配置器里面有一个小内存池。要是这个内存池用完了,要咋整呢?

二级空间配置器想出来了一种很骚的做法:

  • 假设当前我们需要24字节的空间
  • 对应的桶下面没有空间给我们用
  • 内存池也用光了
  • 但是48字节的桶下面还有空间可以用
  • 直接把这个空间拿过来,拆成两个24链接到24的桶下面!

这样便有效提高了空间利用率,也避免了realloc造成的时间消耗。

当然,要是桶里面也没有多余空间了,那就只能老老实实去扩容了

只能大的拆小的,小的空间不连续无法组成大的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//二级空间配置器里面的reallocate封装
template <bool threads, int inst>
void*
__default_alloc_template<threads, inst>::reallocate(void *p,
size_t old_sz,
size_t new_sz)
{
void * result;
size_t copy_sz;

if (old_sz > (size_t) __MAX_BYTES && new_sz > (size_t) __MAX_BYTES) {
return(realloc(p, new_sz));
}
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);
}

2.3 关于单例的说明

在同一个进程里面,空间配置器只会有一个

但STL库中的空间配置器并没有把自己设计成单例类,而是用了一个间接的做法

1
2
3
4
5
static obj * __VOLATILE free_list[__NFREELISTS]; //哈希桶
// Chunk allocation state.
static char *start_free;
static char *end_free;
static size_t heap_size;

那就是所有成员变量都是static静态的

我们知道,在类和对象中,静态成员变量是属于整个类的,所以它也是一种单例模式!


2.4 空间配置器的优点

  • 空间配置器的时间复杂度是O(1),申请空间的效率很高
  • 空间配置器能解决频繁申请空间导致的内存碎片问题

当我们使用STL容器频繁申请小块空间(比如list)就容易导致堆区中有非常多的小块空间被使用,而无法申请一个大的空间

空间配置器提前申请了一个大块空间再私下管理,可以有效解决这个问题

内存外碎片/内碎片

  • 外碎片:多次申请小块空间造成。有足够内存,但是不连续,无法申请大块内存
  • 内碎片:list只需要12个字节,而空间配置器因为内存对齐而给了它16个字节,浪费了4个字节造成的内存碎片

3.容器和空间配置器

之前学习stl容器的时候,就在定义中看到过这里的alloc默认模板参数

image-20221018164053947

这里默认传的便是stl库中的二级空间配置器

只要你自己写的空间配置器符合stl的接口需求,你便可以将自己的空间配置器传入此处让容器使用!

1
2
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;
typedef __default_alloc_template<false, 0> single_client_alloc;

在list中,alloc又被封装了一层,用来处理节点申请的问题

1
2
3
4
5
6
template <class T, class Alloc = alloc>
class list {
protected:
typedef void* void_pointer;
typedef __list_node<T> list_node;
typedef simple_alloc<list_node, Alloc> list_node_allocator;
1
2
3
4
5
6
7
8
9
10
11
12
13
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)); }
};

随后,当创建节点、销毁节点的时候,list就会调用封装好的simple_alloc来处理空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
link_type get_node() { return list_node_allocator::allocate(); }
void put_node(link_type p) { list_node_allocator::deallocate(p); }
link_type create_node(const T& x) {
link_type p = get_node();
__STL_TRY {
construct(&p->data, x);//调用节点的构造函数
}
__STL_UNWIND(put_node(p));
return p;
}
void destroy_node(link_type p) {
destroy(&p->data);//调用节点的析构函数
put_node(p);
}
  • construct通过定位new显式调用节点的构造函数
  • destroy通过指针来调用析构函数
1
2
3
4
5
6
7
8
9
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);
}

stl_tree.h中可以看到库里面的红黑树也对空间配置器进行了类似的封装

1
2
3
4
5
6
7
8
9
template <class Key, class Value, class KeyOfValue, class Compare,
class Alloc = alloc>
class rb_tree {
protected:
typedef void* void_pointer;
typedef __rb_tree_node_base* base_ptr;
typedef __rb_tree_node<Value> rb_tree_node;
typedef simple_alloc<rb_tree_node, Alloc> rb_tree_node_allocator;
typedef __rb_tree_color_type color_type;

结语

关于空间配置器的内容了解这些就差不多啦!

其实就是通过库里面的这个设计来了解一个提高小空间内存申请效率的方法,感觉还是很666的

有啥问题可以在评论提出哦