STL源码剖析allocator

查找C++primer中的allocate

具备次配置力(sub-allocation)的SGI空间配置器

SGI标准的空间配置器,std::allocator

        SGI定义了一个符合部分标准,名为allocator的配置器,效率不高,只把c++的::operator new和::operator delete做了一层薄

薄的包装,SGI没有用过 。主要原因是没有考虑到任何效率的优化。

SGI特殊的空间配置器,std::alloc

       通常在SGI STL中使用缺省的空间配置器,很少需要自行制定配置器名称。SGI STL中的每一个容器都已经制定缺省的空间配置器

为alloc。

       一般而言,我们习惯的C++内存配置操作和释放操作是这样的:

class Foo {...};
Foo *pf=new Foo;
delete pf;

new算式含两个阶段的操作:

  1. ::operator new配置内存。
  2. 调用Foo::Foo()构造对象内容。

delete算式也含有两个阶段的操作:

  1. 调用Foo::~Foo()将对象析构。
  2. 调用::operator delete将内存释放。

STL allocator将上述两阶段操作区分开来。内存配置由alloc::allocator()负责,内存释放操作由alloc::deallocator()负责;对象构造由::constructor()负责,对象析构由::destroy()负责。

STL标准告诉我们,配置器定义于<memory>之中,SGI<memory>中包含以下两个文件:

#include<stl_alloc.h> //负责内存空间的配置和释放

#include<stl_construct.h> //负责对象内容的构造与析构

析构和构造的基本工具,construct()和destroy()

下面是#include<stl_construct.h>的部分内容:

 #include <new.h>                //placement new头文件
  template <class T1, class T2>  
  inline void construct(T1* p, const T2& value) {  
    new (p) T1(value);            //placement new;调用T1::T1(value)
  }  
  ​
  //以下是destroy()第一个版本,接受一个指针  
  template <class T>  
  inline void destroy(T* pointer) {  
      pointer->~T();  
  }  
  ​
  //destory()第二版本,接受两个迭代器。此函数没法找出元素数值型别
  template <class ForwardIterator>  
  inline void destroy(ForwardIterator first, ForwardIterator last) {  
    __destroy(first, last, value_type(first));  
  }  

  template <class ForwardIterator>  
  inline void __destroy(ForwardIterator first, ForwardIterator last,T*) { 
    typedef typename __type_trauts<T>::has_trivial_-destructor trivial_destructor;
    __destroy(first, last, trivial_destructor());  
  }  

  template <class ForwardIterator>  
  inline void  
  __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) {  
    for ( ; first < last; ++first)  
      destroy(&*first);  
  }  

  template <class ForwardIterator>   
  inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {} 

上述destroy()的第一版本接受一个指针,将该指针所指的对象析构掉。

第二版本接受first和last两个迭代器,将这两个迭代器范围内的对象析构掉。

由于不能确定这个范围有多大,当范围很大而且每个对象的析构函数是无意义的、无关痛痒的,那么频繁调用这些析构函数对效率是

一种伤害。因此,手里利用value_type()来获取迭代器所指对象的型别,再利用__type_traits<T>判断该型别的析构函数是否无关痛

痒。如果是true_type,STL就什么都不做;如果是false_type,就会调用每个对象的析构函数来销毁这组对象。

在第二版本中运用了traits编程技法,traits会得到当前对象的一些特性,再根据特性的不同分别对不同特性的对象调用相应的方法。

在第二版本中,STL会分析迭代器所指对象的has_trivial_destructor特性的类型(只有两种:true_type和false_type)。

空间的配置与释放std::alloc

SGI对此的设计哲学如下:

  • 向system heap要求空间。
  • 考虑对线程状态。(先不考虑)
  • 考虑内存不足时的应变措施。
  • 考虑过多“小型区块”可能造成的内存碎片问题。
  • 考虑到小型区块所可能造成的内存破碎问题,SGI设计了双层级配置器。

第一级配置器直接使用malloc()和free()。

第二级配置器则视情况采用不同的策略。当配置区超过128bytes时,视为足够大,便调用第一级配置器,配置区小于128bytes时,

视之过小,采用复杂的内存池整理方式。


C++中new/delete和malloc/free的区别:

  • malloc与free是C/C++的标准库函数,new/delete 是C++的运算符。
  • malloc 返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型。
  • new操作失败的话,报错bad_alloc,malloc失败的话返回NULL。
  • new自动计算需要分配的空间,而malloc需要手工计算字节数。
  • new是类型安全的,而malloc不是。

