1. 程式人生 > >STL六大元件之——分配器(記憶體分配,好深奧的東西)

STL六大元件之——分配器(記憶體分配,好深奧的東西)

SGI設計了雙層級配置器,第一級配置器直接使用malloc()和free(),第二級配置器則視情況採用不同的策略:當配置區塊超過128bytes時,視之為“足夠大”,便呼叫第一級配置器;當配置區小於128bytes時,視之為“過小”,為了降低額外負擔,便採用複雜的memory pool 整理方式,而不再求助於第一級配置器。整個設計究竟只開放第一級配置器,取決於_USE_MALLOC是否被定義:

複製程式碼
1 #ifdef  __USE_MALLOC  
2 ...  
3 typedef __malloc_alloc_template<0> malloc_alloc ;  
4 typedef malloc_alloc alloc ; //
令alloc為第一級配置器 5 #else 6 //令alloc為第二級配置器 7 typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS,0> alloc ; 8 #endif /*! __USE_MALLOC*/
複製程式碼

SGI STL 第一級配置器

template<int inst>

class  __malloc_alloc_template {…} ;

註釋:

1.allocate()直接使用malloc(),deallocate()直接使用free()。

2.模擬C++的set_new_handler()以處理記憶體不足的狀況。第一級配置器以malloc(),free(),realloc()等C函式執行實際的記憶體配置、釋放、重配置操作。

SGI STL 第二級配置器

template<bool threads,int inst>

class __default_alloc_template {…} ;

註釋:

1.維護16個自由連結串列(free lists),負責16種小型區塊的次配置能力。記憶體池(memory pool)以malloc()配置而得。如果記憶體不足,轉呼叫第一級配置器。

2.如果需求區塊大於128byte,就轉呼叫第一級配置器。

下面是一些具體的理解分享:

default allocator

SGI STL 的標頭檔案defalloc.h中有一個符合標準的名為allocator的記憶體分配器,它只是簡單地將::operator new 和::operator delete做了一層薄薄的封裝。在SGI STL的容器和演算法部分從來沒有用到這個記憶體分配器。

STL 的記憶體分配策略

首先簡要介紹一下STL中對記憶體分配的規劃

    當用戶用new構造一個物件的時候,其實內含兩種操作:1)呼叫::operator new申請記憶體;2)呼叫該物件的建構函式構造此物件的內容

    當用戶用delete銷燬一個物件時,其實內含兩種操作:1)呼叫該物件的解構函式析構該物件的內容;2)呼叫::operator delete釋放記憶體

SGI STL中物件的構造和析構由::construct()和::destroy()負責;記憶體的申請和釋放由alloc:allocate()和alloc:deallocate()負責;此外,SGI STL還提供了一些全域性函式,用來對大塊記憶體資料進行操作。

上一段提到的三大模組分別由stl_construct.hstl_alloc.hstl_uninitialized.h 負責

物件的構造和析構工具(stl_construct.h)

stl_construct.h中提供了兩種物件的構造方法,預設構造和賦值構造:

複製程式碼
1 template <class _T1, class _T2>
2 inline void _Construct(_T1* __p, const _T2& __value) {
3   new ((void*) __p) _T1(__value);
4 }
5 
6 template <class _T1>
7 inline void _Construct(_T1* __p) {
8   new ((void*) __p) _T1();
9 }
複製程式碼

上面兩個函式的作用是構造一個型別為T1的物件,並由作為引數的指標p返回。

其中的new (_p) _T1(_value); 中使用了placement new運算元,它的作用是通過拷貝的方式在記憶體地址_p處構造一個_T1物件。(placement new能實現在指定的記憶體地址上用指定型別的建構函式來構造一個物件)。

在物件的銷燬方面,stl_construct.h也提供了兩種析構方法:

複製程式碼
1 template <class _Tp>
2 inline void _Destroy(_Tp* __pointer) {
3   __pointer->~_Tp();
4 }
5 
6 template <class _ForwardIterator>
7 inline void _Destroy(_ForwardIterator __first, _ForwardIterator __last) {
8   __destroy(__first, __last, __VALUE_TYPE(__first));
9 }
複製程式碼

第一個版本的解構函式接受一個指標,將該指標所指的物件析構掉;第二個版本的解構函式接受first和last兩個迭代器,將這兩個迭代器範圍內的物件析構掉。

在第二個版本的destroy函式裡面,運用了STL中慣用的traits技法,traits會得到當前物件的一些特性,再根據特性的不同分別對不同特性的物件呼叫相應的方法。在stl_construct.h中的destroy中,STL會分析迭代器所指物件的has_trivial_destructor特性的型別(只有兩種:true_type和false_type),如果是true_type,STL就什麼都不做;如果是false_type,就會呼叫每個物件的解構函式來銷燬這組物件。

除此之外,stl_construct還為一些基本型別的物件提供了特化版本的destroy函式,這些基本型別分別是char, int, float, double, long。當destroy的引數為這些基本型別時,destroy什麼都不做。

