空间的配置和释放:alloc配置器
stl标准的空间配置为allocator,SGI也实现了这个版本的配置器,但由于其效率不高,SGI STL默认的配置器为alloc。
考虑到小型区块可能造成的内存破碎问题,SGI设计了双层级配置器:当配置区块超过128bytes时,便调用第一级配置器;如果小于128bytes,为降低额外开销overhead(稍后再讲),便采用复杂的memory pool整理方式。
为使得alloc符合STL规格,SGI将Alloc做了一层封装:
第一级配置器 __malloc_alloc_template
template <int __inst>
class __malloc_alloc_template {
private:
// 以下函数将用来处理内存不足的情况
static void* _S_oom_malloc(size_t);
static void* _S_oom_realloc(void*, size_t);
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
static void (* __malloc_alloc_oom_handler)();
#endif
public:
// 第一级配置器直接调用 malloc()
static void* allocate(size_t __n)
{
void* __result = malloc(__n);
// 以下无法满足需求时,改用 _S_oom_malloc()
if (0 == __result) __result = _S_oom_malloc(__n);
return __result;
}
// 第一级配置器直接调用 free()
static void deallocate(void* __p, size_t /* __n */)
{
free(__p);
}
// 第一级配置器直接调用 realloc()
static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
{
void* __result = realloc(__p, __new_sz);
// 以下无法满足需求时,改用 _S_oom_realloc()
if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
return __result;
}
// 以下仿真 C++ 的 set_new_handler(),可以通过它指定自己的 out-of-memory handler
// 为什么不使用 C++ new-handler 机制,因为第一级配置器并没有 ::operator new 来配置内存
// 可能是历史原因
static void (* __set_malloc_handler(void (*__f)()))()
{
void (* __old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = __f;
return(__old);
}
};
当内存分配失败后,将通过out-of-memory handler尝试分配新内存,以_S_oom_malloc为例:
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
// 初值为0,由客户自行设定
template <int __inst>
void (* __malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;
#endif
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);
}
}
C++ new handler机制:你可以要求系统在内存配置需求无法满足时,在抛出std::bad_alloc异常状态前,调用一个你所指定的函数,这个函数就称为new-handler。
第二级配置器 __default_alloc_template
malloc向系统申请内存时,需配置额外负担以管理内存,内存块越小,额外负担所占比例就越大。而在C++中,大部分内存都是小块的(如变量),如果小块内存也直接malloc,将造成大量的空间浪费。
在上图中,蓝色部分为实际数据,绿色部分为字节补齐部分,上下两端的红色部记录内存块的大小(这也是为什么,在free一块内存时,只需要指导这块内存的首地址即可)。红色的部分称为cookie,两个cookie各占4字节。至于为什么上下两端都有,暂时不知道。
因此,STL尽量减少malloc的次数,设置了如下的自由链表free-lists:
free-list的节点结构为:
union _Obj {
union _Obj* _M_free_list_link; // 利用联合体特点
char _M_client_data[1]; /* The client sees this. */
};
free-list有三个值得注意的点:
- union的妙用,可以节省自由链表中指针的额外开销:在当前节点未分配前,节点中的值为下一个节点的地址;当节点分配后,free_list头指针指向它的下一个节点,而它内部的值被替换为用户的data值。=》从这也看出,自由链表从头节点分配效率更高。
- 区块按照8字节对齐:8、16、24、...、128bytes
- 配置器不仅负责区块的分配器,也负责区块的回收,也就是把一个区块挂到对应的free-list
如果自由链表中没有可分配的节点了,将从内存池获取内存并分配节点:
// 配置一大块空间,可容纳nobjs个大小为 size的区块
// 如果内存池空间也不够了,nobjs可能会降低
static char* _S_chunk_alloc(size_t __size, int& __nobjs);
// Chunk allocation state.
static char* _S_start_free; // 内存池起始位置。只在 _S_chunk_alloc() 中变化
static char* _S_end_free; // 内存池结束位置。只在 _S_chunk_alloc() 中变化
static size_t _S_heap_size;
同样,二级配置器也需要符合STL标准接口,需要封装函数 allocate():
static void* allocate(size_t __n)
{
void* __ret = 0;
// 如果需求区块大于 128 bytes,就转调用第一级配置
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n);
}
else {
// 根据申请空间的大小寻找相应的空闲链表(16个空闲链表中的一个)
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
_Obj* __RESTRICT __result = *__my_free_list;
// 空闲链表没有可用数据块,就将区块大小先调整至 8 倍数边界,然后调用 _S_refill() 重新填充
if (__result == 0)
__ret = _S_refill(_S_round_up(__n));
else {
// 如果空闲链表中有空闲数据块,则取出一个,并把空闲链表的指针指向下一个数据块
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
}
}
return __ret;
};
回收区块:deallocate(),其中关于线程安全的代码未粘贴
// 空间释放函数 deallocate()
static void deallocate(void* __p, size_t __n)
{
// 大于 128 bytes,就调用第一级配置器的释放
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;
}
}
free lists的填充
在allocate时,如果没有可用区块时,就调用refill(),为free list重新填充空间。新的空间取自内存池(经由chunk_alloc()完成),取得20个新节点,如果内存池空间也不足,获得的节点数可能小于20。以下为refill()源码:
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20;
// 调用 _S_chunk_alloc(),缺省取 20 个区块作为 free list 的新节点
char* __chunk = _S_chunk_alloc(__n, __nobjs);
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
// 如果只获得一个数据块,那么这个数据块就直接分给调用者,空闲链表中不会增加新节点
if (1 == __nobjs) return(__chunk);
// 否则根据申请数据块的大小找到相应空闲链表
__my_free_list = _S_free_list + _S_freelist_index(__n);
/* 在chunk空间建立free list */
__result = (_Obj*)__chunk;
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
// 从1开始,第0个数据块给调用者,地址访问即chunk~chunk + n - 1
for (__i = 1; ; __i++) { // 头插法
__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);
}
内存池取内存的过程chunk_alloc() :
// 从内存池中取空间
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 { // 内存池剩余空间连一个区块的大小都无法提供
size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
// 内存池的剩余空间分给合适的空闲链表
if (__bytes_left > 0) {
_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;
}
// 配置 heap 空间,用来补充内存池
_S_start_free = (char*)malloc(__bytes_to_get);
if (0 == _S_start_free) { // heap 空间不足,malloc() 失败
size_t __i;
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __p;
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;
return(_S_chunk_alloc(__size, __nobjs));
}
}
_S_end_free = 0;
// 调用第一级配置器
_S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
}
_S_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;
return(_S_chunk_alloc(__size, __nobjs)); // 递归调用自己
}
}
- 内存池空间完全满足容量,直接分配
- 内存池空间不足,但能分配一个以上的区块,则先给分配出来给调用方用着先
- 如果一个区块都分配不了了,就准备扩充内存池了,在此之前,需将内存池中较小的零头区块分配给合适的free list。(96bytes的没有,可能有16bytes的)
- 调用malloc补充内存池
- 假如malloc这里配置了40+n(附加量)个96bytes的区块,一个交给调用方,19个交给free list,另外20+n(附加量)个交给内存池。
- 如果heap空间不足,malloc失败,那就从大块的free list找有没有空闲的节点,有的话就把这个区块释放了,拿给小区块的free list用用。
- 如果free list中的大区快也都用完了,只能借助第一级配置器(借用里面的out-of-memory机制,看有没有哪个程序可以释放一部分资源出来)
内存池的实际操作:
tip:
对于容器,如vector,默认的配置器为alloc(也可以自己指定为allocator),而alloc配置器默认使用二级配置器。