无论alloc被定义为第一级或第二级配置器,SGI还为它再包装了一个接口如下,使得配置器的接口能够符合STL规格:

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

第一级配置器__malloc_alloc_template 

class __malloc_alloc_template{//一部分代码
    static void * allocate(size_t n)  
    {  
        void *result = malloc(n);   //直接使用malloc()  
        if (0 == result) result = 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()  
        if (0 == result) result = oom_realloc(p, new_sz);  
        return result;  
    } 
}

        第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重配置操作,并实现出类似C++ new-handler的机制(::operator new才有new-handler机制)。

        new-handler机制:你可以要求系统在内存配置需求无法被满足的时候,调用一个你指定的函数,也就是在抛出bad_alloc异常之前,会先调用由客端指定的处理例程。

        SGI的做法是,在调用malloc和realloc不成功后,改调用oom_malloc()和oom_realloc(),这两者都有内循环,不断地调用“内存不足处理例程”,期望在某次调用之后,获得足够的内存而圆满完成任务。但如果这个“内存不足处理例程”未被客端设定,就抛出bad_alloc异常啦。(oom:out of memory)。

第二级配置器__default_alloc_template

    第二级空间配置器多了一些机制,避免太多小额区块造成内存的碎片。小额区块带来的不仅是内存碎片,配置时的额外负担也是一个问题。SGI第二级配置器的做法是,如果区块够大,超过128bytes时,就移交第一级配置器。当区块小于128bytes时,则以内存池(memory pool)管理,此法又称为层次配置(sub-allocation):每次配置一大块内存,并维护对应自由链表(free-list)。下次如若再有相同大小的内存需求,就直接从free-lists中拨出。如果客户端释还了小额区块,就由配置器回收到free-lists中,配置器除了负责配置也方便回收。

    SGI STL的第二级内存配置器主动将任何小额区块的内存需求量上调至8的倍数(例如客户端需求30bytes。就自动调整为32bytes),并维护了一个free-list数组,分别用于管理8, 16, 24, 32,40,56,64,72,80,88,96,104,112,120,128 bytes的小额区块,free-list的节点结构如下:

  union obj
  {
      union obj* free_list_link;
      char client_data[1];
  };

​    这里使用union结构,是为了节省空间,也就是说,当节点位于free-list时,通过free_list_link指向下一块内存,而当节点取出来分配给用户使用的时候,整个节点的内存空间对于用户而言都是可用的,这样在用户看来,就完全意识不到free_list_link的存在,可以使用整块的内存了。


union是一个特殊的结构体,与struct不同

  1. 成员只能是基础数据类型;
  2. 所有成员共享同一片内存;
  3. 实际上只有一个数据成员被使用。

​    在分配内存时,会将大小向上调整为8的倍数,因为free-list中的节点大小全是8的倍数。

enum {__ALIGN=8};
static size_t ROUND_UP(size_t bytes)
{
    return (((bytes)+__ALIGN-1)&~(__ALIGN-1));//n+7和~7的进行与运算
}

第二级配置器的做法:如果申请内存大于128bytes时,就用第一级配置器。小于128bytes时,以内存池管理:每次配置一大块内存,并维护相应的自由链表。

具体分配过程:

allocate()函数处理过程:


在该函数实现的代码中设计到volatile关键字,volatile是一个类型修饰符,每次访问被volatile修饰的变量的时候,系统总是重新从它所在的内存进行访问。


1)如果申请内存大于128bytes,就调用第一级配置器,否则说明申请内存小于128bytes,转到2)

2)根据申请内存的大小n在16个free lists中找到其对应大小的my_free_list

3)如果对应的my_free_list中没有空闲区块,分配器首先将申请的内存大小上调至8的倍数n,调用refill(),准备重新填充my_free_list

4)否则说明有可用的空闲区块,更新my_free_list,使得其指向当前被分配区块的下一个区块并将当前被分配的区块的地址返回

refill()函数的处理过程:

1)调用chunk_alloc()函数申请20*n(n为区块大小)的内存空间(不一定取得到)

template <bool threads,int inst>
void* __default_alloc_template<threads,inst>::refill(size_t n)
{
    int nobjs=20;//默认是取20个区块
    //这里注意nobjs是引用传递,所以返回的不一定是20个区块
    char* chunk=chunk_alloc(n,nobjs);
...
}

2)如果只获得一个大小为n的区块,这个区块就分配给调用者,否则从获得的多个区块中取出一块分配给调用者,其余的用在my_free_list上串接起来。(这里的串联方式,是将新增的头指向my_free_list,将原来的接在新增的尾部)。

