STL源碼剖析 — 空間配置器(allocator)


前言


  以STL的實現角度而言,第一個需要介紹的就是空間配置器,因為整個STL的操作對象都存放在容器之中。

  你完全可以實現一個直接向硬件存取空間的allocator。

  下面介紹的是SGI STL提供的配置器,配置的對象,是內存。(以下內容來自《STL源碼剖析》)

 

引子


 

  因為這篇寫得太長,斷斷續續都有幾天,所以先在這里整理一下思路。

  • 首先,介紹 allocator 的標准接口,除了擁有一些基本的typedef之外,最重要的就是內存相關的 allocate 和 deallocate;構造相關的 construct 和 destroy。(兩者分離)然后就是實現一個簡單的配置器,沒有內存管理,只是簡單的malloc。
    • allocate 和 deallocate 負責獲取可以用的內存。
    • construct調用placement new構造函數,destroy調用相應類型的析構函數 ~T()
  • 然后介紹了SGI的第一級和第二級配置器。定義__USE_MALLOC可以設置使用第一級配置器還是兩個都用。
    • 內存池保留沒有被分配到free list的空間,free list維護一張可供調用的空間鏈表。
  • construct 會使用placement new構造,destroy借助traits機制判斷是否為 trivial再決定下一步動作。
  • allocate調用refill函數,會缺省申請20個區塊,一個返回,19個留在free list。refill又有三種情況。
  • deallocate先判斷是否大於128byte,是則調用第一級配置器,否就返回給freelist。

 

空間配置器的標准接口


 

根據STL的規范,allocator的必要接口

  • 各種typedef
    1 allocator::value_type
    2 allocator::pointer
    3 allocator::const_pointer
    4 allocator::reference
    5 allocator::const_reference
    6 allocator::size_type
    7 allocator::difference_type
    8 allocator::rebind // class rebind<U>擁有唯一成員other是一個typedef,代表allocator<U>
  • 默認構造函數和析構函數,因為沒有數據成員,所以不需要初始化,但是必須被定義
    1 allocator::allocator()
    2 allocator::allocator(const allocator&)
    3 template <class U> allocator::allocator(const allocator<U>&)
    4 allocator::~allocator()
  • 初始化,地址相關函數
    1 // 配置空間,足以存儲n個T對象,第二個參數是提示,能增進區域性
    2 pointer allocator::allocate(size_type n, const void*=0)
    3 
    4 size_type allocator::max_size() const
    5 
    6 pointer allocator::address(reference x) const
    7 const_pointer allocator::address(const_reference x) const
  • 構建函數
    1 void allocator::construct(pointer p, const T& x)
    2 void allocator::destory(pointer p)

     

自己設計一個簡單的空間配置器

 1 #ifndef __VIGGO__
 2 #define  __VIGGO__
 3 #include <new>        // for placement new
 4 #include <cstddef>    // for ptrdiff_t, size_t
 5 #include <cstdlib>    // for exit()
 6 #include <climits>    // for UINT_MAX
 7 #include <iostream>    // for cerr
 8 
 9 namespace VG {
10     
11     template <class T>
12     inline T* _allocate(ptrdiff_t n, T*) {
13         set_new_handler(0);
14         T* tmp = (T*)(::operator new((size_t)(n * sizeof(T))));
15         if (tmp == 0) {
16             cerr << "alloc memory error!" << endl;
17             exit(1);
18         }
19         return tmp;
20     }
21 
22     template <class T>
23     inline void _deallocate(T* p) {
24         ::operator delete(p);
25     }
26 
27     template <class T1, class T2>
28     inline void _construct(T1* p, const T2& value) {
29         new(p) T1(value);
30     }
31 
32     template <class T>
33     inline void _destroy(T* p) {
34         p->~T();
35     }
36 
37     template <class T>
38     class allocator {
39     public:
40         typedef T            value_type;
41         typedef T*            pointer;
42         typedef const T*    const_pointer;
43         typedef T&            reference;
44         typedef const T&    const_reference;
45         typedef size_t        size_type;
46         typedef ptrdiff_t    difference_type;
47 
48         template <class U>
49         struct rebind {
50             typedef allocator<U> other;
51         };
52         
53         pointer address(reference x) {return (pointer)&x;}
54         const_pointer address(const_reference x) const {
55             return (const_pointer)&x;
56         }
57 
58         pointer allocate(size_type n, const void *hint=0) {
59             return _allocate((difference_type)n, (pointer)0); // mark
60         }
61 
62         void deallocate(pointer p, size_type n) {
63             _deallocate(p);
64         }
65 
66         size_type max_size() const {return size_type(UINT_MAX / sizeof(T));}
67 
68         void construct(pointer p, const T& x) {
69             _construct(p, x);
70         }
71 
72         void destroy(pointer p) {
73             _destroy(p);
74         }
75     };
76 }
77 #endif

 

  放在 vector<int, VG::allocator<int> > 中測試,可以實現簡單的內存分配,但是實際上的 allocator 要比這個復雜。

 

