代码改变世界

STL源码剖析之配置器(下)

2011-06-04 13:34  Aga.J  阅读(398)  评论(0编辑  收藏  举报

11 因为考虑到内存碎片的情况,所以SGI采用【双层级配置器】,第一级配置器直接使用malloc和free。而第二级则: 当 申请区超过128 byte 时,调用一级配置器。当小于128时,采用复杂的memory pool(内存池),下面介绍SGI的方法(有其自己的空间分配等)

#ifdef __USE_MALLOC

Typedef __malloc_alloc_template<0> malloc_alloc;

Typedef malloc_alloc alloc; // 令alloc为第一级配置器

#else

Typedef __default_alloc_template<__NODE_ALLOCATOR_THREAD,0> alloc;

// 令alloc为二级配置器

#endif

也就是说malloc alloc template就是第一级配置器模板default alloc template就是第二级配置器模板

Template<int inst>

Class __malloc_alloc_template //第一级配置器类模板

{

//allocate使用malloc

//deallocate使用free

//模拟C++的setNewHandler处理内存不足的情况

};

Template<bool threads,int inst>

Class __default_alloc_template //第一,二级配置器

{

//如果需求区大于128byte,就转用一级配置器

//否则二级要防止碎片发生

// 维护16个 free list

// 负责16种小块的次配置能力

// 内存池用alloc配置得到

};

然后再封装配置器功能接口

Template<class T, class Alloc>

Class simple_alloc

{

Public:

Static T *allocate(size_t n) {return 0== n? 0:(T*)Alloc::allocate(n*sizeof(T)) ;}

Static T *allocate(void) {return (T*) Alloc::allocate(sizeof(T));}

Static void deallocate(T* p,size_t n) {if (0!=n) Alloc::deallocate(p,n*sizeof(T));}

Static void deallocate(T *p){ Alloc::deallocate(p,sizeof(T));}

//将调用传递给配置器的成员函数,可能是第一级也可能是第二级

};

SGI的STL容器全部使用这个simple_alloc接口

Template<class T, class Alloc=alloc> //缺省使用alloc为配置器

Class vector

{

Protected:

Typedef simple_alloc<value_type,Alloc> data_allocator;

//使用这个simple_alloc就可以隐藏底层的byte字节参数的传入,使用类型即

Void deallocate()

{

If()

Data_allocator::deallocate( start, end_of_storage-start);

//start是value type,第二个参数则是指针差

}

};

 clip_image002

/*********

首先利用条件编译决定是使用二级配置器还是一级配置器,并且使用typedef来统一配置器的命名为alloc(默认情况下所有容器使用该配置器)

然后为每个容器加上该配置器(配置器也作为模板参数,但是默认情况下是预定义的配置器)

因为默认配置器内的allocate定义等,需要传入byte大小,所以封装这些复杂的输入,抽取出一个simple_alloc的类,来完成基本的byte转换。

最后就在每个容器中定义这个simple_alloc类,完成初始化分配

以上是一个比较完整的一级二级配置器的设计结构,暂时不涉及底层的实际分配情况

面介绍实际的__malloc_alloc_template和__default_alloca_template

*******/

 

 

12 所以最底层的实现是上面两个一级和二级配置器的实现,simple_alloc只是一个高层接口,而容器则是通过这样的配置器来完成内存空间的分配。

一级配置器 malloc_alloc_allocator二级配置器平均速度慢每次都要请求空间分配),但是空间利用率高(分配大块,满块),而且是thread safe.

 

Template<int inst>

Class __malloc_alloc_template

{

Private:

Static void *oom_malloc(size_t);

Static void *oom_realloc(void*,size_t);

Static void (* __malloc_alloc_oom_handler)();

Public:

Static void *allocate(size_t n) //实际的allocate函数(simple_alloc里面定义的)

{

Void *result = malloc(n); //实际上是调用C的malloc来实现,一旦分配失败,则使用oom_malloc

If( 0 == result) result=oom_malloc(n); //无法满足需求时,使用oom_malloc

Return result;

}

Static void deallocate(void *p,size_t )

{

Free(P);                  //直接使用C的free

}

Static void *reallocate(void *p, size_t oldSize, size_t new_sz)

{

Void *result = realloc(p,new_sz); //还是调用底层的realloc

If (0==result) result=oom_realloc(p,new_sz);  //下面有这个函数的实现

Return result;

}

Static void (* set_malloc_handler(void (*f) () ) ) ()

{

Void (*old)() = __malloc_alloc_oom_handler; //转换为void指针

__malloc_alloc_oom_handle=f; //然后指向f

Return (old); //返回这个指针

}

//模仿C++的set_new_handler,我们可以自己指定out of memory handler

};

