STL源碼分析:空間配置器淺析

       

目錄

allocator

alloc

一級配置器

二級配置器

自由鏈表

內存分配allocate

_S_refill函數

_S_chunk_alloc函數

內存釋放deallocate

爲什麼要使用free_list?

爲什麼free_list要把128bytes分成16部分?


對於一些容器如vector、map、set之類的,都需要一個模板參數Alloc,用來指定一個空間配置器,這個配置器的作用,就是管理內存的分配和釋放。在SGI STL中,提供了默認的配置器,這也是爲什麼我們在創建一個vector、map的時候不需要傳入第二個參數。以vector爲例,其頭部定義如下:

template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class vector : protected _Vector_base<_Tp, _Alloc> 
{...}

       這裏指定了一個默認的配置器參數, __STL_DEFAULT_ALLOCATOR(_Tp)。 這是一個宏,它有兩種情況:

# ifndef __STL_DEFAULT_ALLOCATOR
#   ifdef __STL_USE_STD_ALLOCATORS
#     define __STL_DEFAULT_ALLOCATOR(T) allocator< T >
#   else
#     define __STL_DEFAULT_ALLOCATOR(T) alloc
#   endif
# endif

      也就是說,默認的配置器要麼是allocator,要麼就是alloc。

allocator

      當然這裏的allocator並不是本文的重點,因爲它只是把operator new和operator delete簡單進行了封裝,如下所示:

template <class T>
inline T* allocate(ptrdiff_t size, T*) {//進行內存的分配,並且返回分配內存的首地址
    set_new_handler(0);
    T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
    if (tmp == 0) {
	cerr << "out of memory" << endl; 
	exit(1);
    }
    return tmp;
}


template <class T>
inline void deallocate(T* buffer) {//delete釋放資源
    ::operator delete(buffer);
}

template <class T>
class allocator {
public:
    typedef T value_type;
    typedef T* pointer;
    typedef const T* const_pointer;
    typedef T& reference;
    typedef const T& const_reference;
    typedef size_t size_type;
    typedef ptrdiff_t difference_type;
    pointer allocate(size_type n) { 
	return ::allocate((difference_type)n, (pointer)0);
    }
    void deallocate(pointer p) { ::deallocate(p); }
    pointer address(reference x) { return (pointer)&x; }
    const_pointer const_address(const_reference x) { 
	return (const_pointer)&x; 
    }
    size_type init_page_size() { 
	return max(size_type(1), size_type(4096/sizeof(T))); 
    }
    size_type max_size() const { 
	return max(size_type(1), size_type(UINT_MAX/sizeof(T))); 
    }
};

       allocator的內容很簡單,下面着重來看看alloc。

alloc

        alloc是一個經過typedef定義的新類型,其定義如下:

typedef __malloc_alloc_template<0> malloc_alloc;

# ifdef __USE_MALLOC

typedef malloc_alloc alloc;
...
#else typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;
...
#endif

       從這裏可以看到,alloc的類型實際上是由__USE_MALLOC這個宏來定義的,如果定義了,那麼alloc使用的就是__malloc_alloc_template,否則就是__default_alloc_template。

       這兩個都是類,前者就是一級配置器,後者是二級配置器。二者都包含了基本的內存分配、內存釋放等函數。

一級配置器

       一級配置器是__malloc_alloc_template類,其定義如下:

template <int __inst>
class __malloc_alloc_template {

private:

  static void* _S_oom_malloc(size_t);
  static void* _S_oom_realloc(void*, size_t);

#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
  static void (* __malloc_alloc_oom_handler)();
#endif

public:

  static void* allocate(size_t __n)
  {
    void* __result = malloc(__n); //直接用malloc進行分配,如果分配失敗調用_S_oom_mallo
    if (0 == __result) __result = _S_oom_malloc(__n);
    return __result;
  }

  static void deallocate(void* __p, size_t /* __n */)
  {
    free(__p); //釋放
  }

  static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
  {
    void* __result = realloc(__p, __new_sz); //用realloc分配,分配失敗調用_S_oom_realloc
    if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
    return __result;
  }

