SGI STL學習筆記(1):空間配置器(allocator)

li27z發表於2017-01-07

SGI STL設計了雙層級配置器來完成空間的配置與釋放。當配置區塊超過128bytes時,呼叫第一級配置器;當配置區塊小於128bytes時,呼叫第二級配置器。下面通過原始碼來詳細說明。

第一級配置器 __malloc_alloc_template

第一級配置器是對malloc、realloc以及free的封裝,當呼叫malloc和realloc申請不到記憶體空間的時候,會改呼叫oom_malloc()和oom_realloc(),這兩個函式會反覆呼叫使用者傳遞過來的out of memory handler處理函式,直到能用malloc或者realloc申請到記憶體為止。如果使用者沒有傳遞__malloc_alloc_oom_handler,__malloc_alloc_template會丟擲__THROW_BAD_ALLOC異常。

#if 0
#   include <new>
#   define __THROW_BAD_ALLOC throw bad_alloc
#elif !defined(__THROW_BAD_ALLOC)
#   include <iostream.h>
#   define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1)
#endif

template <int inst>
class __malloc_alloc_template {

private:
    static void* oom_malloc(size_t);
    static void* oom_realloc(void*, size_t);
    static void (* __malloc_alloc_oom_handler)();

public:

    static void* allocate(size_t n)
    {
        // 第一級配置器直接使用malloc()
        void* result = malloc(n);

        // 無法滿足需求時,改用oom_malloc()
        if(0 == result) result = oom_malloc(n);
        return result;
    }

    static void deallocate(void* p, size_t /* n */)
    {
        // 第一級配置器直接使用free()
        free(p);
    }

    static void* reallocate(void* p, size_t /* old_sz */, size_t new_sz)
    {
        // 第一級配置器直接使用realloc()
        void* result = realloc(p, new_sz);

        // 無法滿足需求時,改用oom_realloc()
        if(0 == result) result = oom_realloc(p, new_sz);
        return result;
    }

    // 指定自己的out-of-memory handler
    static void (* set_malloc_handler(void (*f)()))()
    {
        void (* old)() = __malloc_alloc_oom_handler;
        __malloc_alloc_oom_handler = f;
        return(old);
    }

};

// 初值為0。有待客端設定
template <int inst>
void (* __malloc_alloc_template::__malloc_alloc_oom_handler)() = 0;

// oom_malloc和oom_realloc都有內迴圈,不斷呼叫“記憶體不足處理例程”
// 期望在某次呼叫之後,獲得足夠的記憶體而圓滿完成任務
// 如果尚未設定“記憶體不足處理例程”,則會丟出異常,或利用exit(1)硬生生中止程式
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);
    }
}

// 直接將inst指定為0
typedef __malloc_alloc_template<0> malloc_alloc;

第二級配置器 __default_alloc_template

enum { __ALIGN = 8; }  // 小型區塊的上調邊界
enum { __MAX_BYTES = 128; }  // 小型區塊的上限
enum { __NFREELISTS = __MAX_BYTES/__ALIGN; }  // free list個數,16

// 第一引數用於多執行緒環境,暫不討論
template <bool threads, int inst>
class __default_alloc_template {

private:
    // ROUND_UP()將bytes上調至8的倍數
    static size_t ROUND_UP(size_t bytes) {
        return (((bytes) + __ALIGN - 1) & ~(__ALIGN - 1));
    }

private:
    // free list的節點構造
    union obj {
        union obj* free_list_link;  // 指向相同形式的另一個obj
        char client_data[1];  // 指向實際的區塊
    };

private:
    // 16個free-lists
    static obj* volatile free_list[__NFREELISTS];

    // 根據區塊大小決定使用第n號free list,n從0起算
    static size_t FREELIST_INDEX(size_t bytes) {
        return (((bytes) + __ALIGN - 1) / __ALIGN - 1);
    }

    // 為free lists重新填充空間
    static void* refill(size_t n);

    // 配置一大塊空間,可容納nobjs個大小為size的區塊,nobjs根據實際情況可能會降低
    static char* chunk_alloc(size_t size, int &nobjs);

    static char* start_free;  // 記憶體池起始位置
    static char* end_free;  // 記憶體池結束位置
    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};

第二級配置器每次配置一大塊記憶體,都需要維護對應的free list,free lists的構造如下圖:
這裡寫圖片描述

第二級配置器的記憶體分配過程如下:
1.如果申請的記憶體空間大於128bytes, 則交由第一個分配器處理

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

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

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

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

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

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

