~~~~ 之前我们了解了一些基础的东西,我们下面来学习一下stl里面的经典,我们只学习SGI,关于源码,后面整理上传到gihub吧。
SGI STL主页:http://www.sgi.com/tech/stl/index.html
STLport主页:http://www.stlport.org/
找到的源码,SGI源码,感谢开源:
https://github.com/karottc/sgi-stl
https://github.com/CaesarTang/STL
~~~~ 我们先看看SGI的对于内存的配置和释放的设计哲学:
向system heap要求空间
考虑多线程(multi threads)状态
考虑内存不足时的应变措施
考虑过多“小型区块”可能造成的内存碎片(fragment)问题。
~~~~
为了不把问题复杂化,我下面写的会比较简单,而且暂时移除多线程状态处理,我们要先入门才行。
~~~~
C++内存配置和释放基本操作就是new和delete,在C里面就相当于malloc和free函数,而我们学习的SGI使用的正式malloc和free完成内存配置和释放。
~~~~
就像我们之前说小型块内存会造成内存碎片的问题,SGI就有为此设计了双层配置器,第一辑配置器直接使用malloc和free,而第二级配置器视情况采用不同的策略。
~~~~
当配置区块超过128bytes时,视为足够大,调用第一级配置器;当配置区块小于128bytes时,视为过小,为了降低额外负担(overhead),便采用复制的memory pool整理方式,而不再求助于第一级配置器,整个设计到底使用哪一个级别的配置器,取决于__USE_MALLOC是否被定义;
~~~~
找了一下SGI的stl_alloca.h的源码,github上面有人上传了,关于选择一级配置器和二级配置器的地方。
...
// SGI STL 第一级配置器
// 无 “template 类型参数”,“非类型参数 __inst”,完全没有用
template <int __inst>
class __malloc_alloc_template {
...
};
...
typedef __malloc_alloc_template<0> malloc_alloc;
...
template <bool threads, int inst>
class __default_alloc_template {
...
}
...
# ifdef __USE_MALLOC
typedef malloc_alloc alloc; // 令 alloc 为第一级配置器
...
# else
...
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc; // 令 alloc 为第二级配置器
...
# endif
可以明显的看到__malloc_alloc_template就是一级配置器,__default_alloc_template就是二级配置器,我们也可以看到,它们都不接受任何template参数,所以它们不符合STL规格,SGI需要为它包装一个接口,用来符合STL规格:
template<class _Tp, class _Alloc>
class simple_alloc {
public:
static _Tp* allocate(size_t __n)
{ return 0 == __n ? 0 : (_Tp*) _Alloc::allocate(__n * sizeof (_Tp)); }
static _Tp* allocate(void)
{ return (_Tp*) _Alloc::allocate(sizeof (_Tp)); }
static void deallocate(_Tp* __p, size_t __n)
{ if (0 != __n) _Alloc::deallocate(__p, __n * sizeof (_Tp)); }
static void deallocate(_Tp* __p)
{ _Alloc::deallocate(__p, sizeof (_Tp)); }
};
第一级适配器
~~~~ 在SGI中第一级适配器为__malloc_alloc_template,我们直接看代码解释
//malloc-based allocator. 通常比稍后介绍的default alloc速度慢
//一般而言是thread-safe,并且对于空间的运用比较搞笑
//一下是第一级适配器
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:
static void* allocate(size_t __n)
{
void* __result = malloc(__n); //一级适配器直接使用malloc
//无法满足需求时
if (0 == __result) __result = _S_oom_malloc(__n);
return __result;
}
static void deallocate(void* __p, size_t /* __n */)
{
free(__p);//以及适配器直接使用free
}
static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
{
void* __result = realloc(__p, __new_sz);//第一季适配器直接使用realloc
//无法满足需求时,改用_S_oom_realloc
if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
return __result;
}
//以下仿真C++的set_new_handler换句话说,你可以通过它指定你自己的out-of-memory handler
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
//初值为0,有待客端设定
#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>::_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);
}
}
template <int __inst>
void* __malloc_alloc_template<__inst>::_S_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);
}
}
//注意以下直接将参数inst指定位 0
typedef __malloc_alloc_template<0> malloc_alloc;
~~~~
第一级适配器以malloc,free,relloc等C函数执行实际的内存配置,是否,重配置操作,并实现出类似C++new-handler的机制,是的,它不能直接运用C++new-handler机制,因为它并非使用new来配置内存。
~~~~
所谓C++new-handler机制是,你可以要求系统在内存配置需求无法被满足时,调用一个你所指定的函数,换句话说,一旦new无法完成任务,在丢出__THROW_BAD_ALLOC异常状态之前,会先调用由客端指定的处理例程,改处理例程通常即被称为new-handler,new-handler解决内存不足的做法有特定的模式。
~~~~
注意,设计内存不足处理例程是客端的责任,设定内存不足处理例程,也是客端责任,再一次提醒你,内存不足处理例程解决问题的做法有着特定的模式。
第二级适配器
~~~~
第二级适配器多了一些机制,避免太多小额区块造成内存碎片,小额区块带来的其实不仅仅是内存碎片,配置时的额外负担也是一个大问题,额外负担永远无法避免,毕竟系统要靠这多出来的空间来管理内存,如图2-3所示,但是区块俞小,额外负担所占比例就愈大,愈显得浪费。
~~~~
SGI第二级适配器的做法是,如果区块足够大没超过128bytes时,就移交给第一适配器处理,当区块小于128bytes时,则以内存池管理,此法又称为次层配置,每次配置一大块内存,并维护对应之自由链表free-list,下次若再有相同大小的内存需求,就直接从free-list中拔出。如果客端是更小的区块,就由配置器回收到free-list中,同时SGI会主动将小额区块的内存需求上调到8的倍数,同时维护这16个free-list,各自管理大小分别为8,16,24,32,40,48,56,64…128bytes的小额区块,free-list的节点结构如下
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
};
~~~~
第一次看到这个共同体我是没有理解书写者的意图的,知道看完二级适配器的代码,我才觉得这里的正确性,回到stl解析的一句话,stl里面的源代码写得比你甚至是大多数人都要好。
~~~~
下面我们来看源码
//下面是二级配置器
//注意template的参数,第一个为线程安全,第二个完全没有使用
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.
#if ! (defined(__SUNPRO_CC) || defined(__GNUC__))
enum {_ALIGN = 8}; //小区块的上调边界
enum {_MAX_BYTES = 128}; //小区块的上限
enum {_NFREELISTS = 16}; // _MAX_BYTES/_ALIGN free_list的个数
# endif
//_S_round_up将bytes上调至8的倍数
static size_t
_S_round_up(size_t __bytes)
{ return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1)); }
__PRIVATE:
union _Obj {//free-list的节点构造
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
};
private:
# if defined(__SUNPRO_CC) || defined(__GNUC__) || defined(__HP_aCC)
static _Obj* __STL_VOLATILE _S_free_list[];
// Specifying a size results in duplicate def for 4.1
# else
//16个free-list
static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS];
# endif
//以下函数根据区块大小,决定使用第n号free-list,n从 起算
static size_t _S_freelist_index(size_t __bytes) {
return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1);
}
//返回一个大小为n的对象,并可能加入大小为n的其他区块到free-list
static void* _S_refill(size_t __n);
//配置一大块空间,可以容纳__nobjs个大小为__size的区块
//如果配置__nobjs个区块有所不便,__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;
public:
/* __n must be > 0 */
static void* allocate(size_t __n);
/* __p may not be 0 */
static void deallocate(void* __p, size_t __n);
static void* reallocate(void* __p, size_t __old_sz, size_t __new_sz);
} ;
~~~~
我们下面来着重分析其中的几个函数
~~~~
第一个allocate,内存配置函数,身为一个配置器,__default_alloc_template 拥有配置器的标准接口函数allocate,次函数首先判断区块大小,大于128的就调用第一级适配器,小于就调用对应的free-list,如果free-list有可用的区块,就直接拿来用,如果没有就调用_S_refill为free-list重新填充空间,_S_refill这个函数我们后面介绍。
~~~~
在这里如果free-list的16链表中对应的链表里可用的区块,就取对应链表的首个区块,同时将区块移除链表,free-list对应直线的首地址,向链表后移;然后我们后面就会发现,当释放区块的时候,同样区块插入的位置也是链表首部,这其实就是一个栈,16个free-list就是16个栈。
// __n必须大于0
static void* allocate(size_t __n)
{
void* __ret = 0;
//大于128就调用第一配置器
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n);
}
else {
//寻找16个free-list中适当的一个
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
_Obj* __RESTRICT __result = *__my_free_list;
if (__result == 0)
//没有找到可用的free-list,准备重新填充free-list
//同时获取一个申请好的符合要求的区块
__ret = _S_refill(_S_round_up(__n));
else {
//调整free-list
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
}
}
return __ret;
};
~~~~
第二个函数deallocate,空间释放函数,作为为一个适配器,同样拥有适配器标准接口函数deallocate。该函数首先判断区块大小,大于128bytes就调用第一级适配器,小于128bytes就找到对应的free-list,将区块收回。
~~~~
如果小于128bytes的区块,我们就是直接插入到对应free-list对应链表的首部。
// __p必须不为空
static void deallocate(void* __p, size_t __n)
{
//大于128就调用第一配置器
if (__n > (size_t) _MAX_BYTES)
malloc_alloc::deallocate(__p, __n);
else {
//寻找对应的free-list
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
_Obj* __q = (_Obj*)__p;
// acquire lock
# ifndef _NOTHREADS
/*REFERENCED*/
_Lock __lock_instance;
# endif /* _NOTHREADS */
//调整区块,回收区块,将回收的区块放到链表首地址
__q -> _M_free_list_link = *__my_free_list;
*__my_free_list = __q;
// lock is released here
}
}
~~~~ 下面就是重新填充函数_S_refill,当没有可用的区块时,就调用_S_refill,准备为free-list重新填充空间,新的空间将取自内存池,经由_S_chunk_alloc函数完成(下面会继续讲),缺省得到20个新的节点,但是如果内存池不足,可能获取的节点数小于20,这里要注意__nobjs是引用传进去的。
//返回一个大小为__n的对象,并且有时候会为适当的free-list增加节点
//假设__n已经适当调节至8的倍数,这了的__n必须先进行调节
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20;
//调用_S_chunk_alloc,尝试取得__nobjs个区块作为free-list的新节点
//注意参数__nobjs是通过引用传递到_S_chunk_alloc中的
//需要通过返回值来判断实际的申请大小
char* __chunk = _S_chunk_alloc(__n, __nobjs);
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
//如果只获取到了一个区块,这个区块就分配给调用者用,free-list无新节点
if (1 == __nobjs) return(__chunk);
//否则准备调整free-list,纳入新节点,重新获取free-list的位置
__my_free_list = _S_free_list + _S_freelist_index(__n);
//在__chunk空间中建立free-list
__result = (_Obj*)__chunk;//这一块返回给客端
//以下导引free-list指向新配置的空间,第一个返回给客端了所以需要+ __n
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
//将free-list的各个节点串连起来
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);//返回配置好的区块
}
~~~~ 而从内存池中取空间给free-list使用,是_S_chunk_alloc做得事情:
//从内存池中取空间给free-list使用,是_S_chunk_alloc的工作
//主要负责在大块中分配内存,防止内存碎片
//__size必须是上调到8的倍数,也就是内存对齐
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为当前可分配最大值
__nobjs = (int)(__bytes_left/__size);
__total_bytes = __size * __nobjs;
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
} else {
//当剩余空间不足以分配哪怕一个__size大小的区块
size_t __bytes_to_get =
2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
//先试着让内存池里面的残余零头还有利用价值
if (__bytes_left > 0) {
//内存池内还有一些零头,先分配给适当的free-list,先寻找适当的free-list
_Obj* __STL_VOLATILE* __my_free_list =
_S_free_list + _S_freelist_index(__bytes_left);
//调整 free-list,将内存池中的残余空间编入
((_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) {
//head空间不足,malloc失败
size_t __i;
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __p;
//尝试使用我们手上有的没有使用的空间,就是那些还没有被分配的
//在free-list上的且足够大的区块
//并不打算配置较小的区块,因为拿在多进程机器上容易导致灾难
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;
//递归自己,同时修正__nobjs
return(_S_chunk_alloc(__size, __nobjs));
//注意任何的残余零头终被编入适当的free-list中备用
}
}
_S_end_free = 0; // 到处都没有内存可以使用了
//调用一级适配器,看看out-of-memory机制是否能出力(new-handler)
_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;
//调用递归,修正__nobjs
return(_S_chunk_alloc(__size, __nobjs));
}
}
~~~~ 上面的函数以_S_end_free和_S_start_free来判断内存池的水量,如果水量充足,就直接调出20个区块返回给free-list,如果水量不足,但是足够提供至少一个以上的区块,就拔出这不足20各区块的空间出去,这时候其引用传递的__nobjs就可以将实际的供应区块数返回到上一层。如果内存池中连一个区块都无法提供,如果剩余空间不为空,则将剩余空间分配给free-list的其他可用的链表,然后去系统的heap中配置内存空间,为内存池注入内存以应付需求,新的内存申请大小为需求的两倍加上一个配置次数而愈来愈大的附加量。