SGI特殊的空間配置器


 

  標准的allocator只是基層內存配置/釋放行為(::operator new 和 ::operator delete)的一層薄薄的包裝,並沒有任何效率上的強化。

  現在我們看看C++內存配置和釋放是怎樣做的:

  new運算分兩階段(1)調用 ::operator new 配置內存;(2) 調用對象構造函數構造對象內容。

  delete運算也分兩階段(1) 調用對象的析構函數;(2)調用 ::operator delete 釋放內存。

 

  為了精密分工,STL allocator決定將兩階段操作區分開來,內存配置由 alloc::allocate() 負責。內存釋放操作由 alloc::deallocate()負責;對象構造由 ::construct() 負責,對象析構由 ::destroy() 負責。

構造和析構基本工具:construct() 和 destroy()


  construct() 接受一個指針p和一個初值value,該函數的用途就是將初值設定到指針所指的空間上。C++的placement new運算子可用來完成這一任務。

  destory()有兩個版本,一是接受一個指針,直接調用該對象的析構函數即可。另外一個接受first和last,將半開范圍內的所有對象析構。首先我們不知道范圍有多大,萬一很大,而每個對象的析構函數都無關痛癢(所謂 trivial destructor),那么一次次調用這些無關痛癢的析構函數是一種浪費。所以我們首先判斷迭代器所指對象是否為 trivial(無意義), 是則什么都不用做;否則一個個調用析構。

上圖為construct的實現函數

 

上圖為destroy的實現函數

這里用到我們神奇的 __type_traits<T>,之前介紹的 traits 是 萃取返回值類型 和 作為重載依據的,現在為每一個內置類型特化聲明一些tag。

現在我們需要用到 兩個標志:

示例:

 