  static void (* __set_malloc_handler(void (*__f)()))()//設置異常捕捉函數,並且返回設之前老的捕捉函數
  {
    void (* __old)() = __malloc_alloc_oom_handler;
    __malloc_alloc_oom_handler = __f;
    return(__old);
  }

};

       一級配置器其實很簡單,它內部的allocate、deallocate和reallocate函數實際上就是對malloc、free和realloc的簡單封裝,還提供了一個設置異常捕捉函數的接口__set_malloc_handler,這個函數的作用就是用傳入的函數指針更新類靜態變量函數指針__malloc_alloc_oom_handler並且返回更新前的__malloc_alloc_oom_handler。

       那麼__malloc_alloc_oom_handler這個函數指針有什麼用呢?

       在allocate和reallocate函數中,如果內存分配失敗,就會調用相應的_S_oom_alloc和_S_oom_realloc,它們的定義如下:

template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)
{
    void (* __my_malloc_handler)();
    void* __result;

    for (;;) {//循環調用malloc,直到分配成功,分配失敗就調用異常捕捉函數
        __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>::_S_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);
    }
}

       二者的作用都是類似的:當內存分配失敗時調用這兩個函數,在這兩個函數內部,會調用__malloc_alloc_oom_handler指向的異常捕捉函數,經過異常捕捉函數處理後,如果順利返回,就會再次調用內存分配函數進行分配,如果內存分配仍然失敗,那麼就會進入下一次循環,再次調用異常捕捉函數進行處理。

       關於異常捕捉函數,用戶是可以通過前面的__set_malloc_handler來自定義的(類似於設置new函數的new_handler),在《Effective C++ 》item 49中提到,這類new_handler的行爲,有以下選擇:

1.讓更多內存可被使用。既然內存分配失敗,說明當前沒有可用的內存,因此new_handler需要採取相應的措施來獲得更多的內存空間;

2.安裝另一個handler。由於多次循環調用的異常處理函數是可以不同的,因此可以在當前的異常處理函數中安裝另一個handler,如果當前的new handler沒有能力讓更多內存供使用,那麼就可以安裝一個有能力的handler;

3.卸除handler。卸除handler相當於讓__malloc_alloc_oom_handler爲0,從程序中可以看到,卸除後如果再次發生分配失敗情況,就會直接拋出bad alloc的異常;

4.拋出異常。可以直接在異常處理函數中拋出ban_alloc的異常;

5.不返回。通過exit或者abort結束程序。

       對於一級配置器來說,和前面的allocator不同,allocator是簡單的封裝了operator new和operator delete,而這裏的一級配置器則是簡單封裝的malloc和free,並且提供了類似於new handler的“malloc handler”

二級配置器

       一級配置器雖然簡單,但是如果分配的區塊很小,那麼就很容易造成內存碎片,這種碎片是外部碎片。並且這樣的小型區塊越多造成的內存碎片就越嚴重,二級配置器就是用於解決這一問題的。

       二級配置器會視情況作出不同決策:如果需要分配的區塊大小超過128bytes,那麼就認爲這是大區塊,就直接使用一級配置器進行分配;如果大小不足128bytes,那麼就認爲這是小區塊,就使用內存池方式來進行分配。

       對於使用內存池的情況,二級配置器的思路是:每次小區塊需要分配的內存都從內存池中去獲取,如果內存池已經沒有空間了,就會重新在堆上爲內存池申請一片較大的空間,以供後續分配,通過內存池的方式,可以明顯減少malloc的調用次數,用集中的內存池空間來取代原來分散的空間,如此來儘量避免內存碎片的產生

       二級配置器是__default_alloc_template類,它把內存池空間分作兩部分,一部分是放在自由鏈表上的已從內存池中取出但還未分配出去,另一部分則是內存池中未被取出的剩餘空間

      __default_alloc_template用三個靜態變量來描述內存池:

  static char* _S_start_free;   //當前內存池剩餘空間的起始地址
  static char* _S_end_free;    //當前內存池剩餘空間的末尾
  static size_t _S_heap_size;   //整個已申請的多個內存池總大小

       需要注意的是這裏的_S_heap_size,是指內存池總大小,它不僅包含當前的內存池,如果當前內存池大小不足,那麼就還會申請一個新的內存池,而這個變量是會記錄所有申請的內存池的總大小的。

自由鏈表

       先來說說自由鏈表是什麼。

       前面說了,使用內存池的情況,是分配的區塊小於128bytes,STL中把這128按照每8個字節分成一組,分了16組,分別對應8、16、24、32、40、48、56、64、72、80、88、96、104、112、120、128bytes。自由鏈表結構如下:

       自由鏈表有以下特點:

1.本身是一個大小爲16的數組_S_free_list,數組元素類型爲obj *;

