帶你深入理解STL之空間配置器(思維導圖+原始碼)
前不久把STL細看了一遍,由於看得太“認真”,忘了做筆記,歸納和總結這步漏掉了。於是為了加深印象,打算重看一遍,並記錄下來裡面的一些實現細節。方便以後能較好的複習它。
以前在專案中運用STL一般都不會涉及到空間配置器,可是,在STL的實現中,空間配置器是重中之重,因為整個STL的操作物件都存放在容器之內,而容器一定需要配置空間以置放資料。所以,在閱讀STL原始碼時,最先需要掌握的就是空間配置器,沒了它,容器,演算法怎麼存在?
C++ STL的空間配置器將記憶體的配置、釋放和物件的構造和析構分開,記憶體配置操作由alloc::allocate()負責,記憶體釋放由alloc::deallocate()負責;物件構造操作由::construct()負責,物件的析構操作由::destroy()負責。首先放一張思維導圖來概述一下STL的整個空間配置器概覽。
物件的構造和析構
個人覺得看原始碼只需要圖和程式碼註釋即可,所以本篇部落格圖片較多!對著圖來看程式碼效率會高很多!
下面是原始碼:
#include <new.h> // 需要placement new的原型
// -----------------建構函式---------------------------------//
// 使用placement new在已經分配的記憶體上構造物件
template <class T1, class T2>
inline void construct(T1* p, const T2& value)
{
new (p) T1(value);//將value設定到指標p所指的空間上
}
// -----------------解構函式---------------------------------//
// -----------第一個版本:接受一個指標--------------------------//
// 呼叫成員的解構函式, 需要型別具有non-trivial destructor
template <class T>
inline void destroy(T* pointer)
{
pointer->~T();
}
// -----------第二個版本:接受兩個迭代器------------------------//
template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last)
{
__destroy(first, last, value_type(first));
}
// 首先是兩個特化版本
inline void destroy(char*, char*) {}
inline void destroy(wchar_t*, wchar_t*) {}
// 析構一組物件, 用於具有non-trivial destructor
template <class ForwardIterator>
inline void
__destroy_aux(ForwardIterator first, ForwardIterator last, __false_type)
{
for ( ; first < last; ++first)
destroy(&*first);
}
// 如果沒有型別non-trivial destructor, 則使用此函式
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {}
// 使用traits技術, 判斷型別是否就有non-trivial destructor, 然後呼叫不同的函式
template <class ForwardIterator, class T>
inline void __destroy(ForwardIterator first, ForwardIterator last, T*)
{
typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
__destroy_aux(first, last, trivial_destructor());
}
記憶體的配置和釋放
在記憶體配置方面,STL分為兩級配置器,當請求的記憶體大於128b的時候呼叫第一級配置器,當請求的記憶體小於等於128b的時候呼叫第二級配置器。先來看看下面這張表,大概就能知道第一級和第二級配置器主要乾了些什麼,其他的一些細節如記憶體池是怎麼工作的,下面會給出具體解釋。
第一級配置器
首先我們來看第一級配置器的原始碼:
template <int inst>
class __malloc_alloc_template
{
private:
//呼叫malloc函式不成功後呼叫
static void *oom_malloc(size_t);
//呼叫realloc函式不成功後呼叫
static void *oom_realloc(void *, size_t);
//類似於C++的set_new_handle錯誤處理函式一樣,如果不設定,在記憶體不足時,返回THROW_BAD_ALLOC
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
static void (* __malloc_alloc_oom_handler)();
#endif
public:
//直接呼叫malloc來分配記憶體
static void * allocate(size_t n)
{
void *result = malloc(n);
if (0 == result) result = oom_malloc(n); //如果分配失敗,則呼叫oom_malloc()
return result;
}
//第一級配置器直接呼叫free來釋放記憶體
static void deallocate(void *p, size_t /* n */)
{
free(p);
}
//直接呼叫reallloc來分配記憶體
static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)
{
void * result = realloc(p, new_sz);
if (0 == result) result = oom_realloc(p, new_sz); //如果realloc分配不成功,呼叫oom_realloc()
return result;
}
//異常處理函式,即記憶體分配失敗後的處理
static void (* set_malloc_handler(void (*f)()))()
{
void (* old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = f;
return(old);
}
};
從上述原始碼中可以看到,STL的第一級配置器僅僅是呼叫了malloc,free等函式,然後增加了記憶體分配錯誤下的異常處理函式,下面我們就通過原始碼來看看在記憶體分配失敗後,STL是怎麼處理的。
// 以下是針對記憶體分配失敗後的處理
//首先,將__malloc_alloc_oom_handler的預設值設為0
template <int inst>
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
#endif
template <int inst>
void * __malloc_alloc_template<inst>::oom_malloc(size_t n)
{
void (* my_malloc_handler)();
void *result;
for (;;) { // 不斷地嘗試釋放、再配置、再釋放、再配置
my_malloc_handler = __malloc_alloc_oom_handler;
if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; } //這裡是當沒有設定處理函式的時候,直接丟擲異常
(*my_malloc_handler)(); // 呼叫處理例程,嘗試釋放記憶體
result = malloc(n); // 再重新分配記憶體
if (result) return(result); // 如果分配成功則返回指標
}
}
template <int inst>
void * __malloc_alloc_template<inst>::oom_realloc(void *p, size_t n)
{
void (* my_malloc_handler)();
void *result;
for (;;) { //不斷地嘗試釋放、再配置、再釋放、再配置
my_malloc_handler = __malloc_alloc_oom_handler;
if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; } //這裡是當沒有設定處理函式的時候,直接丟擲異常
(*my_malloc_handler)(); // 呼叫處理例程,嘗試釋放記憶體
result = realloc(p, n); // 再重新分配記憶體
if (result) return(result); // 如果分配成功則返回指標
}
}
第二級配置器
當申請記憶體小於128b的時候,會呼叫第二級配置器。第二級配置器有一個記憶體池和一個對應的自由連結串列,其定義如下:
union obj
{
union obj * free_list_link;
char client_data[1];
};
這裡有一個技巧,如果使用union的第一個成員,則指向另一個相同的union obj;而如果使用第二個成員,則指向實際的記憶體區域,這樣一來,既實現了連結串列結點只用一個指標的大小空間,卻能同時做索引和指向記憶體區域。
這裡的這個技巧我覺得有必要解釋一下,首先client_data是一個常量指標,指向client_data[0],然後client_data[0]和free_list_link共用同一段記憶體,我們在使用這個union的時候,先讓client_data指向實際的記憶體區域,然後將free_list_link(也就是client_data[0])賦值為下一個結點的地址,注意這裡我只是修改了client_data[0],client_data並沒有修改,而是始終指向實際記憶體。
我們先來看看第二級配置器的部分原始碼,然後再去分析其中每個函式的功能。
enum {__ALIGN = 8}; //小型區塊的上調邊界
enum {__MAX_BYTES = 128}; //小型區塊的上限
enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; //free-lists個數
//第一引數用於多執行緒,這裡不做討論。
template <bool threads, int inst>
class __default_alloc_template
{
private:
// 此函式將bytes的邊界上調至8的倍數
static size_t ROUND_UP(size_t bytes)
{
return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
}
private:
// 此union結構體上面已經解釋過了
union obj
{
union obj * free_list_link;
char client_data[1];
};
private:
//16個free-lists
static obj * __VOLATILE free_list[__NFREELISTS];
// 根據待待分配的空間大小, 在free_list中選擇合適的大小
static size_t FREELIST_INDEX(size_t bytes)
{
return (((bytes) + __ALIGN-1)/__ALIGN - 1);
}
// 返回一個大小為n的物件,並可能加入大小為n的其它區塊到free-lists
static void *refill(size_t n);
// 配置一大塊空間,可容納nobjs個大小為“size”的區塊
// 如果配置nobjs個區塊有所不便,nobjs可能會降低,所以需要用引用傳遞
static char *chunk_alloc(size_t size, int &nobjs);
// 記憶體池
static char *start_free; // 記憶體池起始點,只在chunk_alloc()中變化
static char *end_free; // 記憶體池結束點,只在chunk_alloc()中變化
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_t 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>
__default_alloc_template<threads, inst>::obj * __VOLATILE
__default_alloc_template<threads, inst> ::free_list[__NFREELISTS ] =
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
看完上面這一堆原始碼,你可能早就頭暈眼花,一臉懵逼了,沒事,我再來用一張思維導圖來幫你理一理思緒:
接下來又是枯燥的原始碼時間!相信有上面這張圖,看原始碼的思路就比較清晰了。
空間配置函式allocate()
借用《STL原始碼剖析》裡面的一張圖,來說明空間配置函式的呼叫過程:(看圖放鬆,放鬆完繼續看原始碼!別偷懶)
static void * allocate(size_t n)
{
obj * volatile * my_free_list;
obj * result;
// 大於128就呼叫第一級配置器
if (n > (size_t) __MAX_BYTES) {
return(malloc_alloc::allocate(n));
}
// 尋找16個free_lists中適當的一個
my_free_list = free_list + FREELIST_INDEX(n);
result = *my_free_list;
if (result == 0) {
// 如果沒有可用的free list,準備重新填充free_list
void *r = refill(ROUND_UP(n));
return r;
}
// 調整free list
*my_free_list = result -> free_list_link;
return (result);
};
重新填充函式refill()
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
int nobjs = 20; // 預設獲取20個
char * chunk = chunk_alloc(n, nobjs); //找記憶體池要空間
obj * volatile * my_free_list;
obj * result;
obj * current_obj, * next_obj;
int i;
// 如果記憶體池僅僅只夠分配一個物件的空間, 直接返回即可
if(1 == nobjs) return(chunk);
// 記憶體池能分配更多的空間,調整free_list納入新節點
my_free_list = free_list + FREELIST_INDEX(n);
// 在chunk的空間中建立free_list
result = (obj *)chunk;
*my_free_list = next_obj = (obj *)(chunk + n); //導引free_list指向新配置的空間(取自記憶體池)
for(i = 1; ; i++) { //從1開始,因為第0個返回給客端
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);//返回頭指標
}
記憶體池函式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);//返回起始地址
}
// 如果記憶體池中剩餘的容量不夠分配, 但是能至少分配一個節點時,
// 返回所能分配的最多的節點, 返回start_free指向的記憶體塊
// 並且重新設定記憶體池起始點
else if(bytes_left >= size) {
nobjs = bytes_left/size;
total_bytes = size * nobjs;
result = start_free;
start_free += total_bytes;
return(result);
}
// 記憶體池剩餘記憶體連一個節點也不夠分配
else {
size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
// 將剩餘的記憶體分配給指定的free_list[FREELIST_INDEX(bytes_left)]
if (bytes_left > 0) {
//記憶體池內還有一些零頭,先分給適當的free_list
//尋找適當的free_list
obj * __VOLATILE * my_free_list =
free_list + FREELIST_INDEX(bytes_left);
// 調整free_list,將記憶體池中的殘餘空間編入
((obj *)start_free) -> free_list_link = *my_free_list;
*my_free_list = (obj *)start_free;
}
start_free = (char *)malloc(bytes_to_get);
// 分配失敗, 搜尋原來已經分配的記憶體塊, 看是否有大於等於當前請求的記憶體塊
if (0 == start_free) {// heap裡面空間不足,malloc失敗
int i;
obj * __VOLATILE * my_free_list, *p;
// 試著檢查檢查free_list中的可用空間,即尚有未用的空間,且區塊夠大
for (i = size; i <= __MAX_BYTES; i += __ALIGN) {
my_free_list = free_list + FREELIST_INDEX(i);
p = *my_free_list;
// 找到了一個, 將其加入記憶體池中
if (0 != p) {
*my_free_list = p -> free_list_link;
start_free = (char *)p;
end_free = start_free + i;
// 記憶體池更新完畢, 重新分配需要的記憶體
return(chunk_alloc(size, nobjs));
//任何剩餘零頭將被編入適當的free_list以留備用
}
}
// 再次失敗, 直接呼叫一級配置器分配, 期待異常處理函式能提供幫助
// 不過在我看來, 記憶體分配失敗進行其它嘗試已經沒什麼意義了,
// 最好直接log, 然後讓程式崩潰
end_free = 0;
//呼叫第一級配置器,看看out-of-memory機制能不能起點作用
start_free = (char *)malloc_alloc::allocate(bytes_to_get);
}
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
// 記憶體池更新完畢, 重新分配需要的記憶體
return(chunk_alloc(size, nobjs));
}
}
記憶體釋放函式deallocate()
記憶體釋放函式會將釋放的空間交還給free_list以留備用。其過程如下圖所示:
其實就是一個簡單的單鏈表插入的過程。其原始碼如下:
static void deallocate(void *p, size_t n)
{
obj *q = (obj *)p;
obj * volatile * my_free_list;
// 大於128的直接交由第一級配置器釋放
if (n > (size_t) __MAX_BYTES) {
malloc_alloc::deallocate(p, n);
return;
}
// 尋找適當的free_list
my_free_list = free_list + FREELIST_INDEX(n);
// 調整free_list,回收區塊
q -> free_list_link = *my_free_list;
*my_free_list = q;
}
配置器的使用
通過以上的圖和原始碼,基本上將STL的兩層配置器講完了,接下來就來熟悉一下怎麼使用配置器。
STL將上述配置器封裝在類simple_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)); }
};
接下來就示範在vector中是怎麼使用它的。
template <class T, class Alloc = alloc> //alloc被預設為第二級配置器
class vector {
public:
typedef T value_type;
//...
protected:
// 專屬的空間配置器,每次只分配一個元素的大小
typedef simple_alloc<value_type, Alloc> data_allocator;
// 在釋放記憶體的時候直接呼叫藉口函式即可
void deallocate(){
if(...){
data_allocator::deallocate(start , end_of_storage - start);
}
}
};
至此,STL的空間配置器的原理和使用都已講述完畢,讀者們是否懂了呢? 沒懂就戳下方的聯絡方式或者留言說出你的疑惑吧!
About Me
由於本人也是初學,在寫作過程中,難免有錯誤的地方,讀者如果發現,請在下面留言指出。
最後,如有疑惑或需要討論的地方,可以聯絡我,聯絡方式見我的個人部落格about頁面,地址:About Me。
另外,本人的第一本gitbook書已整理完,關於leetcode刷題題解的,點此進入One day One Leetcode
歡迎持續關注!Thx!