空間的配置和釋放:std::alloc


 

  SGI的設計哲學: 1. 向 system heap 要求空間; 2. 考慮多線程狀態(先略過);3. 考慮內存不足時的應變措施;4. 考慮過多“小型區塊”可能造成的內存碎片問題。

  SGI設計了雙層級配置器,第一級配置器直接使用 malloc() 和 free(),第二級配置器則視情況采用不同的策略;當配置區塊超過128bytes時,交給第一級配置器。

  整個設計究竟只開放第一級配置器,或是同時開放第二級配置,取決於__USE_MALLOC時候被定義:

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

 

  其中__malloc_alloc_template就是第一級配置器,__default_alloc_template為第二級配置器。alloc並不接受任何template型別參數。

  無論alloc被定義為第一級或第二級配置器,SGI還為它在包裝一個接口如下,使配置器的接口能夠符合STL規格:

 1 template <class T, class Alloc>
 2 class simple_alloc {
 3 public:
 4        static T *allocate(size_t n)
 5             {return 0==n? 0 : (T*)Alloc::allocate(n * sizeof(T));}
 6        static T *allocate(void)
 7             {return (T*)Alloc::allocate(sizeof(T));}
 8        static void deallocate(T *p, size_t n)
 9             {if (0 != n) Alloc::deallocate(p, n*sizeof(T));}
10        static void deallocate(T *p)
11             {Alloc::deallocate(p, sizeof(T));}

 

  一二級配置器的關系,接口包裝,及實際運用方式,

 

 第一級配置器 __malloc_alloc_template


 

 1 #if 0
 2 #    include <new>
 3 #    define __THROW_BAD_ALLOC throw bad_alloc
 4 #elif !defined(__THROW_BAD_ALLOC)
 5 #    include <iostream>
 6 #    define __THROW_BAD_ALLOC cerr << "out of memery" << endl; exit(1);
 7 #endif
 8 
 9 // malloc-based allocator.通常比稍后介紹的 default alloc 速度慢
10 // 一般而言是thread-safe,並且對於空間的運用比較高效
11 // 以下是第一級配置器
12 // 注意,無“template型別參數”。置於“非型別參數”inst,則完全沒排上用場
13 template <int inst>
14 class __malloc_alloc_template {
15 private:
16     //以下都是函數指針,所代表的函數將用來處理內存不足的情況
17     static void *oom_malloc(size_t);
18     static void *oom_realloc(void*, size_t);
19     static void (* __malloc_alloc_oom_handler)();
20 public:
21     static void * allocate(size_t n) {
22         void *result = malloc(n); // 第一級配置器直接使用malloc
23         // 無法滿足需求時,改用oom_malloc
24         if (0 == result) result = oom_malloc(n);
25         return result;
26     }
27 
28     static void deallocate(void *p, size_t /* n */) {
29         free(p); // 第一級配置器直接用free()
30     }
31 
32     static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz) {
33         void *result = realloc(p, new_sz);
34         if (0 == result) result = oom_realloc(p, new_sz);
35         return result;
36     }
37 
38     // 以下仿真C++的 set_handler()。換句話,你可以通過它
39     // 指定自己的 out-of-memory handler,企圖釋放內存
40     // 因為沒有調用 new,所以不能用 set_new_handler
41     static void (* set_malloc_handler(void (*f)())) () {
42         void (*old)() = __malloc_alloc_oom_handler;
43         __malloc_alloc_oom_handler = f;
44         return old;
45     }
46 };
47 
48 // 初值為0,待定
49 template <int inst>
50 void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
51 
52 template <int inst>
53 void * __malloc_alloc_template<inst>::oom_malloc(size_t n) {
54     void (* my_malloc_handler)();
55     void *result;
56 
57     for (;;) {
58         my_malloc_handler = __malloc_alloc_oom_handler;
59         if (0 = my_malloc_handler) {__THROW_BAD_ALLOC;} // 如果沒設置
60         (* my_malloc_handler)(); // 調用處理例程,企圖釋放內存
61         result = malloc(n);        // 再次嘗試配置內存
62         if (result) return result;
63     }
64 }
65 
66 template <int inst>
67 void * __malloc_alloc_template<inst>::oom_realloc(void *p, size_t n) {
68     void (* my_malloc_handler)();
69     void *result;
70 
71     for (;;) {
72         my_malloc_handler = __malloc_alloc_oom_handler;
73         if (0 == my_malloc_handler) {__THROW_BAD_ALLOC;}
74         (*my_malloc_handler)();
75         result = realloc(p, n);
76         if (result) return result;
77     }
78 }

 

 第二級配置器 __default_alloc_template


 

 空間配置函數 - allocate()

1 static void * allocate(size_t n);

1. 如果 n 大於128bytes的時候,交給第一級配置器。

2. 找到 n 對應free list下的節點;如果節點不可用(=0)則調用 refill() 填充,否則調整節點指向下一個為止,直接返回可用節點。

 

重新填充free lists - refill()

void * refill(size_t n); //缺省取得20個節點

把大小為 n 的區塊交給客戶,然后剩下的19個交給對應的 free_list 管理。

 

內存池 - chunk_alloc()

char * chunk_alloc(size_t size, int & nobjs); // nobjs是引用,會隨實際情況調整大小

申請內存分三種情況:

  • 內存池剩余空間完全滿足需求。
  • 內存池剩余空間不能完全滿足需求量,當足夠供應一個(含)以上的區塊。
  • 內存池剩余空間連一個區塊的大小都無法提供。

首先必須做的就是查看剩余的空間:

1 size_t bytes_left = end_free - start_free;
2 size_t total_bytes = size * nobjs;

面對第一種情況,內存空間足夠的,只需要調整代表空閑內存的 start_free 指針,返回區域塊就可以。

面對第二種情況,盡量分配,有多少盡量分配。這是nobjs會被逐漸減少,從默認的20到能分配出內存, nobjs = bytes_left / size。

