1. 程式人生 > >【STL深入學習】SGI STL空間配置器詳解(二)-第二級空間配置器

【STL深入學習】SGI STL空間配置器詳解(二)-第二級空間配置器

本文講解SGI STL空間配置器的第二級配置器。

相比第一級配置器,第二級配置器多了一些機制,避免小額區塊造成記憶體的碎片。不僅僅是碎片的問題,配置時的額外負擔也是一個大問題。因為區塊越小,額外負擔所佔的比例就越大。

額外負擔是指動態分配記憶體塊的時候,位於其頭部的額外資訊,包括記錄記憶體塊大小的資訊以及記憶體保護區(判斷是否越界)。要想了解詳細資訊,請參考MSVC或者其他malloc實現。

SGI STL第二級配置器具體實現思想

如下:

  1. 如果要分配的區塊大於128bytes,則移交給第一級配置器處理。
  2. 如果要分配的區塊小於128bytes,則以記憶體池管理(memory pool),又稱之
    次層配置(sub-allocation):每次配置一大塊記憶體,並維護對應的自由連結串列(free-list)。下次若有相同大小的記憶體需求,則直接從free-list中取。如果有小額區塊被釋放,則由配置器回收到free-list中。

下面詳細節介紹記憶體池管理技術。

在第二級配置器中,小額區塊記憶體需求大小都被上調至8的倍數,比如需要分配的大小是30bytes,就自動調整為32bytes。系統中總共維護16個free-lists,各自管理大小為8,16,...,128bytes的小額區塊。

為了維護連結串列,需要額外的指標,為了避免造成另外一種額外的負擔,這裡採用了一種技術:用union表示連結串列節點結構:

  union obj {
        union obj * free_list_link;//指向下一個節點
        char client_data[1];    /* The client sees this. */
  };

union能夠實現一物二用的效果,當節點所指的記憶體塊是空閒塊時,obj被視為一個指標,指向另一個節點。當節點已被分配時,被視為一個指標,指向實際區塊。

以下是第二級配置器總體實現程式碼概覽:

template <bool threads, int inst>
class __default_alloc_template {

private:
  // 實際上我們應該使用 static const int x = N
  // 來取代 enum { x = N }, 但目前支援該性質的編譯器還不多。
# ifndef __SUNPRO_CC
    enum {__ALIGN = 8};
    enum {__MAX_BYTES = 128};
    enum {__NFREELISTS = __MAX_BYTES/__ALIGN};
# endif
  static size_t ROUND_UP(size_t bytes) {
        return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
  }
__PRIVATE:
  union obj {
        union obj * free_list_link;
        char client_data[1];    /* The client sees this. */
  };
private:
# ifdef __SUNPRO_CC
    static obj * __VOLATILE free_list[]; 
        // Specifying a size results in duplicate def for 4.1
# else
    static obj * __VOLATILE free_list[__NFREELISTS]; 
# endif
  static  size_t FREELIST_INDEX(size_t bytes) {
        return (((bytes) + __ALIGN-1)/__ALIGN - 1);
  }

  // Returns an object of size n, and optionally adds to size n free list.
  static void *refill(size_t n);
  // Allocates a chunk for nobjs of size "size".  nobjs may be reduced
  // if it is inconvenient to allocate the requested number.
  static char *chunk_alloc(size_t size, int &nobjs);

  // Chunk allocation state.
  static char *start_free;
  static char *end_free;
  static size_t heap_size;

 /* n must be > 0      */
  static void * allocate(size_t n){...}

 /* p may not be 0 */
  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[
# ifdef __SUNPRO_CC
    __NFREELISTS
# else
    __default_alloc_template<threads, inst>::__NFREELISTS
# endif
] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };


空間配置函式allocate()

具體實現如下:

  1. 要分配的區塊小於128bytes,呼叫第一級配置器。
  2. 否則,向對應的free-list尋求幫助。
    • 對應的free list有可用的區塊,直接拿過來用。
    • 如果沒有可用的區塊,呼叫函式refill()為free list重新填充空間。

程式碼如下:

 /* n must be > 0      */
  static void * allocate(size_t n)
  {
    obj * __VOLATILE * my_free_list;
    obj * __RESTRICT result;

    if (n > (size_t) __MAX_BYTES) {
        return(malloc_alloc::allocate(n));
    }
    my_free_list = free_list + FREELIST_INDEX(n);
    // Acquire the lock here with a constructor call.
    // This ensures that it is released in exit or during stack
    // unwinding.
#       ifndef _NOTHREADS
        /*REFERENCED*/
        lock lock_instance;
#       endif
    result = *my_free_list;
    if (result == 0) {
        void *r = refill(ROUND_UP(n));
        return r;
    }
    *my_free_list = result -> free_list_link;
    return (result);
  };
這裡需要注意的是,每次都是從對應的free list的頭部取出可用的記憶體塊。
圖示如下:


圖一 從free list取出空閒區塊示意圖

refill()-為free list填充空間

當發現對應的free list沒有可用的空閒區塊時,就需要呼叫此函式重新填充空間。新的空間將取自於記憶體池。記憶體池的管理後面會講到。

預設狀況下取得20個新區塊,但是如果記憶體池空間不夠,取得的節點數就有可能小於20.下面是SGI STL中的原始碼:

/* Returns an object of size n, and optionally adds to size n free list.*/
/* We assume that n is properly aligned.                                */
/* We hold the allocation lock.                                         */
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
    int nobjs = 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);
    my_free_list = free_list + FREELIST_INDEX(n);

    /* Build free list in chunk */
      result = (obj *)chunk;
      *my_free_list = next_obj = (obj *)(chunk + n);
      for (i = 1; ; i++) {//將各節點串接起來(注意,索引為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-從記憶體池中取空間供free list使用

具體實現思想如下:

  1. 記憶體池剩餘空間完全滿足20個區塊的需求量,則直接取出對應大小的空間。
  2. 記憶體池剩餘空間不能完全滿足20個區塊的需求量,但是足夠供應一個及一個以上的區塊,則取出能夠滿足條件的區塊個數的空間。
  3. 記憶體池剩餘空間不能滿足一個區塊的大小,則
    • 首先判斷記憶體池中是否有殘餘零頭記憶體空間,如果有則進行回收,將其編入free list。
    • 然後向heap申請空間,補充記憶體池。
      • heap空間滿足,空間分配成功。
      • heap空間不足,malloc()呼叫失敗。則
        • 搜尋適當的free list(適當的是指:尚有未用區塊,並且區塊足夠大),調整以進行釋放,將其編入記憶體池。然後遞迴呼叫chunk_alloc函式從記憶體池取空間供free list。
        • 搜尋free list釋放空間也未能解決問題,這時候呼叫第一級配置器,利用out-of-memory機制嘗試解決記憶體不足問題。如果可以就成功,否則排除bad_alloc異常。

原始碼如下:

/* We allocate memory in large chunks in order to avoid fragmenting     */
/* the malloc heap too much.                                            */
/* We assume that size is properly aligned.                             */
/* We hold the allocation lock.                                         */
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);
    } 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);//注意此處申請的空間的大小
        // Try to make use of the left-over piece.
        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);
        if (0 == start_free) {
            int i;
            obj * __VOLATILE * my_free_list, *p;
            // Try to make do with what we have.  That can't
            // hurt.  We do not try smaller requests, since that tends
            // to result in disaster on multi-process machines.
            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));
                    // Any leftover piece will eventually make it to the
                    // right free list.
                }
            }
	    end_free = 0;	// In case of exception.
            start_free = (char *)malloc_alloc::allocate(bytes_to_get);
            // This should either throw an
            // exception or remedy the situation.  Thus we assume it
            // succeeded.
        }
        heap_size += bytes_to_get;
        end_free = start_free + bytes_to_get;
        return(chunk_alloc(size, nobjs));
    }
}
注意:從heap中配置記憶體時,配置的大小為需求量的兩倍再加上一個隨配置次數逐漸增加的附加量。
記憶體池例項演示:

圖二 記憶體池例項演示

程式一開始,客戶呼叫chunk_alloc(32,20),因為此時記憶體池和free list空間均不夠,於是呼叫malloc從heap配置40個32bytes區塊,其中一個供使用,另一個交給free_list[3]維護。剩餘的20個留給記憶體池。接下來呼叫chunk_alloc(64,20), 此 時 free_list[7] 空空如也,必須向記憶池要求支援。記憶池只夠供應  (32*20)/64=10 個 64bytes區塊,就把這 10 個區塊傳回,第 1 個交給客端,餘 9個由 free_list[7] 維護。此時記憶池全空。接下來再呼叫chunk_alloc(96, 20),此時 free_list[11] 空空如也,必須向記憶池要求支援,而記憶池此時也是空的,於是以malloc()配 置 40+n(附加量)個 96bytes 區塊,其中第 1 個交出,另 19 個交給 free_list[11] 維護,餘 20+n(附加量)個區塊留給記憶池……。 

萬一山窮水盡,整個system heap 空間都不夠了(以至無法為記憶池注入活水源 頭),alloc()行動失敗,chunk_alloc()就到處尋找有無可用區塊, 且區塊夠大之free lists。找到的話就挖一塊交出,找不到的話就呼叫第一級配 置器。第一級配置器其實也是使用malloc()來配置記憶體,但它有 out-of-memory 處理機制(類似 new-handler   機制),或許有機會釋放其它的記憶體拿來此處使用。 如果可以,就成功,否則發出bad_alloc異常。 

deallocate()-空間釋放函式

  1. 如果需要回收的區塊大於128bytes,則呼叫第一級配置器。
  2. 如果需要回收的區塊小於128bytes,找到對應的free -list,將區塊回收。注意是將區塊放入free -list的頭部。

SGI STL原始碼:

 /* p may not be 0 */
  static void deallocate(void *p, size_t n)
  {
    obj *q = (obj *)p;
    obj * __VOLATILE * my_free_list;

    if (n > (size_t) __MAX_BYTES) {
        malloc_alloc::deallocate(p, n);
        return;
    }
    my_free_list = free_list + FREELIST_INDEX(n);
    // acquire lock
#       ifndef _NOTHREADS
        /*REFERENCED*/
        lock lock_instance;
#       endif /* _NOTHREADS */
    q -> free_list_link = *my_free_list;
    *my_free_list = q;
    // lock is released here
  }


參考資料:

下面詳細介紹記憶體池管理技術。