chunk_alloc()函数处理过程:

1)如果内存池剩余空间大于或等于20*n的内存空间,则从这个空间中取出n*20大小的内存空间,更新start_free并返回申请到的内存空间的其实地址,否则转到2)

2)如果内存池剩余空间足够分配一个及以上的区块,则分配整数倍于n的内存空间,更新start_free,由nobjs返回实际分配到的区块个数,并返回申请到的内存空间的其实地址,否则转到3)

3)内存池中无法提供一个大小为n的区块,此时如果内存池中还有一些残余内存(这些内存大小小于n),则将这些内存插入到其对应大小的空闲分区链中

4)调用malloc向运行时库申请大小为(2*20*n+附加量)的内存空间,如果申请成功,更新start_free,end_free 和 heap_size,并递归调用chunk_alloc(),修正nobjs,否则转到5)

5)4)中调用malloc失败,此时分配器依次遍历区块足够大的free_lists,只要有一个未用区块,就释放该区块,递归调用chunk_alloc(),修正nobjs

6)如果出现意外,到处都没有内存可用了,则调用第一级配置器,看out-of-memory机制能否尽点力

template <bool threads,int inst>
void* __default_alloc_template<threads,inst>::
chunk_alloc(size_t size,int& nobjs)
{
...
    //当内存池剩余空间需求量在1~nobjs之间的时候
    nobjs=bytes_left/size;//修改nobjs
...
    //当内存池剩余空间需求量连一个区块的大小都不满足的时候
    size_t bytes_to_get=2*total_bytes+ROUND_UP(heap_size>>4);
    //这里total_bytes为所需要获取的总比特数(=区块数*区块大小)
    //heap_size为堆大小
    //所以每次的附加量为堆大小/16,再调整至8的整数倍
...
}

具体内存释放过程(deallocate函数处理过程):
先判断要释放的背刺区块的大小,大于128bytes就调用第一级配置器释放内存,否则要释放的内存区块小于128bytes,就找出相应的free_list,将区块回收

内存基本处理工具

STL定义了五个全局函数,作用在未初始化的内存空间上。用于构造的construct(),用于析构的destroy()以及uninitialized_copy()、

uninitialized_fill()和uninitialized_fill_n()对应高层次的函数copy(),fill(),fill_n();


POD型别

Plain Old Data,也就是标量型别scalar types或传统的C struct型别。

POD型别必然拥有trivial ctor/dtor/copy/assignment函数。

因此,对POD型别采用最有效率的初值填写手法,而对non-POD型别采用最保险安全的手法。


uninitialized_copy()

当实现一个容器的时候,uninitialized_copy()函数会带来很大的帮助,有助于将容器的内存配置和对象构造分离开来

容器的全区间构造函数通常以两个步骤完成:

  • 配置内存区块,足以包含范围内的所有元素。
  • 使用uninitialized_copy(),在该内存区块上构造元素。

 C++ 标准规格书要求 uninitialized_copy() 具有 "commit or rollback" 语意, 意思是要不就「构造出所有必要元素」,要不就(当有任何一个 copy constructor 失败时)「不构造任何东西」。

首先萃取出result的value_type,然后再判断该型别是否是POD型别。

/*
	参数说明:
		1.迭代器first指向欲初始化空间的起始处
		2.迭代器last指向输入端的结束位置(前闭后开区间)
		3.迭代器result指向输出端(欲初始化空间)的起始处
*/
template <class InputIterator ,class ForwardIterator>
inline ForwardIterator uninitialized_copy(InputIterator first,InputIterator last,ForwardIterator result)
{
	return __uninitialized_copy(first,last,result,value_type(result));//萃取
}
 
template <class InputIterator ,class ForwardIterator,class T>
inline ForwardIterator __uninitialized_copy(InputIterator first,InputIterator last,ForwardIterator result,T*)
{
	typedef typename __type_traits<T>::is_POD_type is_POD;
	return __uninitialized_copy_aux(first,last,result,is_POD());//判断是否是POD
}
 
template <class InputIterator ,class ForwardIterator,class T>
inline ForwardIterator __uninitialized_copy_aux(InputIterator first,InputIterator last,ForwardIterator result,__true_type)
{
	return copy(first,last,result);
}
 
template <class InputIterator ,class ForwardIterator,class T>
ForwardIterator __uninitialized_copy_aux(InputIterator first,InputIterator last,ForwardIterator result,__false_type)
{
	ForwardIterator cur = first;
	for (;first != last;++first,++cur)
	{
		construct(&*cur,*first);
	}
	return cur;
}

对于char*和wchar_t*两张型别,可以采用最具效率的做法memmove(直接移动内存内容)

//针对char*和wchar_t*两种类型,以最具效率的memmove来执行赋值行为
inline char* uninitialized_copy(const char* first,const char* last,char* result)
{
	memmove(result,first,last-first);
	return result + (last - first);
}
 
inline wchar_t* uninitialized_copy(const wchar_t* first,const wchar_t* last,wchar_t* result)
{
	memmove(result,first,sizeof(wchar_t) * (last - first));
	return result + (last - first);
}

uninitialized_fill()

         uninitialized_fill()也能够使我们将内存配置与对象的构造行为分离开来。 如果 [first,last) 范 围内的每个迭代器都指向未初 始化的内存 ,那么 uninitialized_fill() 会在该范围内产生x(上式第三参数)的复制品。换句话说 uninitialized_fill()会针对操作范围内的每个迭代器 i , 呼叫 construct(&*i, x),在 i 所指之处产生 x 的复制品。

        和 uninitialized_copy() 一样,uninitialized_fill() 必须具备 "commit or rollback" 语意,换句话说它要不就产生出所有必要元素,要不就不产生任何元素。 如果有任何一个 copy constructor 丢出异常(exception)uninitialized_fill() 必须能够将已产生之所有元素析构掉。

/*
	参数说明:
		1.迭代器first指向欲初始化空间的起始处
		2.迭代器last指向输入端的结束位置(前闭后开区间)
		3.x表示初值
*/
template <class ForwardIterator,class T>
inline void uninitialized_fill(ForwardIterator first,ForwardIterator last,const T& x)
{
	__uninitialized_fill(first,last,x,value_type(first));
}
 
template <class ForwardIterator,class T,class T1>
inline void __uninitialized_fill(ForwardIterator first,ForwardIterator last,const T& x,T1*)
{
	typedef typename __type_traits<T1>::is_POD_type is_POD;
	__uninitialized_fill_aux(first,last,x,is_POD());
}
 
template <class ForwardIterator,class T>
inline void __uninitialized_fill_aux(ForwardIterator first,ForwardIterator last,const T& x,__true_type)
{
	fill(first,last,x);
}
 
template <class ForwardIterator,class T>
void __uninitialized_fill_aux(ForwardIterator first,ForwardIterator last,const T& x,__false_type)
{
	ForwardIterator cur = first;
	for (;cur != last;++cur)
	{
		construct(&*cur,x);	// 必须一个一个元素地建构,无法批量进行
	}
	return cur;
}

uninitialized_fill_n()

如果 [first, first+n) 范围内的每一个迭代器都指向未初始化的内存,那么 uninitialized_fill_n() 会呼叫 copy constructor,在该范围内产生 x(上式 第三参数)的复制品。也就是说面对 [first,first+n) 范围内的每个迭代器 i, uninitialized_fill_n() 会呼叫 construct(&*i, x),在对应位置处产生 x 的 复制品。

/*
	参数说明:
		1.迭代器first指向欲初始化空间的起始处
		2.n表示欲初始化空间的大小
		3.x表示初值
*/
template <class ForwardIterator,class size,class T>
inline ForwardIterator uninitialized_fill_n(ForwardIterator first,size n,const T& x)
{
	// 利用value_type取出first的value type
	return __uninitialized_fill_n(first,n,x,value_type(first));
}
 
template <class ForwardIterator,class size,class T,class T1>
inline ForwardIterator __uninitialized_fill_n(ForwardIterator first,size n,const T& x,T1*)
{
	typedef typename __type_traits<T1>::is_POD_type is_POD;
	return __uninitialized_fill_n_aux(first,n,x,is_POD());
}
 
//如果是POD类型,籍由函数模板的自变量推导机制得到
template <class ForwardIterator,class size,class T>
ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first,size n,const T& x,__true_type)
{
	return fill_n(first,n,x);
}
 
// 如果不是POD类型
ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first,size n,const T& x,__false_type)
{
	ForwardIterator cur = first;
	for (;n>0;--n,++cur)
	{
		construct(&*cur,x);
	}
	return cur;
}

仿函数

仿函数就是使用起来像函数一样的东西。如果针对某个class进行operator()重载,那么它就成为一个仿函数

template<class T>
struct plus{
    T operator()(const T& x,const T& y) const {return x+y;}
}

这里plus就成为了一个仿函数。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章