記憶體空間管理工具alloc

我想以自底向下的順序介紹一下STL的allocator。首先說說STL內建的兩種分配器,然後介紹STL如何封裝這兩種分配器對外提供統一的介面,最後用一個vector的例子看看容器如何使用這個allocator。

1 __malloc_alloc_template記憶體分配器

該分配器是對malloc、realloc以及free的封裝:

複製程式碼
 1 static void* allocate(size_t __n)
 2   {
 3     void* __result = malloc(__n);
 4     if (0 == __result) __result = _S_oom_malloc(__n);
 5     return __result;
 6   }
 7 
 8   static void deallocate(void* __p, size_t /* __n */)
 9   {
10     free(__p);
11   }
12 
13   static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
14   {
15     void* __result = realloc(__p, __new_sz);
16     if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
17     return __result;
18   }
複製程式碼

當呼叫malloc和realloc申請不到記憶體空間的時候,會改呼叫oom_malloc()和oom_realloc(),這兩個函式會反覆呼叫使用者傳遞過來的out of memory handler處理函式,直到能用malloc或者realloc申請到記憶體為止。如果使用者沒有傳遞__malloc_alloc_oom_handler,__malloc_alloc_template會丟擲__THROW_BAD_ALLOC異常。所以,記憶體不足的處理任務就交給類客戶去完成。

2 __default_alloc_template分配器

這個分配器採用了記憶體池的思想,有效地避免了內碎片的問題(順便一句話介紹一下內碎片和外碎片:內碎片是已被分配出去但是用不到的記憶體空間,外碎片是由於大小太小而無法分配出去的空閒塊)。如果申請的記憶體塊大於128bytes,就將申請的操作移交__malloc_alloc_template分配器去處理;如果申請的區塊大小小於128bytes時,就從本分配器維護的記憶體池中分配記憶體。分配器用空閒連結串列的方式維護記憶體池中的空閒空間,空閒連結串列大概類似於下面的形狀:

如上圖所示,s_free_list是這些空閒分割槽鏈的起始地址組成的陣列,大小為16。這16個連結串列中每個連結串列中的空閒空間的大小都是固定的,第一個連結串列的空閒塊大小是8bytes, 依次是16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128bytes。

  另外還有三個指標s_start_free, s_end_free, s_heap_size。它們分別指向整個記憶體池的起始地址,結束地址和可用空間大小。

分配記憶體過程:

  1)如果申請的記憶體空間大於128bytes, 則交由第一個分配器處理

  2)分配器首先將申請記憶體的大小上調至8的倍數n,並根據n找出其對應的空閒連結串列地址__my_free_list

  3)如果該空閒連結串列中有可用的空閒塊,則將此空閒塊返回並更新__my_free_list,否則轉到4)

  4)到這一步,說明__my_free_list中沒有空閒塊可用了,分配器會按照下面的步驟處理:

  a) 試著呼叫_s_chunk_alloc()申請大小為n*20的記憶體空間,注意的是,此時不一定能申請到n*20大小的記憶體空間

b) 如果只申請到大小為n的記憶體空間,則返回給使用者,否則到c)

    c) 將申請到的n*x(a中說了,不一定是n*20)記憶體塊取出一個返回給使用者,其餘的記憶體塊鏈到空閒連結串列__my_free_list中

_s_chunk_alloc()的具體過程為:

    1)如果_s_start_free和_s_end_free之間的空間足夠分配n*20大小的記憶體空間,則從這個空間中取出n*20大小的記憶體空間,更新_s_start_free並返回申請到的記憶體空間的起始地址,否則轉到2)

    2) 如果_s_start_free和_s_end_free之間的空間足夠分配大於n的記憶體空間,則分配整數倍於n的記憶體空間,更新_s_start_free,由nobj返回這個整數,並返回申請到的記憶體空間的起始地址;否則轉到3)

  3) 到這一步,說明記憶體池中連一塊大小為n的記憶體都沒有了,此時如果記憶體池中還有一些記憶體(這些記憶體大小肯定小於n),則將這些記憶體插入到其對應大小的空閒分割槽鏈中

  4) 呼叫malloc向執行時庫申請大小為(2*n*20 + 附加量)的記憶體空間, 如果申請成功,更新_s_start_free, _s_end_free和_s_heap_size,並重新呼叫_s_chunk_alloc(),否則轉到5)

   5) 到這一步,說明4)中呼叫malloc失敗,這時分配器依次遍歷16個空閒分割槽鏈,只要有一個空閒鏈,就釋放該鏈中的一個節點,重新呼叫_s_chunk_alloc()

記憶體釋放過程:

記憶體的釋放過程比較簡單,它接受兩個引數,一個是指向要釋放的記憶體塊的指標p,另外一個表示要釋放的記憶體塊的大小n。分配器首先判斷n,如果n>128bytes,則交由第一個分配器去處理;否則將該記憶體塊加到相應的空閒連結串列中。

