空间配置器
这里在STL中说的空间配置器更多的是指内存的申请和释放。在STL中,一般拥有两级空间配置器,其中第一级配置器更像我们传统意义上的申请;而第二级配置器是一个池,一般称为内存池。
这里对空间配置器进行一个简短的综述,空间配置器管理存储资源的申请和释放的策略是当申请的资源较大时,这里说的较大时大于128BYTES时,调用第一级配置器,采用malloc和free的方式申请和释放;当小于128BYTES时,调用第二级配置器,第一级配置器实际上是由一个链表进行维护,但freeList中找到了合适大小的内存,直接给申请者使用,如果实在不够的话,就只能用部分,甚至再用堆申请一部分,即用malloc申请。释放的时候加入到freeList中。
采用二级配置器的一个利好就是,能够较大程度上减少碎片化(包括内部碎片和外部碎片),提高内存内部的使用率,减少资源浪费。
外部碎片很好理解,这里强调一下内部碎片,
假如堆块的实现是隐式空闲链表的话,一个堆块(也就是一次malloc申请的堆空间,会有个头部和脚部的边界标记(数据是一样的,都是记录这个块有多大,和有没有被分配出去,目的是为了实现合并,现在知道为啥 free 不需要给到底要释放多少个字节的信息了吧,还有delete和delete[] 对于POD trivial类型为啥没区别了,还有为啥c++的allocator类 deallocate 需要你给多少个字节,这玩意就是为了给容器这种放同一类东西准备的),分别占一个字(4字节),并且考虑地址对齐要求,假设是双字,那么我申请一个字节的空间,最后给的堆块也会是一个4*4=16字节的),这样就会有很大的浪费,而对于stl的容器来说,假如我们要放100w个数据,每个数据的类型是一样,也就是说放数据需要的空间我们是确定的,那就不需要100w个头部和脚部,所以为了更高效的利用内存空间,使用内存池来分配内存。
假如是显示空闲链表的话,同样会有头部脚部,并且空闲块还有祖先后继地址,反正这个头部和脚部的边界标记省不了。
总览
allocator把new和delete分别干的两件事给分开
new:
1)分配内存 ——::operator new(size_t)——allocate
2)执行构造函数——定位new new (p) T1(value)——construct
delete:
1)执行析构函数—— 显式的执行析构函数 p->~string()——destroy
2)释放内存——::operator delete (void *p)——deallocate
construct和destroy没什么花样,分配内存和释放内存这一块可以有不同的设计,sgi stl 2.9中,有个两级的allocator,当用户请求的内存大于128bytes时,则通过malloc和free再一个类似set_new_handler函数,(或者直接用::operator new和operator delete)来配置内存(一级配置器),当请求内存小于128bytes时,则通过内存池来管理内存(二级配置器)。
第二层配置器通过内存池和自由链表(free_list)使得用户申请<128bytes的内存更快捷并造成更少的内存碎片。它每次配置一块大的内存交给自由链表维护,用户每次申请的内存都从链表中获取,并且在释放时交还与自由链表。SGI STL将用户申请的128bytes以内的内存自动上调到8的倍数,并维护16个free_list,各free_list负责的大小分别为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128bytes。比如用户申请10bytes内存,将被上调到16bytes,并且从负责管理16bytes内存的free_list中取出一个节点(也就是一块内存),如果free_list中当前没有节点,则从内存池中之前用malloc申请内存的时候,自己剩下来的那些保管的那部分内存中(start-free~end-free)分配足够内存,并且填充到free_list。如果内存池也没有,那就用malloc去申请内存(其中一部分切割成一块一块交给free_list,还有一部分自己留着)。
Free_list的节点结构如下:
union obj
{
union obj* free_list_link; //用于维护空闲内存,指向下一个空闲节点
char client_data[1]; //用于用户使用
}
注意节点是union类型的,当节点空闲(未被分配时),节点使用第一字段指向下一个空闲节点,当节点被分配后,用户可以直接使用第二字段,这样自由链表就不会因为free_list_link指针而造成内存的浪费(当节点被分配出去后,free_list_link指针就没有意义了)。
一个简单的allocator配置器
首先我们来看一个SGI STL中符合标准,名为allocator的空间配置器:
//位于cygwin-b20\include\g++\defalloc.h
#ifndef DEFALLOC_H
#define DEFALLOC_H
#include <new.h>
#include <stddef.h>
#include <stdlib.h>
#include <limits.h>
#include <iostream .h>
#include <algobase.h>
template <class T>
inline T* allocate(ptrdiff_t size, T*) {
set_new_handler(0);
T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
if (tmp == 0) {
cerr << "out of memory" << endl;
exit(1);
}
return tmp;
}
template <class T>
inline void deallocate(T* buffer) {
::operator delete(buffer);
}
template <class T>
class allocator {
public:
typedef T value_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
pointer allocate(size_type n) {
return ::allocate((difference_type)n, (pointer)0);
}
void deallocate(pointer p) { ::deallocate(p); }
pointer address(reference x) { return (pointer)&x; }
const_pointer const_address(const_reference x) {
return (const_pointer)&x;
}
size_type init_page_size() {
return max(size_type(1), size_type(4096/sizeof(T)));
}
size_type max_size() const {
return max(size_type(1), size_type(UINT_MAX/sizeof(T)));
}
};
class allocator<void> {
public:
typedef void* pointer;
};
这份allocator很简单,只是对::operator new和::operator delete的简单封装而已。上面的allocator完成对内存的分配与回收,但是并没有对象的构造,对象的构造是在defalloc同目录下的stl_construct.h中的函数完成的,之所以将对象的分配和构造(析构和回收)分开,是为了提高效率,避免不必要的构造和对对象构造析构的优化,在使用上面的allocator配置器时,内存的分配和回收分别由allocator::allocate()和allocator::deallocate()负责,而对象的构造和析构则由::construct()和destroy()负责(位于stl_construct.h),这种分工使整个空间配置更灵活高效。
但是SGI STL并未使用上面的allocator,因为它效率不高,保留它是为了与HP STL风格兼容。SGI
STL真正使用的是一个叫alloc的配置器,它在很多方面都与STL规范不同,但是性能卓越。将alloc和stl_construct中的construct(),destroy()结合,成为了SGI
STL的独门利器。
构造和析构基本工具:construct()和destroy()
首先来看看相对简单的对象构造和析构工具: construct()和destroy()
//cygwin-b20\include\g++\stl_construct.h
#ifndef __SGI_STL_INTERNAL_CONSTRUCT_H
#define __SGI_STL_INTERNAL_CONSTRUCT_H
#include <new.h>
__STL_BEGIN_NAMESPACE
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); //布局new(placement new) 在p地址处调用T1构造函数构造对象
}
template <class ForwardIterator>
inline void
__destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) {
for ( ; first < last; ++first)//如果元素的析构函数是必要的 那么逐个调用析构函数
destroy(&*first);
}
template <class ForwardIterator> //如果元素的析构函数是无关紧要的 就什么也不做
inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {}
template <class ForwardIterator, class T>
inline void __destroy(ForwardIterator first, ForwardIterator last, T*) {
//通过元素型别来判断析构函数是否无关紧要(trivial) 并调用对应的函数进行析构
typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
__destroy_aux(first, last, trivial_destructor());
}
template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last) {
__destroy(first, last, value_type(first));//通过泛型的类型识别技术来得到元素类型
}
inline void destroy(char*, char*) {}
inline void destroy(wchar_t*, wchar_t*) {}
__STL_END_NAMESPACE
#endif /* __SGI_STL_INTERNAL_CONSTRUCT_H */
这里面的构造函数construct()只是调用placement new,关于placement new和::operator new以及new运算符的关系和区别我前面专门有博客阐述。
而析构函数destroy就比较巧妙了。它有两个重载,一个销毁指定元素,这个只需要调用其析构函数即可,另一个接受first和last两个迭代器,用于销毁迭代器内的所有元素。这时候destroy并不是盲目地对这个范围内所有元素依次调用析构函数,为了效率起见,它先通过泛型的类型解析从而在_destory()中得到元素类型,再通过元素类型的_type_traits<T>::has_trivial_destructor
trivial_destructor来判断元素类型的析构函数是否是无关紧要(trivial)的,如果是,那么trivial_destructor()值为true_type,否则为false_type,注意,这里的true_type,false_type是一种类型而不是值,因此再通过一个_destroy_aux()的重载即可对两种情况分别处理。如果是false_type,也就是元素类型的析构函数是必要的,那么就老老实实依次调用每个元素的析构函数,否则,什么也不做。这对于销毁大范围的元素来说,如果析构函数无关痛痒,那么效率上将会有很大提高。
空间的分配与释放:std::alloc
接下来,看看std::alloc又是如何以高效率淘汰前面的allocator的。
简单来说,alloc主要在如下方面超越了allocator
1.通过内存池技术提升了分配效率:
2.对小内存频繁分配所可能造成的内存碎片问题的处理
3.对内存不足时的处理
两级配置器:
alloc采用了两级配置器,其中一级配置器直接从堆中获取和释放内存(通过malloc),效率和前面的allocator相当。二级适配器采用内存池技术,对用户的小区块申请进行了优化,当用户申请大区块时,它将其交予一级配置器。当用户申请小区块时,将于内存池打交道,内存池通过自由链表来管理小区块,当内存池不足时,会一次性向堆中申请足够大的空间。用户可以通过宏来控制使用哪一级配置器(默认为二级配置器)。
在前面的allocator中,内存的分配和释放都使用的是::operator new()和::operator
delete()函数,它们的内部其实也是调用C语言的malloc和free来实现的,在alloc中,内存的分配和释放直接使用malloc()和free()两个函数。当用户请求的内存大于128bytes时,则通过malloc和free来配置内存(一级配置器),当请求内存小于128bytes时,则通过内存池来管理内存(二级配置器)。用户可以通过一个_USE_MALLOC宏来控制是否使用二级配置器(即不管请求内存大小,均从堆中分配)。当_USE_MALLOC被定义时,将不使用二级配置器。
用《STL源码剖析》中的两幅图来概览一下两级配置器:
一级配置器:
cygwin-b20\include\g++\stl_alloc.h
template <int inst>
class __malloc_alloc_template {
private:
static void *oom_malloc(size_t);// malloc内存不足处理例程
static void *oom_realloc(void *, size_t);// realloc内存不足处理例程
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
static void (* __malloc_alloc_oom_handler)();//函数指针,保存用户定义的内存不足处理函数
#endif
public:
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);
}
static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)
{
void * result = realloc(p, new_sz);
if (0 == result) result = oom_realloc(p, new_sz);
return result;
}
//设置malloc内存不足处理例程
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)() = 0;
#endif
template <int inst>
void * __malloc_alloc_template<inst>::oom_malloc(size_t n)
{
void (* my_malloc_handler)();
void *result;
for (;;) {//反复调用用户定义(通过set_malloc_hander函数)的内存不足函数
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>//和oom_malloc类似
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 (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }
(*my_malloc_handler)();
result = realloc(p, n);
if (result) return(result);
}
}
第一层配置器直接通过malloc来分配内存,并在此之上建立内存不足处理例程。当allocate()通过malloc分配内存失败时,它会调用内存不足处理例程oom_malloc(),oom_malloc会不断调用用户定义的处理函数(由_malloc_alloc_oom_handler保存,通过set_malloc_handler()设置),并且尝试分配内存。如果用户未定义处理函数,则抛出异常。在这个过程中,设计和设置内存不足处理函数都是用户的责任。
二级配置器:
第二层配置器通过内存池和自由链表(free_list)使得用户申请<128bytes的内存更快捷并造成更少的内存碎片。它每次配置一块大的内存交给自由链表维护,用户每次申请的内存都从链表中获取,并且在释放时交还与自由链表。SGI
STL将用户申请的128bytes以内的内存自动上调到8的倍数,并维护16个free_list,各free_list负责的大小分别为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128bytes。比如用户申请10bytes内存,将被上调到16bytes,并且从负责管理16bytes内存的free_list中取出一个节点(也就是一块内存),如果free_list中当前没有节点,则从内存池中分配足够内存,并且填充到free_list。
Free_list的节点结构如下:
union obj
{
union obj* free_list_link; //用于维护空闲内存,指向下一个空闲节点
char client_data[1]; //用于用户使用
}
注意节点是union类型的,当节点空闲(未被分配时),节点使用第一字段指向下一个空闲节点,当节点被分配后,用户可以直接使用第二字段,这样自由链表就不会因为free_list_link指针而造成内存的浪费(当节点被分配出去后,free_list_link指针就没有意义了)。如下图
下面是主要代码:
template <bool threads, int inst>
class __default_alloc_template {
private:
// Really we should use static const int x = N
// instead of enum { x = N }, but few compilers accept the former.
enum {__ALIGN = 8};
enum {__MAX_BYTES = 128};
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};//16
//上调到8的倍数
static size_t ROUND_UP(size_t bytes) {
return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
}
__PRIVATE:
union obj {
union obj * free_list_link;
char client_data[1]; /* The client sees this. */
};
private:
//自由链表数组 各自管理不同大小的内存节点
static obj * __VOLATILE free_list[__NFREELISTS];
//找到bytes所属的free_list
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; //内存池的起始地址 只在chunk_alloc()中变化
static char *end_free; //内存池的结束地址 只在chunk_alloc()中变化
static size_t heap_size;
public:
/* 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));
}
my_free_list = free_list + FREELIST_INDEX(n);//找到对应free_list
result = *my_free_list;//直接使用最前面那个节点
if (result == 0) {//如果没有节点了
void *r = refill(ROUND_UP(n));//重新填充free_list
return r;
}
//调整free_list
*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;
//如果>128bytes 使用以及配置器回收
if (n > (size_t) __MAX_BYTES) {
malloc_alloc::deallocate(p, n);
return;
}
//找到节点所属的free_list
my_free_list = free_list + FREELIST_INDEX(n);
//放回free_list 并调整
q -> free_list_link = *my_free_list;
*my_free_list = q;
}
static void * reallocate(void *p, size_t old_sz, size_t new_sz);
} ;
上面是部分源码,删掉了多线程的那部分代码。主要函数有allocate()和deallocate()(STL标准规定空间配置器必须有allocate()和deallocate()接口)。在allocate()中,先判断用户申请的内存大小,如果大于128bytes,则交由一级配置器。否则自由链表中找到对应大小的free_list,然后取出第一个节点并返回。如果对应free_list没有节点,那么通过refill()函数从内存池分配足够节点填充free_list并返回给客户端。refill()代码如下:
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
int nobjs = 20;
//尝试从内存池分配nobjs个大小为n的区块,如果内存池不够,
//实际分配个数由nobjs指出 注意:nobjs参数类型为引用
char * chunk = chunk_alloc(n, nobjs);
obj * __VOLATILE * my_free_list;
obj * result;
obj * current_obj, * next_obj;
int i;
//如果内存池只返回了一个区块 则直接返回 无需调整free_list
if (1 == nobjs) return(chunk);
my_free_list = free_list + FREELIST_INDEX(n);
//将返回区块的第一块返回给客户端 其余的填充到free_list
/* 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);
}
可以看到,与内存池打交道的任务实际是chunk_alloc()完成的:
memory pool还有两个指针(s_start_free 和 s_end_free) 用于计算空闲内存块的大小,还有size_t的变量heap_size用于记录当前memory pool总容量。memory pool可提供的操作主要是如何根据实际内存请求从memory pool中拨出内存(s_chunk_alloc(size_t size, int& nobjs); ),以及根据内存释放请求归还内存块到memory pool中。
s_chunk_alloc函数是二级内存配置器的核心函数,因为二级配置器default_alloc_template的allocate接口实现都是高度依赖此函数的实现。s_chunk_alloc也是default_alloc_template的业务逻辑核心。
// We allocate memory in large chunks in order to avoid fragmenting the
// malloc heap (or whatever __mem_interface is using) 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>::_S_chunk_alloc(size_t __size,
int& __nobjs)
{
char* __result; // 预返回的内存块儿首址
size_t __total_bytes = __size * __nobjs;
/* 预分配的内存块总大小,通常情况下nojbs的大小为20,
* nobjs的大小由s_refill函数传进。
*/
size_t __bytes_left = _S_end_free - _S_start_free;
//memory pool剩余内存块大小
if (__bytes_left >= __total_bytes)
/* I: 如果memory pool剩余内存块完全满足需求量 */
{
__result = _S_start_free;
//则将剩余内存块儿的首地址作为结果返回
_S_start_free += __total_bytes;
//将剩余内存块儿的首地址变为增加需求量之后的地址
return(__result);
}
else if (__bytes_left >= __size)
/* II:如果剩余内存空间不足需求量,但是足够供应一个size大小的内存块 */
{
__nobjs = (int)(__bytes_left/__size);
//重新计算nobjs
__total_bytes = __size * __nobjs;
//改变整体预分配内存的大小
__result = _S_start_free;
// 返回分配的内存地址为当前剩余内存的首址
_S_start_free += __total_bytes;
//改变剩余内存的首址为增加了分配内存块后的地址
return(__result);
}
else
/* III:memory pool剩余的空间连一个区块的大小也无法提供 */
{
size_t __bytes_to_get =
2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
/* 扩大需要量, 新需求量是原需求量的二倍与现有内存池大小的四分之一之和 */
if (__bytes_left > 0)
/* 如果内存池还有残余的零头内存,处理零头内存 */
{
_Obj* volatile* __my_free_list =
_S_free_list + _S_freelist_index(__bytes_left);
//寻找监管残余内存的freelist
((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
*__my_free_list = (_Obj*)_S_start_free;
/* 归还那些因不足实际需求大小的内存块到监管的freelist中 */
}
_S_start_free = (char*) __mem_interface::allocate(__bytes_to_get);
// 请求一级内存配置器分配空间
if (0 == _S_start_free)
//如果一级配置器分配失败
{
size_t __i;
_Obj* volatile* __my_free_list;
_Obj* __p;
__i = __size;
for (; __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;
/* 以下三行比较巧妙,__bytes_left = __i;
* 通过递归调用s_chunk_alloc(size, nobjs)后走II分支
* 找到freelist中未必使用的内存区块,且这个区块足以大于client的实际需求。
*/
_S_start_free = (char*)__p;
_S_end_free = _S_start_free + __i;
return(_S_chunk_alloc(__size, __nobjs));
}
}
/*以上for循环顺次查找每个freelist,看是否还有freelist监管的内存区块未被使用且足够大*/
_S_end_free = 0;
_S_start_free = (char*)__mem_interface::allocate(__bytes_to_get);
/*上述尝试若失败,通过一级配置器抛出bad_alloc异常 */
}
_S_heap_size += __bytes_to_get;
//修改memory pool的总容量
_S_end_free = _S_start_free + __bytes_to_get;
//修改内存池剩余空间大小
return(_S_chunk_alloc(__size, __nobjs));
/* 在一级内存分配器分配了申请的内存后,
* 递归调用s_chunk_alloc后通过I分支处理分配内存。
*/
}
}
chunk_alloc总结1:
根据上述代码,s_chunk_alloc的主要逻辑实际上是处理三种情况:
I: 内存池剩余空间足以满足内存的分配请求(注意此时的接收到的内存分配请求是client需求量的20倍);
处理方法:直接从剩余空间中拨出需求量大小的内存区块。
II: 内存池剩余空间不足满足需求量,但是大于一个区块(client实际需求量);
处理方法:缩小需求量至内存池剩余空间按照client实际需求量的最大倍数,然后按照新需求量分配内存块返回;
III: 内存池剩余空间既不能满足需求量,也不能满足实际client需求量;
处理方法:
先调整内存池中零头的内存块,使其归为到对应的freelist中。
请求一级内存配置器分配扩大后需求量。注意,扩大后的需求量是原始需求量的二倍与内存池大小的四分之一的和那么大。
如果一级内存配置器分配新需求量失败,试图在已分配但未使用的内存块(freelist)中寻找足以大于实际client需求的内存分配内存块返回给客户。
如果步骤3失败,再向一级内存配置器请求帮助,如果帮助未果则利用一级内存配置器抛出异常bad_alloc。
如果一级内存配置器分配请求成功后,递归调用自己走I情况分配内存块。
chunk_alloc总结2:
这个函数就有点复杂了,它先是判断自己的内存池里面的内存还够不够这次分配。如果够,就正常处理下,然后返回。如果只够一个block,也会返回。但是如果一个block也不够了呢?
这时候就需要malloc
了,具体要申请的是2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
,这个total_bytes
好理解,就是我们需要的空间,而这个heap_size
则是这个class template中的一个static data member,存储的是迄今为止用了多少heap memory,也就是malloc
了多少内存,初始值为0。所以说,每次要申请的内存是两倍的需求量加上,一点点附加量。形象来看,就是老顾客,就需求量多点,多给点。然后,malloc
会比需求多点,给内存池存点余粮,免得频繁malloc
,速度慢。但是一下子申请多了也不可以,可能会浪费。
好了,决定要malloc
后,会尽量把内存不浪费,把剩下的内存切成块放到链表中去。然后就是malloc
了。而malloc
失败后,会尝试把链表里面的mem block都收集到内存池中去,然后再去尝试满足这次分配。然后就是调用malloc_alloc
,争取让他可能有的out of memory handler去搜刮点内存来。
所以说,这个chunk_alloc
实际上是,有内存就任性给。但是如果没有了,那就想尽各种办法去节约和搜刮点内存来满足这次请求。
在了解上述memory pool分配内存块的基本逻辑后,还需要进一步了解s_refill函数。因为二级内存配置器的allocate函数实现是s_refill与s_chunk_alloc两个实现的协作实现。
s_refill模板函数的实现
// Returns an object of size __n, and optionally adds to "size
// __n"'s 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>::_S_refill(size_t __n)
{
int __nobjs = 20;
//默认20个,实际client需求的20倍
char* __chunk = _S_chunk_alloc(__n, __nobjs);
//请求memory pool分配内存块,实际需求的20倍
_Obj* 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);
__result = (_Obj*)__chunk;
//将返回给客户20块内存区块的第一块地址
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
//修改监管freelist的首地址为第一个空闲内存块的地址
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;
}
}
//上述for循环,是将freelist中的各结点串接起来。
return(__result);
}
refill的逻辑是,先尝试去调用chunk_alloc
去内存池中分配20个块大小的内存,并且 以传引用的方式来返回真正分配了的内存块数量。如果返回的只有1个块,还是返回之。其他情况就把分配得来的空间,给切成小块,然后加入到对应的链表中。
所以,refill其实就是个进货的函数,具体怎么进货的,也就是内存池如果工作的,还是chunk_alloc
函数实现的。
最后我们看看二级内存配置器是如何完成内存配置服务的。
static void*
allocate(size_t __n)
{
void* __ret = 0;
if (__n > (size_t) _MAX_BYTES)
// 如果请求大于128字节,由一级内存配置器分配内存
__ret = __mem_interface::allocate(__n);
else
// 当内存请求小于等于128字节时
{
_Obj* volatile* __my_free_list = _S_free_list
+ _S_freelist_index(__n);
//定位监管的freelist
_Obj* __restrict__ __result = *__my_free_list;
if (__result == 0)
//如果freelist是空的,则通过调用s_refill分配freelist
__ret = _S_refill(_S_round_up(__n));
else
//如果freelist有空闲内存块,则返回一个空闲块给客户
//同时监管freelist的头指针后移一个空闲块
{
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
}
}
return __ret;
};
下面举个例子,假设程序一开始,二级内存配置器相继收到如下内存请求:
256字节, 32字节, 32字节,64字节,48字节, 48字节;
1. 256字节:因为其大小大于128字节,则由一级内存配置器直接分配大块内存256字节给客户。
2. 32字节:小于128字节,二级配置器接管请求;找到监管freelist是第4个,数组下标#3, 发现freelist是空;则进一步调用s_refill申请内存;此时s_refill(32)接收到请求后,调用s_chunk_alloc(32, 20)先完成内存池中第四个freelist的分配;
2.1. s_chunk_alloc(32,20)被调用后,内存池是空的,于是直接请求一级内存配置器分配640字节的内存块给内存池。
2.2 一级内存配置器完成分配后,返回给内存池,内存池将此640字节大小的内存块首地址记录到s_start_free指针,同时修改内存池大小,并记录空闲内存块的尾指到s_end_free指针。
2.3 递归调用s_chunk_alloc(32, 20), 此时内存池的空闲内存块是640字节,大于320字节,所以程序处理流程进入I分支。I分支会将这640字节的内存块分成320字节的两大块,前320字节那块作为结果返回,后320字节作为内存池的空闲内存块,s_start_free的地址修改指向后320字节内存块的首地址。
2.4 s_refill此时接收到调用s_chunk_alloc(32, 20)返回的结果后,将320字节内存块分成20块,第一个32字节内存块作为客户请求返回,其余的19个32字节内存块依次串接起来作为第4个freelist监管起来。
3. 32字节:小于128字节,二级配置器接管请求;找到监管freelist是第4个,数组下标#3,发现freelist已分配;此时,直接将freelist中的第一个32字节的内存块(余下19个32字节内存块中的第一个,相当于初始分配的20个内存块中的第二个)返回给客户,同时监管的freelist指向余下的18个内存块。
4. 64字节:小于128字节,二级配置器接管请求。定位监管freelist是第8个,数组下标#7,为空。调用s_refill(64)。同样,s_refill调用s_chunk(64, 20);
4.1. s_chunk_alloc(64, 20)被调用后,内存池的空闲内存块大小是320字节,注意此时需求量是1280字节。
4.2. 因为320字节小于1280字节,但又大于64字节,所以满足II分支的条件。
4.3. II分支先在内存池中修改需求量到320字节,nobjs由原来的20变为5. 将空闲的320字节内存块首址作为结果返回,同时内存池已经没有空闲内存块了。
4.4. 同样,s_refill得到结果后,将320字节的内存块分成5块,第一个64字节内存块作为响应客户的64字节请求返回,而余下的4块串接起来作为第8个freelist监管起来。
5. 48字节:小于128字节,二级配置器接管请求。定位监管freelist是第6个,数组下标#5,为空。调用s_refill(48)。
同样,s_refill调用s_chunk(48,20)。
5.1. s_chunk_alloc(48,20)被调用后,内存池空闲内存块是0字节,需求量是960字节,实际需求是48字节。
5.2. 因为空闲内存块是0字节,所以进入分支III。
5.3. 分支III,请求一级配置器帮助分配1920字节的内存块,但是我们假定现在一级内存配置器分配内存失败。
5.4.1. 此时,分支III从监管48字节的freelist开始,循环随后每一个freelist,略过第8个freelist之前2个空freelist。
5.4.2. 第8个freelist不空,将内存池的空闲内存块设置成freelist监管的4个64字节内存块中的第一个,然后递归调用s_chunk_alloc(48, 20);
5.4.3. 此时内存池空闲内存大小是64字节,走分支II。
5.4.4. 分支II,重新计算nobjs=1。此64字节的内存块的起始地址将作为结果返回,同时修改内存池空闲内存的起始地址到返回结果偏移48字节的地址。所以此时内存池的空闲地址是64-48=16字节。(注意此16字节就是零碎内存块了,这16字节的空闲内存,只有下次内存请求的时候会被归还。)
5.4. s_refill得到结果后,直接将结果再返回给客户。
6. 48字节: 小于128字节,二级配置器接管请求。定位监管freelist是第6个,数组下标#5,为空。调用s_refill(48)。
同样,s_refill调用s_chunk(48,20)。
6.1. s_chunk_alloc(48,20)被调用后,内存池空闲内存是16字节,需求量是960字节,实际需求是48字节。
6.2. 所以走分支III。
6.3. 分支III。
6.3.1. 先将16字节的空闲内存归并到第2个freelist中。(相当于将空闲内存快作为freelist的头元素插入)
6.3.2. 然后请求一级配置器帮助分配1920字节的内存块,但是仍然无功而返
6.3.3. 从第6个freelist开始遍历,略过空freelist,直至第8个freelist。
6.3.4. 第8个freelist不空,将内存池的空闲内存块设置成freelist监管的3个64字节内存块中的第一个,然后递归调用s_chunk_alloc(48, 20);
6.3.5. 此时内存池空闲内存大小是64字节,走分支II。
6.3.6. 分支II,重新计算nobjs=1。此64字节的内存块的起始地址将作为结果返回,同时修改内存池空闲内存的起始地址到返回结果偏移48字节的地址。所以此时内存池的空闲地址是64-48=16字节。
6.3.7. s_refill得到结果后,直接将结果再返回给客户。
以上便是二级内存配置器的内存分配请求逻辑;下面的deallocate相对来说,就简单至极了。寻找的监管的freelist,然后通过修改指针,归并释放的内存块到监管的freelist即可。注意归还的时候,都是作为freelist的头元素插入到freelist中的。
static void
deallocate(void* __p, size_t __n)
{
if (__n > (size_t) _MAX_BYTES)
__mem_interface::deallocate(__p, __n);
else
{
_Obj* 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;
}
}
到此,整个二级配置器就完工了。现在从客户端申请内存开始来理一下流程:
当客户端第一次申请30bytes内存时,首先被上调到32bytes,在allocate()中,找到管理32bytes块的free_list[3],发现它为空,因此调用chunk_alloc(32,20)从内存池中取出内存,但是此时内存池也是空的,因此在chunk_alloc()中,调用malloc()分配了20*2=40个32bytes区块,其中1个交给客户端,19个填充到free_list,剩余20*32bytes内存留给内存池。当用户再申请60bytes时,上调到64bytes,然后找到free_list[7]发现也是空的,然后调用chunk_alloc(64,20),但是此时内存池只有32*20bytes空间,只够10个64bytes区块,因此内存池返回这10个区块,在refill()中,1个返回给客户端,剩余9个交给free_list[7]维护。如果用户再申请90bytes内存,此时仍然会调用chunk_alloc(96,20),此时内存池又空了,会通过malloc()申请(20*2+n)*96bytes的空间(其中n是一个附加量,内存池malloc()次数越多,该值越大),之后1个交给客户端,19个交给free_list[11],剩余(20+n)*96bytes内存留给内存池。。。。
如果内存池中malloc()失败,即系统堆也不够了,那么先尝试从free_list中找到一个比申请区块更大的空闲区块,如果有,则放入内存池,然后递归调用自身。如果没有找到,最后尝试一级配置器,或许内存不足处理例程能做些什么,再否则就抛出bad_alloc异常。