2.每一個元素中包含了一個obj結點的地址,指向一個鏈表;

3.指向的鏈表由一系列obj類型的結點構成,每個結點都是一個內存塊,同一鏈表中的內存塊大小都相同;

4.所有鏈表上的內存塊都是從內存池中取出,但是還沒有真正分配出去

5.如果需要分配8個字節大小的內存塊,就返回_S_free_list[0]對應的鏈表中的第一個結點,然後該鏈表把這個結點淘汰出去,第二個結點成爲新的結點;如果需要分配16個字節,就對應_S_free_list[1];分配24、32、...、128以此類推;

       到這裏簡單的說了一下自由鏈表的特點和結構,如前所述,自由鏈表裏的各個鏈表結點都是obj類型,該類型定義如下:

union _Obj {
        union _Obj* _M_free_list_link;
        char _M_client_data[1];    /* The client sees this.        */
  };

        這是一個聯合體,也就是說,每條鏈表中的每個結點都是一個聯合體_Obj類型,它其中包含了兩個成員:指向另一個聯合體_Obj類型的指針_M_free_list_link,和一個1字節的char數組_M_client_data。

        _M_free_list_link很好理解,就是一個指向同類型的指針,它就是鏈表結點與結點之間的“後驅指針”。

        而至於_M_client_data,雖然這裏定義了但是整個STL中都沒有用到,根據書中所說,_M_client_data相當於是一個指向實際區塊的指針,但是個人覺得本身分配實際區塊時返回的就是其地址,難道誰還會通過_M_client_data去獲取地址麼?顯然不夠使人信服,查過一些其它資料但是都莫衷一是,既然STL中沒有用到,無法直觀體會其妙處,因此這裏就不做過多研究了。(如果讀者有比較好的解釋,希望留言指教)。

內存分配allocate

       自由鏈表的每個元素都對應了內存大小爲8的倍數的內存塊所組成的鏈表,但是傳入的需要分配的大小往往不是8的倍數,舉個例子,如果需要用allocate分配大小爲1~8bytes的空間,那麼顯然就應該分配大小爲8字節的內存塊,如果需要分配大小爲9~16的空間,就應該分配大小爲16的內存塊。因此,對於傳入的參數__n,需要先將其轉換到不小於__n的最小的8的倍數,然後分配該數值對於的內存塊。allocate函數定義如下:

static void* allocate(size_t __n)
  {
    void* __ret = 0;

    if (__n > (size_t) _MAX_BYTES) {//如果超過128字節
      __ret = malloc_alloc::allocate(__n);//直接使用一級配置器來分配
    }
    else {
      _Obj* __STL_VOLATILE* __my_free_list
          = _S_free_list + _S_freelist_index(__n); //_S_freelist_index找到__n對應的在數組的元素索引,__my_free_list就是數組上的那個元素,這個元素所指向的鏈表中結點都是大小爲__n上調至8的倍數的內存塊
      // 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
		//__my_free_list就是大小爲__n對應的鏈表所在數組中的那個元素地址
		//數組的每個元素都是一個_Obj *類型,* __my_free_list取得該obj指針
      _Obj* __RESTRICT __result = *__my_free_list;//__result保存第一個obj結點的地址
      if (__result == 0)//說明__my_free_list指向一個null的obj結點
        __ret = _S_refill(_S_round_up(__n));
      else {
        *__my_free_list = __result -> _M_free_list_link; //把下一個obj結點地址放到數組中
        __ret = __result;
      }
    }

    return __ret;//返回obj結點的地址
  };

該函數主要做了以下幾件事:

1.如果需要分配的空間大小超過128,就認爲是大區塊,使用一級配置器進行分配;

2.如果使用二級配置器,先找到不小於傳入的參數__n的最小的8的倍數設爲m,然後找到大小爲m的區塊所在鏈表對應自由鏈表數組的元素(如分配22bytes的空間就要找到_S_free_list[2]);

3.找到相應的數組元素後,由於該元素存放的就是對應鏈表的頭結點地址,取出元素值,如果爲0說明自由鏈表中不存在多餘的大小爲m的內存塊了,此時就應該從內存池中去申請內存塊;如果不爲0,說明自由鏈表中存在大小爲m的內存塊,就將這個內存塊結點從鏈表中抽出,第二個內存塊作爲新的頭結點,返回抽出的內存塊的地址。

       可以看到,allocate函數返回的就是分配的內存塊的地址,第1步自然不用說,第2步需要對傳入的參數__n進行處理,通過_S_freelist_index函數實現:

static  size_t _S_freelist_index(size_t __bytes) {
        return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1);//找到能存放下__bytes個字節的那一條鏈表
  }

       該函數返回的是free_list的索引。

       第3步發生的情況是,找到了__n對應的free_list元素,但是這個元素下面沒有內存塊組成的鏈表,這就說明自由鏈表中沒有相應大小的內存塊,此時就需要到內存池中去申請內存塊了。如下所示:

_S_refill函數

       比如說一次要分配8個字節的內存,二級配置器並不會只是從內存池中取出8個字節,而是取出n*8的大小,然後把這n*8的大小分成n塊,每塊8個字節,把這n塊連接起來成爲鏈表,把第一塊的地址放在自由鏈表中8字節對應的位置。這樣,以後每次需要分配8個字節內存時,直接從自由鏈表上8字節對應的位置,找到對應的鏈表,返回第一個結點地址即可。這裏所說的n,默認值爲20,這一點在_S_refill函數中有所體現。

template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
    int __nobjs = 20;
    char* __chunk = _S_chunk_alloc(__n, __nobjs);//向內存中申請20個大小爲n的塊,實際上是申請大小爲20*__n的連續空間,實際上分配的塊數保留在__nobjs中
    _Obj* __STL_VOLATILE* __my_free_list;
    _Obj* __result;
    _Obj* __current_obj;
    _Obj* __next_obj;
    int __i;

    if (1 == __nobjs) return(__chunk);//如果只分配了一個塊,那麼就直接返回這個塊的地址

	//如果分配了多個大小爲__n的塊(這些塊的地址都是連續的,就要把這些塊鏈起來,放到free_list下面的鏈表中
    __my_free_list = _S_free_list + _S_freelist_index(__n);//找到這個塊大小對應的free_list的位置

    /* Build free list in chunk */
      __result = (_Obj*)__chunk;//先記錄下分配的塊的首地址
      *__my_free_list = __next_obj = (_Obj*)(__chunk + __n);//__chunk偏移__n就是下一個塊的地址,把下一個塊的地址保存在__next_obj中,並且放在free_list下鏈表的頭部
      for (__i = 1; ; __i++) {//從__newxt_obj開始,依次把這些塊鏈起來
        __current_obj = __next_obj;
        __next_obj = (_Obj*)((char*)__next_obj + __n);
        if (__nobjs - 1 == __i) {//已經到最後一個塊了
            __current_obj -> _M_free_list_link = 0;//最後一個塊的後驅爲0
            break;
        } else {
            __current_obj -> _M_free_list_link = __next_obj;
        }
      }
    return(__result);
}

該函數做了以下幾件事:

1.通過_S_chunk_alloc函數從內存池中申請20個大小爲n的內存塊,實際申請的內存塊數爲nobjs;

2.如果只申請了一個塊,那麼直接返回這個塊的首地址即可;

3.如果申請了nobjs個塊(nobjs>1),先找到n大小的塊對應在free_list的位置,然後把這nobjs個塊一個接一個的用後驅指針鏈成一個鏈表,末尾結點的後驅指針爲0,返回首結點地址。

     由此可見,_S_refill的作用,是從內存池中申請多個大小爲__n的內存塊放到自由鏈表中的相應位置。而具體的申請,則是通過_S_chunk_alloc函數完成的。

_S_chunk_alloc函數

       由於_S_refill每次申請時並不只是申請一個區域塊,這也就使得每次從內存池中申請內存時都會告訴內存池兩個信息:需要申請的塊數nobjs和每一個塊的大小n。此時就會出現以下幾種情況:

1.內存池中剩餘大小足以分配n*nobjs,那麼直接返回內存池中剩餘空間的首地址,並且偏移n*nobjs;

2.內存池中剩餘大小不足以分配nobjs個大小爲n的塊,但是至少可以分配1個大小爲n的塊,那麼就能分配多少個大小爲n的塊就分配多少個,然後返回剩餘空間首地址後進行相應的偏移;