SGI STL 為了方便使用者訪問,為上面提到的兩種分配器包裝了一個介面(對外提供的分配器介面),該介面如下:

複製程式碼
 1 template<class _Tp, class _Alloc>
 2 class simple_alloc {
 3 
 4 public:
 5     static _Tp* allocate(size_t __n)
 6       { return 0 == __n ? 0 : (_Tp*) _Alloc::allocate(__n * sizeof (_Tp)); }
 7     static _Tp* allocate(void)
 8       { return (_Tp*) _Alloc::allocate(sizeof (_Tp)); }
 9     static void deallocate(_Tp* __p, size_t __n)
10       { if (0 != __n) _Alloc::deallocate(__p, __n * sizeof (_Tp)); }
11     static void deallocate(_Tp* __p)
12       { _Alloc::deallocate(__p, sizeof (_Tp)); }
13 };
複製程式碼

使用者呼叫分配器的時候,為simple_alloc的第二個模板引數傳遞要使用的分配器。

vector(使用者方式)使用STL分配器的程式碼,可以看到vector的基類呼叫simple_alloc作為其分配器:

複製程式碼
 1 template <class _Tp, class _Alloc>
 2   //cobbliu 注:STL vector 的基類 
 3 class _Vector_base {  
 4 public:
 5   typedef _Alloc allocator_type;
 6   allocator_type get_allocator() const { return allocator_type(); }
 7 
 8   _Vector_base(const _Alloc&)
 9     : _M_start(0), _M_finish(0), _M_end_of_storage(0) {}
10   _Vector_base(size_t __n, const _Alloc&)
11     : _M_start(0), _M_finish(0), _M_end_of_storage(0) 
12   {
13     _M_start = _M_allocate(__n);
14     _M_finish = _M_start;
15     _M_end_of_storage = _M_start + __n;
16   }
17 
18   ~_Vector_base() { _M_deallocate(_M_start, _M_end_of_storage - _M_start); }
19 
20 protected:
21   _Tp* _M_start;
22   _Tp* _M_finish;
23   _Tp* _M_end_of_storage;
24 
25   typedef simple_alloc<_Tp, _Alloc> _M_data_allocator;
26   _Tp* _M_allocate(size_t __n)
27     { return _M_data_allocator::allocate(__n); }
28   void _M_deallocate(_Tp* __p, size_t __n) 
29     { _M_data_allocator::deallocate(__p, __n); }
30 };
複製程式碼

基本記憶體處理工具

除了上面的記憶體分配器之外,STL還提供了三類記憶體處理工具:uninitialized_copy(), uninitialized_fill()和uninitialized_fill_n()。這三類函式的實現程式碼在標頭檔案stl_uninitialized.h中。

uninitialized_copy()像下面的樣子:

複製程式碼
1 template <class _InputIter, class _ForwardIter>
2 inline _ForwardIter
3   uninitialized_copy(_InputIter __first, _InputIter __last,
4                      _ForwardIter __result)
5 {
6   return __uninitialized_copy(__first, __last, __result,
7                               __VALUE_TYPE(__result));
8 }
複製程式碼

uninitialized_copy()會將迭代器_first和_last之間的物件拷貝到迭代器_result開始的地方。它呼叫的__uninitialized_copy(__first, __last, __result,__VALUE_TYPE(__result))會判斷迭代器_result所指的物件是否是POD型別(POD型別是指擁有constructor, deconstructor, copy, assignment函式的類),如果是POD型別,則呼叫演算法庫的copy實現;否則遍歷迭代器_first~_last之間的元素,在_result起始地址處一一構造新的元素。

uninitialized_fill()像下面的樣子:

複製程式碼
1 template <class _ForwardIter, class _Tp>
2 inline void uninitialized_fill(_ForwardIter __first,
3                                _ForwardIter __last, 
4                                const _Tp& __x)
5 {
6   __uninitialized_fill(__first, __last, __x, __VALUE_TYPE(__first));
7 }
複製程式碼

uninitialized_fill()會將迭代器_first和_last範圍內的所有元素初始化為x。它呼叫的__uninitialized_fill(__first, __last, __x, __VALUE_TYPE(__first))會判斷迭代器_first所指的物件是否是POD型別的,如果是POD型別,則呼叫演算法庫的fill實現;否則一一構造。

uninitialized_fill_n()像下面這個樣子:

1 template <class _ForwardIter, class _Size, class _Tp>
2 inline _ForwardIter 
3 uninitialized_fill_n(_ForwardIter __first, _Size __n, const _Tp& __x)
4 {
5    return __uninitialized_fill_n(__first, __n, __x, __VALUE_TYPE(__first));
6 }

uninitialized_fill_n()會將迭代器_first開始處的n個元素初始化為x。它呼叫的__uninitialized_fill_n(__first, __n, __x, __VALUE_TYPE(__first))會判斷迭代器_first所指物件是否是POD型別,如果是,則呼叫演算法庫的fill_n實現;否則一一構造。