具备次配置力的SGI空间配置器
SGI STL的配置器与标准规范不同,其名称是alloc,不接受任何参数(PS:SGI STL的每一个容器都已经指定其缺省的空间配置器为alloc,对于编码而言没有多大的困扰)
1、SGI有一个符合标准的allocator适配器,但效率不高,SGI并没有使用(因此注释写着有DO NOT USE THIS FILE)
2、SGI特殊的空间配置器,std::alloc
首先对于new算式和delete算式而言,实际上分为两个阶段
new:①调用operator new配置内存;②调用构造函数构造对象
delete:①调用析构函数将对象析构;②调用::operator delete释放内存
3、构造和析构基本工具:construct()和destroy()
主要是这张图:
construct()接受一个指针p和一个初值value,至于这个函数内部代码的一些解释可以部分参照我的上一篇文章。
destroy()有两个版本,第一个版本接受一个指针,准备将该指针之所致之物析构掉;第二个版本接受first和last
两个迭代器,准备将[first, last)范围内的所有对象析构掉。这里就存在一个问题了,对于基本类型而言,多次重复
的调用析构函数是种损伤,因此就有了_type_traits<T>判断该类型的析构函数是否无关痛痒。是的话就啥也不做,
否的话,每经历一个对象就调用第一个版本的destroy(个人认为就是调用对象的析构函数,至于原因就是防止内存
泄漏,析构函数能够保证内存被完整释放)
4、空间的配置和释放,std::alloc
首先谈一下SGI的设计哲学(此处手动滑稽):
SGI是以malloc( )和free( )完成内存的配置和释放的,考虑到小型区块可能造成的内存破碎问题,SGI设计了双层级配置器
第一级适配器:直接使用malloc和free
第二级适配器:使用memory pool整理方式,分配内存,自身无法处理或是内存不够时会调用第一级
PS:界限为128bytes
SGI给alloc包装了一个接口simple_alloc使其符合ST标准,这个接口使配置单位从bytes转到单个元素大小,其它容器使用这个
simple_alloc接口,里面的函数就是单纯的转调用
5、第一级适配器 __malloc_alloc_template剖析
(1)第一级适配器有以下几个部分组成:
①处理内存不足时使用的函数指针(静态),一个用于分配,一个再分配,一个错误处理函数
②接口,分配allocate,释放deallocate,再分配reallocate
③仿C++的set_new_handler()(PS:对这部分有兴趣的可以参照Effective C++ 第49条)
(2)内存处理函数:(至于几个接口,实质上主要工作是由内存处理函数完成的)
①static void (*__malloc_alloc_oom_handler)()
内存异常处理函数,初值为0,有待客户端设定,这个很重要,对于其它两个函数处理来说,如果
这个没有被设定,程序将直接抛出bad_alloc异常,或是直接终止程序
②static void *oom_malloc(size_t)
注意:这里使用了C++的new-handle模式,但是并不能直接使用C++的set_new_handler(),因为C++不支持realloc
③static void *oom_realloc(void *, size_t):过程与上面相同,只不过malloc变成了realloc
总结:第一级适配器直接使用malloc和free,同时对于内存分配异常时采用了new-handle模式
6、第二级适配器 __default_alloc_template剖析
第二级适配器使用了内存池的机制,如果区块够小就采用第二级配置器,第二级配置器可以避免太多小额区块造成
内存的碎片以及配置时的额外负担(我的理解是,由于每一块都需要额外的开销记录内存的一些信息,所以小区快越多
空间利用的效率就越低,而且如果小区块在空间上的位置分配的过于疏散,那么对于系统本身去维护这些小区快会造成
很大的负担),利用内存池的机制,尽可能的节省内存开销。
次层配置:区块小于128bytes交由内存池管理
(1)内存池的原理:
①free-lists的节点结构:
union obj{
union obj* free_list_link;
char client_data[1];
}
使用联合体union来节省内存从第一个字段来看,被视为指向下一个节点的指针;从第二阶段来看,可以被视为一个指针
指向实际的区块(我的理解是,在链表上的时候使用第一个字段,但是当它从链表上摘下来的时候,使用第二字段)
②内存池的结构:
内存池实现原理是由16个链表构成的,这十六个链表free_list各自管理的大小不同,分别是8,16,24,,,128bytes的小额
区块,接下来我来看一下代码
首先是边界值:
# ifndef
__SUNPRO_CC
enum {__ALIGN=
8}; //小型区块的上调边界
enum {__MAX_BYTES=
128}; //小型区块上限
enum {__NFREELISTS= __MAX_BYTES/__ALIGN}; //free_lists个数
# endif
PS:这里的enum没有给出类型名,表示只打算使用常量而不创建枚举变量
下面来看一下内存池(位于_default_alloc_template内):
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
这里的_VOLATILE的关键字就是volatile,volatile关键字告诉编译器不要进行过激的优化(具体内容自行百度)
这里的free_list数组就是管理内存分配的16条链表(__NFREELISTS :上文出现了这个枚举变量,把它变成小写n_freelists,意思就很明确了),现在就是有一个指针数组,大小为16,每个代表的是一个obj的链表
给个草图吧:
free_list数组:
真草图。。。。。
数组中的一项:
(2)内存池的实现
Ⅰ、每个链表管理的项的大小是不同的,这是怎么做到的
Ⅱ、对于一个内存分配的请求,内存池是如何分配内存的,同时之后又是如何回收的
Ⅲ、内存池是如何实现对自身的管理的,如果自己内存小了,不能完成分配任务,怎么扩容,其次,怎么管理内存碎片的
下面解答这三个问题,这三个问题的一起解答
①前期准备工作
首先对内存池的初始化:
template <bool threads,int inst>
__default_alloc_template<threads, inst>::obj*
__VOLATILE
__default_alloc_template<threads, inst> ::free_list[
# ifdef
__SUNPRO_CC
__NFREELISTS
# else
__default_alloc_template<threads, inst>::__NFREELISTS
# endif
] = {0,0,
0,0,
0,0,
0,0,
0,0,
0,0,
0,0,
0,0, };
// The 16 zeros are necessary to make version 4.1 of the SunPro
// compiler happy. Otherwise it appears to allocate too little
// space for the array.
来看一下几个对内存进行管理的函数
static
size_t ROUND_UP(size_t bytes) //将分配空间上调至8的倍数,比如bytes为20,实际上分配大小会上升到24
static
size_t FREELIST_INDEX(size_t bytes) //找到对应的链表在数组中的下标,比如2-free_list[0],
30-free_list[3]
static
void *refill(size_t
n); //当所在链表的空间为零时,重新装填
static
char *chunk_alloc(size_t
size, int
&nobjs); //重新装填的具体函数
②代码分析:
函数:ROUND_UP();
staticsize_tROUND_UP(size_t
bytes) {
return (((bytes)+ __ALIGN-1)&~(__ALIGN-1));
}
模拟一遍,就知道这个函数可以实现,将任何比8的倍数小的bytes升级成离他最近的8的倍数
函数:FREELIST_INDEX();
static size_t
FREELIST_INDEX(size_t bytes) {
return (((bytes)+ __ALIGN-1)/__ALIGN-
1);
}
找到与bytes最为接近的一个内存块(内存块大小是8的倍数)所对应的链表在内存池(数组)中的下标
人工模拟一遍内存操作过程:
Ⅰ、外部函数通过接口allocate申请内存
staticvoid
* allocate(size_t n)
{
obj * __VOLATILE* my_free_list;
obj * __RESTRICT result;
if (n> (size_t)
__MAX_BYTES) { //大于128Bytes调用第一级适配器,否则由第二级适配器的内存池进行分配
return(malloc_alloc::allocate(n));
}
my_free_list = free_list+
FREELIST_INDEX(n); //找到与当前size_t n大小最为接近的8的倍数所对应的->内存池中的链表
//这个地方free_list是一个指针数组
// 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; //得到所在项obj的地址,因为my_free_list是一个双重指针,所以要解引用一层
if (result==
0) {
void*r
=refill(ROUND_UP(n)); //如果内存不够,就得重新装填
return r;
}
*my_free_list= result ->free_list_link; //链表头移至下一项
return (result);
};
执行过程见注释
template <bool threads,int inst>
void* __default_alloc_template<threads, inst>::refill(size_t
n)
{
int nobjs=
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); //chunk_alloc第二个参数是按引用传递的,如如果申请的内存整好是一块且符合要求
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++) { //余下块划分成链表节点,每个的大小是n(8的倍数)
current_obj = next_obj;
next_obj = (obj*)((char*)next_obj
+ n); //这个链表的大小指的的上限是19个,所以知道nobjs是干啥用的了
if (nobjs-
1== i) {
current_obj -> free_list_link=
0;
break;
} else {
current_obj -> free_list_link= next_obj;
}
}
return(result);
}
template <bool threads,int inst>
char*
__default_alloc_template<threads, inst>::chunk_alloc(size_t
size, int& nobjs)
{
char* result;
size_t total_bytes= size
* nobjs;
size_t bytes_left= end_free
- start_free;
//分配时,函数会先从回收的堆中分配,如果回收的堆的大小不够就得像系统申请内存了
if (bytes_left>= total_bytes) { //剩余的内存够不够,这个内存是指内存池的回收heap
result = start_free; //对nobjs有疑问的看上面的注释
start_free += total_bytes; //但是由于两者的度量方式不一样,heap的大小指的是byte数量,而size是8的倍数,所以是size
* nobjs
return(result);
} elseif (bytes_left>=
size) { //size才是想要的byte大小,这是从回收的堆中分配的大小
nobjs = bytes_left/size;
total_bytes = size* nobjs; //后面的操作就是重新调整堆的大小
result = start_free;
start_free += total_bytes;
return(result);
} else {
size_t bytes_to_get=
2* total_bytes+
ROUND_UP(heap_size>>
4);
//申请新内存的大小,至于为什么这样计算我就不知道了,有一点可以肯定的是,申请的大小一定是8的倍数
//
尝试利用回收堆中的内存
if (bytes_left>
0) {
obj * __VOLATILE* my_free_list=
free_list +FREELIST_INDEX(bytes_left);
((obj *)start_free) ->free_list_link=
*my_free_list;
*my_free_list= (obj
*)start_free;
}
start_free = (char*)malloc(bytes_to_get);
if (0== start_free)
{ //分配失败,莫名蛋疼
int i;
obj * __VOLATILE* my_free_list,*p;
// Try to make do with what we have. That can't
// hurt. We do not try smaller requests, since that tends
// to result in disaster on multi-process machines.
//这里干的活,就是把新得到的内存划分成一个个大小为8的倍数区块,挂到各自链表上
for (i= size; i
<= __MAX_BYTES; i
+= __ALIGN) {
my_free_list = free_list+
FREELIST_INDEX(i);
p =*my_free_list;
if (0!=
p) { //找比当前块规模更大的链表,然后将链表的头结点交换给内存池,递归自己重新分配
*my_free_list= p ->
free_list_link;
start_free = (char*)p;
end_free = start_free+ i;
return(chunk_alloc(size, nobjs));
// Any leftover piece will eventually make it to the
// right free list.
}
}
end_free =0; // In case of exception.
start_free = (char*)malloc_alloc::allocate(bytes_to_get);
// This should either throw an
// exception or remedy the situation. Thus we assume it
// succeeded.
}
heap_size += bytes_to_get; //剩下的就好说了
end_free = start_free+ bytes_to_get;
return(chunk_alloc(size, nobjs));
}
}
又来一张草图(具体写的时候情况肯定比这个要复杂很多,包括很多细节以及来自C++的秘境,这个等撸的时候再说)
至此关于内存分配的告一段落
可能有些地方讲解的不正确,欢迎交流以及指正。
Ⅱ、使用realloc申请:
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);
//使用new_sz划分空间
copy_sz = new_sz> old_sz?
old_sz: new_sz; //保证新划分的空间不能比原来的要小
memcpy(result, p, copy_sz); //后面的一个是复制数据,然后是归还老内存
deallocate(p, old_sz);
return(result);
}
Ⅲ、使用deallocate释放
staticvoid
deallocate(void*p,
size_t n)
{
obj *q= (obj
*)p;
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); //否则的话就直接把这个内存块挂到对应的链表上,注意保持一致性,只能释放 //由allocate分配的内存,记住Byte的大小是8的倍数
// 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
}
过程就是这样
应该先好好看书,再去看代码。。。。。