// 空間配置函式allocate()
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));
    }

    // 小於128就檢查對應的free lists,如果有可用區塊就直接拿來用
    // 如果沒有可用區塊,就將區塊大小上調至8倍數邊界,然後呼叫refill()為free lists重新填充空間
    my_free_list = free_list + FREELIST_INDEX(n);
    result = *my_free_list;
    if(result == 0) {
        void* r = refill(ROUND_UP(n));
        return r;
    }

    *my_free_list = result->free_list_link;
    return (result);
}

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

// 空間釋放函式deallocate()
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;
    }

    // 小於128就找出對應的free lists,將區塊回收
    my_free_list = free_list + FREELIST_INDEX(n);
    q->free_list_link = *my_free_list;
    *my_free_list = q;
}
// 重新填充的新的空間取自記憶體池
// 預設取得20個新區塊,但萬一記憶體池空間不足,獲得的區塊數可能小於20
template<bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
    int nobjs = 20;

    // 呼叫chunk_alloc(),嘗試取得nobjs個區塊作為free list的新節點
    // 這裡的引數nobjs是按引用傳遞的(pass by reference)
    char* chunk = chunk_alloc(n, nobjs);

    obj* volatile* my_free_list;

    obj* result;
    obj* current_obj, * next_obj;
    int i;

    // 如果只獲得一個區塊,這個區塊就分配給呼叫者用,free list無新節點
    if(1 == nobjs) return(chunk);

    //否則準備調整free list,納入新節點
    my_free_list = free_list + FREELIST_INDEX(n);

    // 這一塊準備返回給客端
    result = (obj*)chunk;

    // 以下導引free list指向新配置的空間
    *my_free_list = next_obj = (obj*)(chunk + n);
    // 以下將free list的各節點串接起來
    // 從1開始,因為第0個將返回給客端
    for(i = 1; ; i++) {
        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()的具體過程如下:
1.如果start_free和end_free之間的空間足夠分配size*20大小的記憶體空間,則從這個空間中取出size*20大小的記憶體空間,更新start_free並返回申請到的記憶體空間的起始地址,否則轉到2

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

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

4.呼叫malloc申請大小為(2*n*20 + 附加量)的記憶體空間, 如果申請成功,更新start_free, end_free和heap_size,並重新呼叫chunk_alloc(),否則轉到5

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

// chunk_alloc()從記憶體池取空間給free list使用
// 假設size已經適當上調至8的倍數
// 引數nobjs是按引用傳遞
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;

    // 記憶體池剩餘空間完全滿足需求量,就直接調出20個區塊返回給free list
    if(bytes_left >= total_bytes) {
        result = start_free;
        start_free += total_bytes;
        return(result);
    } else if(bytes_left >= size) {
        // 記憶體池剩餘空間不能完全滿足需求量,但足夠供應一個(含)以上的區塊,就撥出這些空間出去
        // 此時按引用傳遞的nobjs被修改為實際能夠供應的區塊數
        nobjs = bytes_left / size;
        total_bytes = size * nobjs;
        result = start_free;
        start_free += total_bytes;
        return(result);
    } else {
        // 記憶體池剩餘空間連一個區塊的大小也無法提供,就需要利用malloc從heap中配置記憶體
        // 新水量的大小為需求量的兩倍,再加上一個隨著配置次數增加而愈來愈大的附加量
        size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);

        // 讓記憶體池殘餘的零頭還有利用價值
        if(bytes_left > 0) {
            obj* volatile* my_free_list = free_list + FREELIST_INDEX(bytes_left);
            ((obj*)start_free)->free_list_link = *my_free_list;
            *my_free_list = (obj*)start_free;
        }

        start_free = (char*)malloc(bytes_to_get);

        // heap空間不足,malloc失敗
        if(0 == start_free) {
            int i;
            obj* volatile* my_free_list, *p;

            // 遍歷空閒分割槽鏈,如果尚有未用區塊,就釋放該區塊,然後遞迴呼叫chunk_alloc()
            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));
                }
            }
            // 如果到處都沒記憶體可用了,就呼叫第一級配置器,看看oom機制能否有效
            // 這會導致丟擲異常,或記憶體不足的情況得到改善
            end_free = 0;
            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));
    }
}

參考資料:
1.《STL原始碼剖析》
2.STL原始碼學習—-記憶體管理. http://www.cnblogs.com/cobbliu/archive/2012/04/05/2431804.html

相關文章