1. 程式人生 > >SGI STL內存配置器(一):內存泄漏?

SGI STL內存配置器(一):內存泄漏?

分解 產生 for void 客戶端 lin call free 創建過程

閱讀了Alexander大神的SGI STL源碼,膜拜,很高質量的源碼,獲益匪淺。溫故而知新!下文中所有STL如無特殊說明均指SGI版本實現。

STL 內存配置器

STL對內存管理最核心部分我覺得是它將C++對象創建過程分解為構造、析構和內存分配、釋放!擺脫了由於頻繁調用new或malloc函數向操作系統申請空間而造成的低效。其中析構操作時對具有non-trival、trival 析構函數的class區別對待也提高了效率。至於SGI的兩級配置器結構則屬於錦上添花的類型。

STL兩級結構的內存配置器使得STL能對小的空間內存分配管理更為合理,其二級結構表現為該配置器在獲取內存和釋放內存時分為兩種情況,對大於128Byte的內存塊直接調用malloc,不大於128Byte的內存塊使用一個內存池和一個鏈表cache來單獨維護。接下來結合代碼中的註釋,概述一下這個二級結構的工作機制!

該內存配置器實現在類__default_alloc_template 中 [源碼1]:

template <bool threads, int inst>
class __default_alloc_template {
...
};

 

其中內存池相關變量代碼:

    // Chunk allocation state.
    static char* _S_start_free; //內存池起點
    static char* _S_end_free;  //內存池終點
    static size_t _S_heap_size;//共向操作系統申請過的多少空間補充給內存池

用來維護從內存池中取出來返回給過客戶端代碼的小內存塊的鏈表:

# if defined(__SUNPRO_CC) || defined(__GNUC__) || defined(__HP_aCC)
    static _Obj* __STL_VOLATILE _S_free_list[]; 
        // Specifying a size results in duplicate def for 4.1
# else
    static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS];//數組的每個元素的值為一串相同size的內存塊鏈表頭,其中 _NFREELISTS = 16, 內存塊size取值為8,16,..., 8*16
# endif

而在__default_alloc_template 中對內存獲取和釋放主要通過兩個靜態成員函數來完成:

  獲取 allocate(size_t __n) :

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

    if (__n > (size_t) _MAX_BYTES) {//大於128Byte
      __ret = malloc_alloc::allocate(__n);//這裏其實是對malloc的包裝
    }
    else {
      _Obj* __STL_VOLATILE* __my_free_list
          = _S_free_list + _S_freelist_index(__n);//從鏈表維護的cache中取出滿足條件的內存塊
      // 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
      _Obj* __RESTRICT __result = *__my_free_list;
      if (__result == 0)//如果取出失敗,從內存池中獲取內存來填充Cache
        __ret = _S_refill(_S_round_up(__n));
      else {//取出成功則直接返回
        *__my_free_list = __result -> _M_free_list_link;
        __ret = __result;
      }
    }

    return __ret;
  };

  釋放 deallocate(void* __p, size_t __n):

 /* __p may not be 0 */
  static void deallocate(void* __p, size_t __n)
  {
    if (__n > (size_t) _MAX_BYTES)//大於128Byte直接調用free釋放
      malloc_alloc::deallocate(__p, __n);
    else {//釋放回鏈表組織的Cache中
      _Obj* __STL_VOLATILE*  __my_free_list
          = _S_free_list + _S_freelist_index(__n);
      _Obj* __q = (_Obj*)__p;

      // acquire lock
#       ifndef _NOTHREADS
      /*REFERENCED*/
      _Lock __lock_instance;
#       endif /* _NOTHREADS */
      __q -> _M_free_list_link = *__my_free_list;
      *__my_free_list = __q;
      // lock is released here
    }
  }

STL內存配置器有沒有內存泄漏?

看了源碼後很多人疑惑為什麽在該Allocator的實現裏只有對內存池malloc的代碼,沒看到類似free這樣釋放內存的代碼,甚至該Allocator類都沒有析構函數,這樣不是會存在內存泄漏嗎?

其實不然。對於由鏈表維護的內存,其內存的釋放工作應該是上一層調用者負責,比如容器Vector在析構函數中就將其申請的所有capacity大小的內存釋放。相反內存池的中的內存將會一直保留直到程序退出。有的同學可能會認為“這不就是內存泄漏嗎?比如創建了一個Vector變量,到Vector析構了之後再內存中竟然有一塊內存沒有被系統回收,這不是memory leak嗎”。其實不然:

1. 申請的內存沒有被及時釋放 不等於 內存泄漏

在單線程中,由於該Allocator中記錄內存池起始的指針是靜態類型,所以只要是你在同一個線程中,無論你創建多少個Allocator,記錄內存池的變量都是同一個,換句話說,當下次再創建Vector時,還是使用上一次使用的那個。也就是說他的存在時有意義的,這也是cache或memory pool的意義所在!

2. 該內存池不會瘋狂野生長

這個內存池的空間其實是很小的,因為大於128Byte的內存申請都直接轉調了malloc,從源碼中也可以看出,內存池每次重新灌水的新容量是2*total_size + round_up(heap_size >> 4)。

內存池的存在是為了避免大量內存碎片的產生,代價是管理內存所需要多付出的時間和空間消耗。

以上就是內存池一種存在直至程序退出的原因。

在GCC 5.4.0 中的使用的SGI已經對該種設計做了大幅修改:

1. 默認的Allocator也不在是侯捷一書中說指出的具有內存池的配置器,而是"\usr\include\c++\5\ext\new_allocator.h"其實現直接調用new;

2. 而之前相應的具備內存池的配置器則被當做STL的擴展,實現於"\usr\include\c++\5\ext\pool_allocator.h"中,且該實現不在存在內存池的設計,只保留了使用鏈表將小內存塊連接起來的設計(使用時記得include該文件路徑,命名空間為__gnu_cxx::__pool_alloc<int> )。

而在llvm的實現中Allocator也是直接調用的new函數。

FYI:

GCC更換Allocator設計,http://www.cppblog.com/peakflys/archive/2015/01/14/209513.html

SGI源碼,https://www.sgi.com/tech/stl/download.html

SGI源碼,allocator,https://www.sgi.com/tech/stl/stl_alloc.h

SGI源碼,vector,https://www.sgi.com/tech/stl/stl_vector.h

SGI STL內存配置器(一):內存泄漏?