Template<int inst>

Void (*__malloc_alloc_template<inst>::__malloc_alloc_com_handler ) () =0;

Template<int inst>

Void *__malloc_alloc_template<inst>::oom_malloc(size_t n)   //oom_malloc的实现

{

Void (*my_malloc_handler)();

Void *result;

For(;;)                                        //循环直到分配成功

{

My_malloc_handler = __malloc_alloc_oom_handler;

//指向预定义的例程地址,通过调用static void(* set_malloc_handler(void(*f)()))()来设定执行内容

If( 0== my_malloc_handler)

   {__THROW_BAD_ALLOC; } //未定义

else

   (*my_malloc_handler)(); //如果定义了,就调用例程

 

Result = malloc(n); //result = realloc(p,n); //并且重新分配,知道分配成功

If( result) return (result);

}

}

//因为不是使用C++的::operator new,所以不能直接使用C++的new handler只能仿真

//在该(分配失败处理)中,不断调用内存不足处理例程”,也就是这里的my_malloc_handler,期望在某次调用后,可以获得内存完成任务,而如果“该例程没有被客户端设置,则直接抛出错误”。

Typedef __malloc_alloc_template<0> malloc_alloc;

这是一级配置器的分配过程及其分配失败的处理过程(模拟了C++的new handler,完成自己的异常处理程序)

 

13 二级配置器 __default_alloc_template

二级配置器能帮助我们避免太多小额区块所造成的内存碎片,但是小额碎片在配置时会有标志区域(这会带来额外的负担),所以小额区域越小,负担越大。(既需要一个pa,又需要使用cookie来记录内存的大小)

 clip_image004

二级配置器的做法是:如果块够大,超过128byte时,则移交第一级配置器处理,当块小于128时,则以内存池(memory pool)管理:每次配置一大块内存,并维护所对应的free list,如果下次有相同大小的请求,则直接从free list中取出。如果释放了 ,则回收到free list中。(为了配置方便,二级配置器会将小额内存需求量上调至8的倍数)然后维护16个free list,每个list管理 8,16。Xxxx的区域大小

一种free list的定义可能是这样

Struct Node

{

Int size;

Char client_data[];

Node* next;

};

这样一来,每个节点都需要一个额外的指针来指向下一个节点,这会带来新的负担。所以使用下面的定义方式

Union obj

{

Union obj *free_list_link;

Char client_data[];

};

利用union的好处,第一个字段可以指向另一个obj,第二个字段可以指向 实际区块。(怎样做到节省了内存?只有空闲时才使用第一个字段,被分配时会使用第二个字段),也就是说,元素的free—list只保存了下一个节点的地址,也就是使用了第一个obj,然后再被申请后,就会使用第二个字段(这时候也没必要维护它在free中的位置),所以很巧妙。

 clip_image006

实现:

Enum { __ALIGN = 8};

Enum { __MAX_BYTES = 128};

Enum { __NFREELISTS = __MAX_BYTES/__ALIGN};

Template<bool threads, int inst>

Class __default_alloc_template

{

Private:

Static size_t ROUND_UP(size_t bytes) //将byte上调到8的倍数

{ return  ((bytes)+8 – byte+8 )% 8}

{ return ( ((bytes)+__ALIGN-1) & ~(__ALIGN -1 ) ) ; }

//思想:加上8后,进一步对齐,然后再取出最后3位,保证没有多余的数,这样就可以整除8了

Private:

Union obj //高效的节点定义方法

{

Union obj *free_list_link;

Char client_data[1];

};

Private:

Static obj *volatile free_list[__NFREELISTS];

//max128,每个块对齐8,所以需要128/8个list槽

 

Static size_t FREELIST_INDEX (size_t bytes) //根据所需内存块的大小,决定使用

{ return (((bytes)+__ALIGN -1 )/ __ALIGN -1 ); }//第n号的free-list

Static void *refill (size_t n); //加入free list

Static char *chunk_alloc(size_t size, int &nobjs); //配置一个可以容纳多个size的区域

 

Static char *start_free; //内存池起始位置

Static char *end_free;

 

Static size_t heap_size;

Public:

Static void *allocate( size_t n) {}

Static void deallocate ( void *p, size_t n){}

Static void *reallocate( void *p, size_old_sz, size_t new_sz):

};

Template<bool threads,int inst>

Char *__default_alloc_template<threads, inst>::start_free=0;

 

Template<bool threads,int inst>