面對第三種情況,情況有點復雜。

  • 既然 [start_free, end_free) 之間的空間不夠分配 size * nobjs 大小的空間,就先把這段空間分配給合適的 free list 節點(下一步有用)。
  • 從 heap 上分配 兩倍的所需內存+heap大小的1/16(對齊成8的倍數) 大小的內存。
    • 如果heap分配都失敗的話,就在 free list 中比 size 大的節點中找內存使用。
    • 實在不行只能調用第一級配置器看看有咩有奇跡,oom機制。
  • 最后調整 heap_size 和 end_free,遞歸調用 chunk_alloc 知道至少能分出一個區塊。

 

空間釋放函數 - deallocate()

大於128就交給第一級配置器,否則調整free list,釋放內存。

 

完整代碼

  1 enum {__ALIGN = 8};
  2 enum {__MAX_BYTES = 128};
  3 enum {_NFREELISTS = __MAX_BYTES/__ALIGN};
  4 
  5 // 以下是第二級配置器
  6 // 注意,無“template型別參數”,且第二參數完全沒排上用場
  7 // 第一參數用於多線程環境下
  8 template <bool threads, int inst>
  9 class __default_alloc_template {
 10 private:
 11     // 將bytes上調至8的倍數
 12     static size_t ROUND_UP(size_t bytes) {
 13         return ((bytes) + __ALIGN-1) & ~(__ALIGN-1);
 14     }
 15     
 16     union obj { // free-lists的節點構造
 17         union obj *free_list_link;
 18         char client_data[1];
 19     };
 20 
 21     static obj *volatile free_list[_NFREELISTS];
 22     static size_t FREELIST_INDEX(size_t bytes) {
 23         return ((bytes) + (__ALIGN-1)) / (__ALIGN-1);
 24     }
 25 
 26     // 返回一個大小為n的對象,並可能加入大小為n的其他區塊到free list
 27     static void *refill(size_t n);
 28     // 配置一大塊空間,可容納 nobj 個大小為“size”的區塊
 29     // 如果配置 nobjs 個區塊有所不便,nobjs可能會降低
 30     static char *chunk_alloc(size_t size, int &nobjs);
 31 
 32     // Chunk allocation state
 33     static char *start_free;    // 內存池起始位置,只在chunk_alloc中變化
 34     static char *end_free;        // 內存池結束為止,同上
 35     static size_t heap_size;
 36 
 37 public:
 38     static void *allocate(size_t n);
 39     static void deallocate(void *p, size_t n);
 40     static void * reallocate(void *p, size_t old_sz, size_t new_sz);
 41 };
 42 
 43 template <bool threads, int inst>
 44 char * __default_alloc_template<threads, inst>::start_free = 0;
 45 
 46 template <bool threads, int inst>
 47 char * __default_alloc_template<threads, inst>::end_free = 0;
 48 
 49 template <bool threads, int inst>
 50 size_t * __default_alloc_template<threads, inst>::heap_size = 0;
 51 
 52 template <bool threads, int inst>
 53 __default_alloc_template<threads, inst>::obj *volatile
 54     __default_alloc_template<threads, inst>::free_list[_NFREELISTS] = 
 55 {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
 56 
 57 // n must > 0
 58 template<bool threads, int inst>
 59 void * __default_alloc_template<threads, inst>::allocate(size_t n) {
 60     obj * volatile * my_free_list; // 一個數組,數組元素是obj*
 61     obj * result;
 62 
 63     if (n > (size_t) __MAX_BYTES) {
 64         return malloc_alloc::allocate(n);
 65     }
 66 
 67     // 尋找16個free lists中適當的一個
 68     my_free_list = free_list + FREELIST_INDEX(n);
 69     result = *my_free_list;
 70     if (result == 0) {
 71         // 沒找到可用的free list,准備重新填充free list
 72         void *r = refill(ROUND_UP(n));
 73         return r;
 74     }
 75 
 76     // 調整free list
 77     *my_free_list = result -> free_list_link;
 78     return result;
 79 }
 80 
 81 template <bool threads, int inst>
 82 void __default_alloc_template<threads, inst>::deallocate(void *p, size_t n) {
 83     obj *q = (obj*)p;
 84     obj * volatile * my_free_list;
 85 
 86     if (n > (size_t) __MAX_BYTES) {
 87         malloc_alloc::deallocate(p, n);
 88         return ;
 89     }
 90 
 91     my_free_list = free_list + FREELIST_INDEX(n);
 92     q -> free_list_link = *my_free_list;
 93     *my_free_list = q;
 94 }
 95 
 96 template <bool threads, int inst>
 97 void * __default_alloc_template<threads, inst>::refill(size_t n) {
 98     int nobjs = 20;
 99     // 調用chunk_alloc(),嘗試取得nobjs個區塊作為free list的新節點
100     // 注意參數nobjs是pass by reference
101     char * chunk = chunk_alloc(n, nobjs);
102     obj * volatile * my_free_link;
103     obj * result;
104     obj * current_obj, * next_obj;
105     int i;
106 
107     // 如果只獲得一個區塊,這個區塊就分配給調用者用,free list無新節點
108     if (1 == nobjs) return chunk;
109     // 否則准備調整free link,納入新節點
110     my_free_link = free_list + FREELIST_INDEX(n);
111 
112     // 以下是chunk空間內建立free list
113     result = (obj *)chunk;
114     // 以下引導free list指向新配置的空間(取自內存池)
115     *my_free_link = next_obj = (obj*) (chunk + n);
116     // 以下將free list的各節點串接起來
117     for (i=1; ; ++i) { // 從1開始,因為第0個將返回給客戶端
118         current_obj = next_obj;
119         next_obj = (obj *)((char *)next_obj + n);
120         if (nobjs - 1 == i) {
121             current_obj -> free_list_link = 0;
122             break;
123         } else {
124             current_obj -> free_list_link = next_obj;
125         }
126     }
127     return result;
128 }
129 
130 
131 // 假設size已經上調至8的倍數
132 // 注意參數nobjs是pass by reference
133 template <bool threads, int inst>
134 char *
135     __default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs) {
136         char * result;
137         size_t total_bytes = size * nobjs;
138         size_t bytes_left = end_free - start_free;
139 
140         if (bytes_left >= total_bytes) {
141             // 內存池剩余空間完全滿足需求量
142             result = start_free;
143             start_free += total_bytes;
144             return result;
145         } else if (bytes_left >= size) {
146             // 內存池剩余空間不能完全滿足需求量,但足夠供應一個(含)以上的區塊
147             nobjs = bytes_left/size;
148             total_bytes = size * nobjs;
149             result = start_free;
150             start_free += total_bytes;
151             return result;
152         } else {
153             // 內存池剩余空間連一個區塊的大小都無法提供
154             size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
155             // 以下試着讓內存池中的殘余零頭還有利用價值
156             if (bytes_left > 0) {
157                 // 內存池內還有一些零頭,先配給適當的free list
158                 // 首先尋找適當的free list
159                 obj * volatile * my_free_list = free_list + FREELIST_INDEX(bytes_left);
160                 // 調整free list,將內存池中的殘余空間編入
161                 ((obj *)start_free) -> free_list_link = *my_free_list;
162                 *my_free_list = (obj *)start_free;
163             }
164 
165             // 配置heap空間,用來補充內存池
166             start_free = (char *)malloc(bytes_to_get);
167             if (0 == start_free) {
168                 // heap空間不足,malloc失敗
169                 int i;
170                 obj * volatile * my_free_list, *p;
171                 // 試着檢視我們手上擁有的東西,這不會造成傷害。我們不打算嘗試配置
172                 // 較小的區塊,因為那在多進程機器上容器導致災難
173                 // 以下搜尋適當的free list
174                 // 所謂適當是指“尚未用區塊,且區塊夠大”的free list
175                 for (i=size; i <= __MAX_BYTES; i+=__ALIGN) {
176                     my_free_list = free_list + FREELIST_INDEX(i);
177                     p = *my_free_list;
178                     if (0 != p) { // free list內尚有未用塊
179                         // 調整free list以釋放未用區塊
180                         *my_free_list = p -> free_list_link;
181                         start_free = (char *)p;
182                         end_free = start_free + i;
183                         // 遞歸調用自己,為了修正nobjs
184                         return chunk_alloc(size, nobjs);
185                         // 注意,任何殘余零頭終將被編入適當的free list中備用
186                     }
187                 }
188                 end_free = 0; // 如果出現意外,調用第一級配置器,看看oom機制能否盡力
189                 start_free = (char *)malloc_alloc::allocate(bytes_to_get);
190                 // 這會拋出異常 或 內存不足的情況得到改善
191             }
192             heap_size += bytes_to_get;
193             end_free = start_free + bytes_to_get;
194             // 遞歸調用自己,為了修正nobjs
195             return chunk_alloc(size, nobjs);
196         }
197 }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM