具备次配置力的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();
static
size_t
ROUND_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申请内存
static
void
*
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);
}
else
if
(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释放
static
void
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
}
过程就是这样
应该先好好看书,再去看代码。。。。。