3.內存池中剩餘大小連一個大小爲n的塊都分配不了,那麼就要考慮重新從堆上分配空間給內存池了,由於分配後的內存池必定是一個全新的內存池,所以在重新分配之前應該把當前內存池中剩餘空間利用起來,把剩餘空間放到自由鏈表上(由於內存池大小必定爲8的倍數,而每次從內存池中取出的大小也是8的倍數,所以剩餘的空間也必定是8的倍數,所以就不會出現7個字節接到8個字節的鏈表,12個字節接到16個字節這樣類似的情況)。

    接着分配一個新的內存池。如果分配成功,那就很好辦,重新用nobjs和n向新分配的內存池申請空間即可。

    如果此時分配失敗,那麼就試着在已經掛在自由鏈表的那一部分空間中找出是否還有大於n的塊還未分配的,如果有,就把大於n的這個塊的首地址返回。這個過程就是:從自由鏈表上n+8對應的結點開始遍歷(不能從頭開始,因爲比n小的結點是放不下n的),查看是否有結點對應的鏈表不爲空,不爲空說明還有空間可以用來分配大小爲n的空間,就把這個空間的首地址返回。不管是否能找到更大的空間來分配一個n大小的塊,都會試着調用一級配置器中的allocate函數,此時要麼就是最差的情況:實在沒有辦法找到空間來分配,那麼allocate函數勢必會拋出異常,要麼就是allocate函數能夠改善內存不足的情況。

      二級配置器中就是使用_S_chunk_alloc函數來向內存池中申請nobjs個大小爲n的空間,其定義如下所示:

template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size, 
                                                            int& __nobjs)
{
    char* __result;
    size_t __total_bytes = __size * __nobjs;  //總共需要分配的大小
    size_t __bytes_left = _S_end_free - _S_start_free;   //內存池的剩餘大小

    if (__bytes_left >= __total_bytes) {  //如果內存池的剩餘空間足夠
        __result = _S_start_free;  //start_free就是實際分配的首地址
        _S_start_free += __total_bytes;   //相當於內存池空間減少
        return(__result);  //返回分配空間首地址
    } else if (__bytes_left >= __size) {  //內存池的剩餘空間不足裝下所有,但是至少能分配一個節點
        __nobjs = (int)(__bytes_left/__size);//計算能分配的結點數目
        __total_bytes = __size * __nobjs;//實際分配空間大小
        __result = _S_start_free;//實際分配空間的首地址
        _S_start_free += __total_bytes;//相當於內存池空間減少已分配的大小
        return(__result);
    } else {//內存池的剩餘空間連一個節點都不夠分配
        size_t __bytes_to_get = 
	  2 * __total_bytes + _S_round_up(_S_heap_size >> 4); //一次性多分配一些
        // Try to make use of the left-over piece.
        if (__bytes_left > 0) { //如果內存池還有剩餘的空間
            _Obj* __STL_VOLATILE* __my_free_list =
                        _S_free_list + _S_freelist_index(__bytes_left);//把內存池剩餘的空間也利用起來

            ((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;//_S_start_free相當於內存池剩餘的空間的首地址,把剩下的空間作爲一個結點插到free_list的頭部
            *__my_free_list = (_Obj*)_S_start_free;
        }
        _S_start_free = (char*)malloc(__bytes_to_get);//重新分配
        if (0 == _S_start_free) {//分配失敗
            size_t __i;
            _Obj* __STL_VOLATILE* __my_free_list;
	    _Obj* __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 <= (size_t) _MAX_BYTES;
                 __i += (size_t) _ALIGN) {//從__size(已經成爲了8的倍數)對應的節點開始遍歷free_list上的所有節點
                __my_free_list = _S_free_list + _S_freelist_index(__i);
                __p = *__my_free_list;//獲取第一個大小爲__i的obj
                if (0 != __p) {//__p不爲0說明找到了一個比__size大的節點,就把這個obj放到內存池中
                    *__my_free_list = __p -> _M_free_list_link;
                    _S_start_free = (char*)__p;//把這個obj放到內存池中
                    _S_end_free = _S_start_free + __i;
                    return(_S_chunk_alloc(__size, __nobjs));//此時內存池中必定能放下__size大小了,就再次調用chunk
                    // Any leftover piece will eventually make it to the
                    // right free list.
                }
            }//到這裏說明內存池中無論如何都找不到__size的空間了,就只能重新分配了
	    _S_end_free = 0;	// In case of exception.
            _S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);//重新分配大小
            // This should either throw an
            // exception or remedy the situation.  Thus we assume it
            // succeeded.
        }
        _S_heap_size += __bytes_to_get;//記錄內存池的總大小
        _S_end_free = _S_start_free + __bytes_to_get;
        return(_S_chunk_alloc(__size, __nobjs));
    }
}

       當內存池中剩餘空間連一個內存塊都不夠時,會用__bytes_to_get 來計算需要分配的新的內存池的大小。

       即size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4);

       其中的__total_bytes是nobjs * n的總大小,這個大小乘以2,然後還有一個附加量。乘以2很好理解,還有一個附加量,這個附加量會隨分配次數的增加而增大(相當於一旦出現內存池不足的情況,那麼附加量就會上一次分配內存池時的附加量大),這裏使用了一個_S_round_up函數,該函數作用是把參數提升到8的倍數。其定義如下:

static size_t
  _S_round_up(size_t __bytes) //提升到8的倍數
    { return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1));

       這樣就能保證內存池的大小必定爲8的倍數,有利於提高內存池的利用率(在前面分析的_S_chunk_alloc進行的第3步行爲中有描述)。

內存釋放deallocate

       該函數定義如下:

static void deallocate(void* __p, size_t __n) //需要指明需要釋放的空間的大小n
  {
    if (__n > (size_t) _MAX_BYTES)
      malloc_alloc::deallocate(__p, __n);//如果大於128就用一級空間配置器
    else { //不超過128就是用二級空間配置器
      _Obj* __STL_VOLATILE*  __my_free_list
          = _S_free_list + _S_freelist_index(__n);  //根據n找到節點數組中相應的元素
      _Obj* __q = (_Obj*)__p; //

      // acquire lock
#       ifndef _NOTHREADS
      /*REFERENCED*/
      _Lock __lock_instance;
#       endif /* _NOTHREADS */
      __q -> _M_free_list_link = *__my_free_list;  //相當於把需要刪除的obj結點還回到鏈表頭部
      *__my_free_list = __q;
      // lock is released here
    }
  }

      可以看到,最重要的一點,釋放空間時又會把它歸還到內存池中。deallocate函數需要指定釋放的空間大小n,根據這個大小來找到它需要歸還的目的位置,然後把這個被釋放的內存塊插到相應鏈表的頭部即可。

爲什麼要使用free_list?

       使用內存池的一個簡單方法就是記錄當前內存池剩餘空間的首地址,需要取出多大的空間就取出後讓這個地址偏移多少,這樣不管是取1個2個4個8個字節都是可以的,並且還比自由鏈表這種方式更爲便捷。但是這種方式最大的問題是內存塊釋放之後無法歸還

       好的內存池設計必然是“有借有還”的,而free_list就是這樣。free_list的每個元素都對應了一種固定大小的內存塊鏈表,通過需要釋放的內存塊的大小,就很容易能找到這個內存塊需要歸還的位置,然後直接將其添加到相應鏈表中即可。

        而“有借無還”的內存池,每次借出空間都是無規律的,需要多少就借出多少,可能連着的兩個內存塊1個是1字節而另一個是100個字節,如果你要讓這些借出去的內存塊知道歸還的位置,就不得不維護其它的數據結構來記錄分配出去的內存塊的位置,這並不一定就比free_list簡單。

爲什麼free_list要把128bytes分成16部分?

      這裏有兩個問題,一個是爲什麼要分,另一個是爲什麼要分成16部分。

      先來說下爲什麼要分。上面說了爲什麼要使用free_list,因爲free_list的一個重要特點就是可以根據需要分配/釋放的內存塊大小來找到相應的位置,如果不分,那就相當於從內存池中取出來的所有內存塊只有一種,爲了滿足128bytes的條件,自然這種內存塊就是128bytes大小的了,這樣一來,如果你只需要分配1字節,那麼內存池實際分配給你的也是128bytes,這樣就造成了內存碎片,而且是內部碎片。

       爲了避免內存碎片,自然是讓free_list適應各種大小。那麼要不把內存池分成128種內存塊?每一字節都對應一種?這樣就違背了內存對齊的原則。因爲整個內存池也是從堆中分配的,那麼內存池的首地址必然是邊界對齊的,如果內存池中的內存塊存在各個字節的情況,那麼內存池內部分出去的就顯然不是內存對齊的了,這樣帶來的後果就是訪存變慢,或者直接無法運行。

       因此,爲了保證內存對齊,索性把內存池中的內存塊按照8來區分,這樣不管如何分配,分配出去的內存塊必定都是邊界對齊的,有考慮到要儘量適應多種大小的內存塊,所以就把128bytes以內的大小分成了8 16 24...128按8遞增一共16種大小的內存塊,這也就使得free_list數組大小爲16,與這16種內存塊一一對應。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章