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就成爲了一個仿函數。

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