1. 程式人生 > >帶你深入理解STL之空間配置器(思維導圖+原始碼)

帶你深入理解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!