stl六大组件简介
我们知道,stl有容器,空间配置器,适配器,迭代器,仿函数以及算法这6个组件,它们六者关系大概如下:容器通过配置器取得数据存储空间,算法通过迭代器获取容器内容,仿函数可以协助算法完成不同的策略变化,配接器可以修饰或套界仿函数。
侯捷在《STL源码剖析》一书讲到:
空间配置器:为了解决因频繁小块开辟时,产生的内存碎片问题。
如果开辟的内存大于128个字节时,就调用一级空间配置器,小于128个字节时,就调用二级空间配置器。
在SGI版本的STL中,空间的配置释放都由< stl_alloc.h > 负责。它的设计思想如下:
(1)向system heap要求空间
(2)考虑多线程
(3)考虑内存不足的应变措施
(4)考虑内存碎片的问题
一级空间配器:封装了malloc()和free()。
注意:如果客户端设置的内存不足处理函数,没有设置好,会存在死循环的危险。
template<int Inst>
class __MallocAllocTemplate //一级空间配置器
{
typedef void (*OOM_HANDLER)();
private:
//these funs below are used for "OOM" situations
//OOM = out of memory
static void* OOM_Malloc(size_t n); //function
static void* OOM_Realloc(void *p, size_t newSZ); //function
static OOM_HANDLER OOM_Handler; //function pointer
public:
static void* Allocate(size_t n)
{
void* ret = malloc(n);
if (ret == NULL)
ret = OOM_Malloc(n);
return ret;
}
static void Deallocate(void* p, size_t n)
{
free(p);
}
static void* Reallocate(void* p, size_t oldSZ, size_t newSZ)
{
void* ret = realloc(p, newSZ);
if (ret == NULL)
ret = OOM_Realloc(p, newSZ);
return ret;
}
//static void (* set_malloc_handler(void (*f)()))()
//参数和返回值都是函数指针void (*)()
static OOM_HANDLER SetMallocHandler(OOM_HANDLER f)
{
OOM_HANDLER old = OOM_Handler;
OOM_Handler = f;
return old;
}
};
//让函数指针为空
template<int Inst>
void (*__MallocAllocTemplate<Inst>::OOM_Handler)() = NULL;
template<int Inst>
void* __MallocAllocTemplate<Inst>::OOM_Malloc(size_t n)
{
void* ret = NULL;
void(*myHandler)() = NULL;
for (;;)
{
myHandler = OOM_Handler;
if (myHandler == NULL)
throw bad_alloc();
(*myHandler)();
ret = malloc(n);
if (ret != NULL)
return ret;
}
}
template<int Inst>
void* __MallocAllocTemplate<Inst>::OOM_Realloc(void* p, size_t newSZ)
{
void* ret = NULL;
void(*myHandler)() = NULL;
for (;;)
{
myHandler = OOM_Handler;
if (myHandler == NULL)
throw bad_alloc();
(*myHandler)();
ret = realloc(p, newSZ);
if (ret != NULL)
return ret;
}
}
typedef __MallocAllocTemplate<0> MallocAlloc; //一级空间配置重命名
第二级空间配置器
如果配置区块超过128 bytes,则移交给第一级配置器处理;
如果配置区块小于128 bytes,则采用内存池管理(memory pool)。每次配置一大块内存,则维护对应的自由链表(free-list),下次若再有相同大小的内存需求,就直接从 free-list 中拨出(没有就继续配置内存,具体后面讲述),如果客端释换小额区块,就有配置器回收到 free-list 中。
需要说明的是:本系列引用的STL源码版本为 v3.3,对比版本 v2.03 风格上有了些许变化,但设计思想还是不变的。
/* part of stl source code v3.3 */
enum { _ALIGN = 8 }; //小型区块的上调边界
enum { _MAX_BYTES = 128 }; //小型区块的上限
enum { _NFREELISTS = 16 }; // _MAX_BYTES/_ALIGN //free-list 编号数
//配置内存后,维护对应内存块的空闲链表节点结构
union _Obj {
union _Obj* _M_free_list_link; //空闲链表
char _M_client_data[1]; /* The client sees this. 用户使用的*/
};
考虑内存对其等因素,SGI 第二级配置器会主动将任何小额区块的内存需求量上调至 8 的倍数。并维护 16 个 free-list,各自管理大小分别为 8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128 bytes 的小额区块。
union _Obj 中的 _M_client_data 是C/C++中常见的变长数组实现方式(如果编译器支持 0 长度的数组,还可以声明为 _M_client_data[0] 达到更节省的目的),所以 union _Obj 的大小就是一个指针大小(_M_free_list_link 指针的大小)。union 实现一物二用,这个free-list 有两个作用:一个是指向下一块空白内存(当存在于 free-list 中时),一个就是供用户使用的一块内存(不存在于 free-list)。
回顾第二级配置器的设计思想的第 2 点,内存空间的分配大致是这样的:配置器分配空间时,先从 free-list 中拨出,如果有,就直接拨出,该需求大小的区块位于 free-list 对应编号的第一位置,然后从该链表中拨出,这样该区块就不位于 free-list 中对应编号内,第一位置向后移动指向,仅此一区块(刚已经拨出去),则指向 0 ,表示 free-list 中没有该大小的区块;如果没有则需要向 free-list 填补区块,继而转向内存块分配函数,然后分配所需大小的新区块(一次性缺省分配20个,不够就分配小于20的,至少一个),分配成功后,第一个区块直接划给客端,然后后面的(如果有)就填进 free-list,这样下次再有相同大小的内存需求时,可直接从 free-list 中拨出,如果一个区块都分配不出,就转向调用第一级配置器里的内存分配异常处理例程。 配置器还可以回收释放的内存,释还的小额内存区块划进 free-list 中(其实是关联到 free-list 对应编号区域)。
SGI 缺省的 free-list 中都是0值,也就是说该链表中没有可用的小额区块,因为不可能 SGI STL 一开始就自动的给你分配出来
/*源码看上去相当绕口,这里精简下*/
template <bool __threads, int __inst>
__default_alloc_template<__threads, __inst>::_Obj* volatile
__default_alloc_template<__threads, __inst> ::_S_free_list[_NFREELISTS] =
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; //一一对应大小为 8,16 ... 128 bytes 大小的区块
最开始只要需求小额区块内存,就会转到内存块分配函数。这样一来,相当于 free-list 是用来分配内存的,底层分配内存时,一次性分配一定数量的该额度区块(实际上是一次性分配大块内存,然后“分割”成小块),这样下次需求的时候直接从 free-list 中获取,而不用频繁的分配小额内存块,减少了内存碎片的产生,也提高了效率。
下面我们跟踪源码,来看看其内部是如何来实现这种机制的。
/*将 __bytes 上调至最邻近的 8 的倍数*/
static size_t
_S_round_up(size_t __bytes)
{
return (((__bytes)+(size_t)_ALIGN - 1) & ~((size_t)_ALIGN - 1));
}
/*返回 __bytes 大小的小额区块位于 free-list 中的编号*/
static size_t _S_freelist_index(size_t __bytes) {
return (((__bytes)+(size_t)_ALIGN - 1) / (size_t)_ALIGN - 1);
}
先看 allocate(),然后穿针引线
static void* allocate(size_t __n) //分配大小为 __n 的区块
{
void* __ret = 0;
if (__n > (size_t)_MAX_BYTES) {
__ret = malloc_alloc::allocate(__n); //大于128 bytes 就调用第一级配置器
}
else { //__n 大小区块对应的位置:free-lists 首地址 + __n 位于free-lists 中的编号
_Obj* __STL_VOLATILE* __my_free_list //这里是二级指针,便于调整 free-lists
= _S_free_list + _S_freelist_index(__n);
# ifndef _NOTHREADS
_Lock __lock_instance;
# endif
_Obj* __RESTRICT __result = *__my_free_list; //将对应位置的区块拨出(第一个)
if (__result == 0) //如果 free-lists 中没有对应大小的区块
__ret = _S_refill(_S_round_up(__n)); //调用 _S_refill()
else {
*__my_free_list = __result->_M_free_list_link;//这个结构有点类似链式哈希表结构,这里是指向下一块空闲内存块
//二级指针调整 free-lists,拨出去的区块就不属于该链表了
__ret = __result;
}
}
return __ret;
}
如果 free-list 中没有对应大小的区块,就转去调用 _S_refill()。
template <bool __threads, int __inst>
void* //重新填充__n大小的区块进 free-list
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20; //缺省取得 20 个新区块
char* __chunk = _S_chunk_alloc(__n, __nobjs); //调用_S_chunk_alloc()
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
/*如果只获得一个新区块,直接划给用户,free-list 仍然无新节点*/
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);
/*接下来的区块(拨出去了__n大小给用户)填补进 free-list*/
for (__i = 1;; __i++) {
__current_obj = __next_obj;
__next_obj = (_Obj*)((char*)__next_obj + __n);
/*将分配的连续大块"分割"成__n bytes大小的区块*/
if (__nobjs - 1 == __i) { //如果新区块填补完毕
__current_obj->_M_free_list_link = 0; //free-list 最后位置指向0
break;
}
else {//把_M_free_list_link当做链表的 next 指针理解
__current_obj->_M_free_list_link = __next_obj; //将各节点串联起来填进空闲链表
}
}
return(__result);
}
这样将把第一块返回给客端的剩余 “大块”内存 “分割” 成指定大小的区块,并填进 free-list 中。新的区块取自内存池,由 _S_chunk_alloc() 完成。
这个函数的具体实现思想为:
1、内存池剩余空间完全满足 20 个区块的需求量,则直接取出对应大小的空间;
2、内存池剩余空间不能完全满足 20 个区块的需求量,但可以提供一个及以上的区块,
则取出能够满足需求区块的最大个数的空间;
3、内存池剩余空间不能满足一个需求区块的大小,则进行以下处理:
1. 首先判池中是否存在残余零头的内存空间,如果有则进行回收,将其划入 free-list 中的适当位置;
2. 然后向 system heap 申请空间,补充内存池。
2.1 若 heap 空间充足,则空间分配成功;
2.2 若 heap 空间不足,出现 malloc() 调用失败。
则搜寻适当的 free-list (适当是指 “尚有未有区块,且区块较大” 的 free-list ),
即搜寻 free-list 大于等于需求块的区块,将其编入内存池,然后递归调用 _S_chunk_alloc()
函数从内存池中取空间重复上述过程。如果很不幸,free-list 中没有合适的内存空间可用了,
这时候则调用第一级配置器,利用 out-of-memory 机制尝试解决内存不足问题,
结果要么内存不足的情况获得改善,要么抛出 bad_alloc 异常。
static char* _S_start_free; //内存池起始位置
static char* _S_end_free; //内存池末端位置
static size_t _S_heap_size; //堆空间容量
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 = //剩余空间寻找适当的free-list
_S_free_list + _S_freelist_index(__bytes_left);
//调整 free-list,将内存池中残余空间编入 free-list 对应位置中
((_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); //配置heap空间,用来补充内存池
if (0 == _S_start_free) { //system heap 空间不足,分配失败
size_t __i;
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __p;
for (__i = __size; //起始大小为需求区块大小
__i <= (size_t)_MAX_BYTES;
__i += (size_t)_ALIGN) { //以 8 为步长搜寻整个 free-list
__my_free_list = _S_free_list + _S_freelist_index(__i); //找到 __i大小区块在free-list 中的位置
__p = *__my_free_list;
if (0 != __p) { //如果 free-list 中该区块未使用
*__my_free_list = __p->_M_free_list_link; //调整 free-list,释放第一位置块
_S_start_free = (char*)__p; //编入内存池
_S_end_free = _S_start_free + __i;
return(_S_chunk_alloc(__size, __nobjs)); //递归调用
/*该for循环的作用就是从 free-list 中划出大于需求区块(单个)的未用空间区块到内存池,然后再从内存池中取出。
由于从大于__size 的区块开始搜寻,所以如果 free-list 中搜寻到,那么只需动用该搜寻区块的第一位置区块即可,
最后取出的空间也可能是单个区块,也可能是多个区块(取决于 free-list 未用区块的最小区块(大于__size)的大小)*/
}
}
_S_end_free = 0; //表示到处无内存可用,for循环中free-list没有搜寻到适当的未用空间
_S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get); //调用第一级配置器
/*要么内存不足的问题获得改善,要么抛出 bad_alloc 异常*/
}
_S_heap_size += __bytes_to_get; //调用第一级配置器后,内存不足问题获得改善,调整堆空间
_S_end_free = _S_start_free + __bytes_to_get; //编入内存池
return(_S_chunk_alloc(__size, __nobjs)); //重新调用 _S_chunk_alloc()
}
这里补充一下内存池的概念:内存池是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样,内存池允许在运行期以常数时间规划内存块,并且尽量避免了内存破碎的情况产生,使得内存分配的效率得到提升。