Char *__default_alloc_template<threads,inst>::end_free=0;

 

Template<bool threads,int inst>

Size_t __default_alloc_template<threads,inst>::heap_size = 0;

 

Template<bool threads, int inst>

__defualt_alloc_template<threads,inst>::obj *volatile __default_alloc_template<threads,inst>::free_list __NFRESSLISTS ={0};

//二级配置器,使用内存池的概念,主要是维护一个free list列表,每个free list都对应一种大小的空闲区域块。(辅助函数是上调块大小返回对应free list号),然后主要完成allocate,deallocate,reallocate(这些函数再调用私有分配函数完成

    做为配置器,__default_alloc_template拥有配置器的标准接口函数allocate(这就是为什么定义一个新的分配接口) 当调用allocate时,先判断块的大小,如果大于128就使用第一级的配置器,小于128就根据实际byte大小检查对应的free list,如果有可用的块,就拿来使用,如果没有的话,就上调至8倍数的边界,然后refill,

Static void *allocate(size_t n)

{

Obj *volatile *my_free_list;

Obj *result;

If ( n> (size_t)__MAX_BYTES)        //判断请求的块的大小,根据大小来决定使用一级配置器还是二级配置器

{

Rerurn (malloc_alloc::allocate(n)); //调用一级配置器

}

My_free_list = free_list + FREELIST_INDEX(n); //指向数组的一个空闲的slot,这个slot的可分配块的大小和n对齐到8的倍数后的值适配。

Result = my_free_list;    //得到这个list下面的第一个空闲块

If( result == 0) //没有可用的空间

{

Void *r= refill( ROUND_UP(n));  

Return r;

}

*my_free_list = result->free_list_link; //指向下一个未分配的node

Return result;

}

 clip_image008

Static void deallocate( void *p,size_t n)

{

Obj *q=(obj*)p;

Obj * volatile *my_free_list;

If ( n>(size_t)__MAX_BYTES)

{

Malloc_alloc::deallocate(p,n);

Return;

}

My_free_list = free_list + FREELIST_INDEX(n);

q->free_list_link = *my_free_list;

*my_free_list=q;                                             //见下图说明

//q里面的脏数据怎么解决???delete []q.data;

}

 clip_image010

前面allocate时,如果free list没有可用的块时,则使用refill,即为free list重新填充空间。新的空间有chunk_alloc完成

Template<bool threads, int inst>

Void* __default_alloc_template<threads,inst>::refill(size_t n)

{

Int nobjs=20;

Char *chunk = chunk_alloc( n,nobjs); //分配20个新节点,大小为n

Obj *volatile *my_free_list;

Obj *result;

Obj *current_obj, *next_obj;

Int I;

If (1==nobjs); return (chunk); //仅仅获取一个,就给调用者使用

My_free_list = free_list+ FREELIST_INDEX(n);

Result = (obj*) chunk; //第一块准备返回给客户

*my_free_list = next_obj =(obj*)(chunk+n); //free指向新配置的空间

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);

}

内存池 memory pool chunk_alloc的源码

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)

{

Result = start_free;

Start_free += total_bytes;    //水量满足,交付预定大小的空间

Return (result);

}

Else if ( bytes_left >= size)   //剩余水量大于一个请求的大小

{

Nobjs = bytes_left/size;       //把剩下的也交付出去

Total_bytes=size*nobjs;

Result= start_free;

Start_free+=total_bytes;

Return result;

}

Else

{

….

}

}

内存池的操作:

1 计算 内存池(一个数组,每个slot维护一条list,该list的节点统一大小,slot之间的list大小有递增关系)的剩余空间, 计算 所需要的总数空间

2 如果 空间满足,则返回其实位置,并重新设定剩余空间大小

3 如果不能满足,但是足够提供一个以上的区块,则给出这么多个区块的空间

4 如果一个区块都不能满足,??

clip_image012

clip_image014

clip_image016

14 内存基本处理工具

STL定义了5个全局的函数来操作 已经分配但是未初始化 的空间。

Construct,destroy,uninitialized_copy, uninitialized_fill, uninitialized_fill_n

1) uninitialized_copy(InputIterator first, InputIterator last, ForwardIterator result)

如果目的地 result,result+(last-first) 未初始化,那么这个函数会使用 复制构造函数为first到last之间的对象产生一个copy [ construct ( & * (result +(i-first)), *i ) ]

2) uninitialized_fill ( ForwardIterator first, ForwardIterator last, const T& x);

如果first到last指向的目标区域都指向没有初始化的内存,那么该函数会产生x类型的复制品。

3) uninitailized_fill_n