STL 内存空间的分配和回收
STL中控制内存空间的分配和回收的内容在<stl_alloc.h>中,SGI对这部分的设计哲学如下:
- 向system heap中获得空间
- 考虑内存不足时的做法
- 考虑小内存造成的内存碎片问题
- 考虑多线程情况
不过在阅读书和源码的过程中都先忽略了多线程的情况,所以这边也先不考虑多线程的情况,后面看看有没有机会看看多线程中时如何实现的。
SGI 在设计了双层的空间适配器,第一级配置器直接使用malloc和free进行内存的分配和回收。第二级配置器使用内存池的方式对内存对象进行管理,默认将128字节作为分割。大于128字节的内存直接由一级分配器进行分配,小于等于128字节的内存则在内存池内进行分配。通过__USE_MALLOC的宏定义可以决定是否开放第二级配置。
# ifdef __USE_MALLOC
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc; // 将一级分配器设置为 alloc
....
# else
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc; // 将二级分配器设置为alloc
....
# endif
在基础的空间适配器之上,SGI还额外设计包装了一个接口使其适配STL的规范,
template<class _Tp, class _Alloc>
class simple_alloc {
public:
static _Tp* allocate(size_t __n)
{ return 0 == __n ? 0 : (_Tp*) _Alloc::allocate(__n * sizeof (_Tp)); }
static _Tp* allocate(void)
{ return (_Tp*) _Alloc::allocate(sizeof (_Tp)); }
static void deallocate(_Tp* __p, size_t __n)
{ if (0 != __n) _Alloc::deallocate(__p, __n * sizeof (_Tp)); }
static void deallocate(_Tp* __p)
{ _Alloc::deallocate(__p, sizeof (_Tp)); }
};
simple_alloc 的接口都只是在内部将调用都转换成_Alloc的调用,STL的所有容器都使用的是simple_alloc的接口,例如:
template <class _Tp, class _Alloc>
class _Vector_base {
public:
.......
typedef simple_alloc<_Tp, _Alloc> _M_data_allocator;
_Tp* _M_allocate(size_t __n)
{ return _M_data_allocator::allocate(__n); }
void _M_deallocate(_Tp* __p, size_t __n)
{ _M_data_allocator::deallocate(__p, __n); }
};
// 在stl_config 里定义
// define __STL_DEFAULT_ALLOCATOR(T) alloc
template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class vector : protected _Vector_base<_Tp, _Alloc> {
.......
}
所以容器与分配器之间关系如下图:
第一级内存分配器
template <int __inst>
class __malloc_alloc_template {
private:
static void* _S_oom_malloc(size_t);
static void* _S_oom_realloc(void*, size_t);
public:
static void* allocate(size_t __n)
{
void* __result = malloc(__n);
if (0 == __result) __result = _S_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 = _S_oom_realloc(__p, __new_sz);
return __result;
}
static void (* __set_malloc_handler(void (*__f)()))()
{
void (* __old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = __f;
return(__old);
}
};
// 如果使用的一级分配器,则执行以下宏定义
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc;
一级分配器基本上就是对malloc、realloc 和 free进行了简单的包装。只有当malloc和realloc分配内存不足时,会重试调用OOM的处理
template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_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);
}
}
template <int __inst>
void* __malloc_alloc_template<__inst>::_S_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);
}
}
oom 的处理也很简单,就是不断尝试调用OOM的处理函数(需要通过__set_malloc_handler来设置),直到分配成功,或者OOM处理函数不存在为止。
第二级内存分配器
在具体看二级分配器的源码前,先介绍下二级分配器是如何设计的。
二级分配器构建了一个长度为16的数组管理16个链表,每个链表都管理了一部分内存,其中第i个链表管理的是一组内存大小为 (i + 1) * 8 的内存。链表上的每一块内存都会记录下一块内存的地址,形成链表。当分配的内存小于128字节的时候,分配器会找到合适的链表,然后分配一块内存出去;回收的时候也是判断是否128字节,如果小于则会找到合适的位置重新插回链表。大概模型如下图所示:
而大于128字节的部分则直接由一级分配器进行分配。
而为了避免在填充上述链表的时候频繁去向系统申请内存,二级管理器在单次申请的时候,会额外多申请一部分的内存捏在手上不进行分配,等待下一次填充。
源码分析
接下来就要看看源码里是怎么对上述模型进行构建的。
template <bool threads, int inst>
class __default_alloc_template {
private:
// 定义管理的内存大小、链表长度
enum {_ALIGN = 8};
enum {_MAX_BYTES = 128};
enum {_NFREELISTS = 16}; // _MAX_BYTES/_ALIGN
static size_t _S_round_up(size_t __bytes) {
return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1));
}
__PRIVATE:
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
};
private:
static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS];
static size_t _S_freelist_index(size_t __bytes) {
return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1);
}
static char* _S_chunk_alloc(size_t __size, int& __nobjs);
// Chunk allocation state.
static char* _S_start_free;
static char* _S_end_free;
static size_t _S_heap_size;
public:
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);
} ;
__default_alloc_template 就是二级分配器的结构,和一级分配器一致,开放了三个函数用于内存的管理。其中_S_free_list中管理了16个链表,_S_start_free和_S_end_free分别指向预留内存的开始和结束。
allocate
/* __n must be > 0 */
static void* allocate(size_t __n)
{
void* __ret = 0;
// 大于128字节直接用一级分配器分配
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n);
}
else {
//_S_freelist_index会返回对应大小的内存的所在链表下标
_Obj* __STL_VOLATILE* __my_free_list = _S_free_list + _S_freelist_index(__n);
_Obj* __RESTRICT __result = *__my_free_list;
if (__result == 0)
__ret = _S_refill(_S_round_up(__n)); // 如果链表是空的,则需要填充
else {
*__my_free_list = __result -> _M_free_list_link; // 否则从单链表上拆下一快,然后返回
__ret = __result;
}
}
return __ret;
};
填充的过程在后面
deallocate
static void deallocate(void* __p, size_t __n)
{
// 大于128字节的说明是从一级分配器里获得的,需要直接释放
if (__n > (size_t) _MAX_BYTES)
malloc_alloc::deallocate(__p, __n);
else {
// 找到对应链表
_Obj* __STL_VOLATILE* __my_free_list = _S_free_list + _S_freelist_index(__n);
_Obj* __q = (_Obj*)__p;
// 将内存头部写入链表地址,以此插入链表
__q -> _M_free_list_link = *__my_free_list;
*__my_free_list = __q;
}
}
_S_refill 填充内存
在分配过程中如果链表是空的话, 会触发填充的过程。填充主要是向系统或者持有的预留内存上申请一块,然后将申请出来的内存进行切分,形成链表后挂在管理链表的数组里。
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20;
// 申请内存
char* __chunk = _S_chunk_alloc(__n, __nobjs); // __nobjs是引用,
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
// 如果申请到内存只有1个,那么直接返回
if (1 == __nobjs) return(__chunk);
__my_free_list = _S_free_list + _S_freelist_index(__n);
// 将申请到的内存进行切分
__result = (_Obj*)__chunk;
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
for (__i = 1; ; __i++) { // 第0号是需要返回给外部的,所以这边从1开始
__current_obj = __next_obj;
__next_obj = (_Obj*)((char*)__next_obj + __n);
if (__nobjs - 1 == __i) {
__current_obj -> _M_free_list_link = 0;
break;
} else {
__current_obj -> _M_free_list_link = __next_obj; // 每块内存都指向下一块内存,形成链表
}
}
return(__result);
}
_S_chunk_alloc 申请内存
申请内存的过程主要是如下:
- 计算申请内存大小
- 判断预留空间是否足够分配申请的大小,够则返回
- 判断预留空间是否足够分配1个对象的大小,够则返回
- 向系统申请内存,将申请到内存放入预留空间,重新调用函数
- 系统申请失败则查找是否链表上有更大的内存,有则取出给预留空间,重新调用函数
- 到这一步说明实在没内存了,转向一级分配器,尝试OOM处理
template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size,
int& __nobjs)
{
// 计算需要申请的内存数量
char* __result;
size_t __total_bytes = __size * __nobjs;
size_t __bytes_left = _S_end_free - _S_start_free;
// 剩余内存 > 分配的内存
if (__bytes_left >= __total_bytes) {
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
} else if (__bytes_left >= __size) {
// 剩余内存 > 单个对象内存, 但小于目标需求
__nobjs = (int)(__bytes_left/__size);
__total_bytes = __size * __nobjs;
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
} else {
// 一次都不够了,需要重新申请了, heap_size 会逐渐加大
size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
// 如果之前还有剩余需要分配掉
if (__bytes_left > 0) {
// 这边__bytes_left一定小于128字节,所以直接往对应的free_list内插入就行
_Obj* __STL_VOLATILE* __my_free_list = _S_free_list + _S_freelist_index(__bytes_left);
((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
*__my_free_list = (_Obj*)_S_start_free;
}
_S_start_free = (char*)malloc(__bytes_to_get);
// 处理向系统申请内存失败的情况
if (0 == _S_start_free) {
size_t __i;
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __p;
// 从大的freelist里找是否有空闲内存
for (__i = __size; __i <= (size_t) _MAX_BYTES; __i += (size_t) _ALIGN) {
__my_free_list = _S_free_list + _S_freelist_index(__i);
__p = *__my_free_list;
if (0 != __p) {
*__my_free_list = __p -> _M_free_list_link;
_S_start_free = (char*)__p;
_S_end_free = _S_start_free + __i;
// 将free_list中更大的内存拿出来当作_S_start_free的起点,然后重新调用函数进行分配
return(_S_chunk_alloc(__size, __nobjs));
}
}
_S_end_free = 0; // In case of exception. // 到这里说明自己的资源已经确实分配不出来了
_S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get); // 这边会调用一级分配器,这个调用主要是一级分配器内有个OOM的处理,也许可以分配成功,失败的话,也由以及一级分配器抛出OOM的信息
}
_S_heap_size += __bytes_to_get; // 累计获得的内存大小,下次分配会逐渐变大
_S_end_free = _S_start_free + __bytes_to_get; //管理新分配出来的内存
// 有了内存之后重新调用自己
return(_S_chunk_alloc(__size, __nobjs));
}
}
二级分配器全貌
小结
从二级分配器的实现上可以看出,代码是考虑了各个方面的性能。比如避免大量malloc调用,优先申请较大部分的内存回来自己管理,在Python中也有类似的操作,不过STL里malloc申请的内存是逐渐加大,而python一次性就是申请256K的内存回来管理。在给链表上分配内存的时候也和Python有区别,Python一般一次分配4KB内存到链表上,而STL里最多一次分配20个块到对应链表上。
在内存优化上,链表头部保存下一个节点的地址,这一点倒是和Python的一致,不过Python的链表是双向链表,STL里的是单向。