C++ 空間配置器(allocator)


C++ 空間配置器(allocator)

 

在STL中,Memory Allocator 處於最底層的位置,為一切的 Container 提供存儲服務,是一切其他組件的基石。對於一般使用 STL 的用戶而言,Allocator 是不可見的,如果需要對 STL 進行擴展,如編寫自定義的容器,就需要調用 Allocator 的內存分配函數進行空間配置。

在C++中,一個對象的內存配置和釋放一般都包含兩個步驟,對於內存的配置,首先是調用operator new來配置內存,然后調用對象的類的構造函數進行初始化;而對於內存釋放,首先是調用析構函數,然后調用 operator delete進行釋放。 如以下代碼:

class Foo { ... }; Foo* pf = new Foo; ... delete pf;


Allocator 的作用相當於operator new 和operator delete的功能,只是它考慮得更加細致周全。SGI STL 中考慮到了內存分配失敗的異常處理,內置輕量級內存池(主要用於處理小塊內存的分配,應對內存碎片問題)實現, 多線程中的內存分配處理(主要是針對內存池的互斥訪問)等,本文就主要分析 SGI STL 中在這三個方面是如何處理的。在介紹着三個方面之前,我們先來看看 Allocator的標准接口。

 

1. Allocator 的標准接口

在 SGI STL 中,Allocator的實現主要在文件 alloc.h 和 stl_alloc.h 文件中。根據 STL 規范,Allocator 需提供如下的一些接口(見 stl_alloc.h 文件的第588行開始的class template allocator):

// 標識數據類型的成員變量,關於中間的6個變量的涵義見后續文章(關於Traits編程技巧) typedef alloc _Alloc; typedef size_t size_type; typedef ptrdiff_t difference_type; typedef _Tp* pointer; typedef const _Tp* const_pointer; typedef _Tp& reference; typedef const _Tp& const_reference; typedef _Tp value_type; template <class _Tp1> struct rebind {  typedef allocator<_Tp1> other; }; // 一個嵌套的class template,僅包含一個成員變量 other // 成員函數 allocator() __STL_NOTHROW {} // 默認構造函數,其中__STL_NOTHROW 在 stl_config.h中定義,要么為空,要么為 throw() allocator(const allocator&) __STL_NOTHROW {} // 拷貝構造函數 template <class _Tp1> allocator(const allocator<_Tp1>&) __STL_NOTHROW {} // 泛化的拷貝構造函數 ~allocator() __STL_NOTHROW {} // 析構函數 pointer address(reference __x) const { return &__x; } // 返回對象的地址 const_pointer address(const_reference __x) const { return &__x; } // 返回const對象的地址 _Tp* allocate(size_type __n, const void* = 0) {  return __n != 0 ? static_cast<_Tp*>(_Alloc::allocate(__n * sizeof(_Tp))) : 0;  // 配置空間,如果申請的空間塊數不為0,那么調用 _Alloc 也即 alloc 的 allocate 函數來分配內存, } //這里的 alloc 在 SGI STL 中默認使用的是__default_alloc_template<__NODE_ALLOCATOR_THREADS, 0>這個實現(見第402行) void deallocate(pointer __p, size_type __n) { _Alloc::deallocate(__p, __n * sizeof(_Tp)); } // 釋放空間 size_type max_size() const __STL_NOTHROW // max_size() 函數,返回可成功配置的最大值  { return size_t(-1) / sizeof(_Tp); } //這里沒看懂,這里的size_t(-1)是什么意思? void construct(pointer __p, const _Tp& __val) { new(__p) _Tp(__val); } // 調用 new 來給新變量分配空間並賦值 void destroy(pointer __p) { __p->~_Tp(); } // 調用 _Tp 的析構函數來釋放空間

 

在SGI STL中設計了如下幾個空間分配的 class template:

template <int __inst> class __malloc_alloc_template // Malloc-based allocator. Typically slower than default alloc typedef __malloc_alloc_template<0> malloc_alloc template<class _Tp, class _Alloc> class simple_alloc template <class _Alloc> class debug_alloc template <bool threads, int inst> class __default_alloc_template // Default node allocator. typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc typedef __default_alloc_template<false, 0> single_client_alloc template <class _Tp>class allocator template<>class allocator<void> template <class _Tp, class _Alloc>struct __allocator template <class _Alloc>class __allocator<void, _Alloc>

 

其中 simple_alloc , debug_alloc , allocator 和 __allocator 的實現都比較簡單,都是對其他適配器的一個簡單封裝(因為實際上還是調用其他配置器的方法,如 _Alloc::allocate )。而真正內容比較充實的是 __malloc_alloc_template 和 __default_alloc_template 這兩個配置器,這兩個配置器就是 SGI STL 配置器的精華所在。其中 __malloc_alloc_template 是SGI STL 的第一層配置器,只是對系統的 malloc , realloc 函數的一個簡單封裝,並考慮到了分配失敗后的異常處理。而 __default_alloc_template 是SGI STL 的第二層配置器,在第一層配置器的基礎上還考慮了內存碎片的問題,通過內置一個輕量級的內存池。下文將先介紹第一級配置器的異常處理機制,然后介紹第二級配置器的內存池實現,及在多線程環境下內存池互斥訪問的機制。

 

2. SGI STL 內存分配失敗的異常處理

內存分配失敗一般是由於out-of-memory(oom),SGI STL 本身並不會去處理oom問題,而只是提供一個 private 的函數指針成員和一個 public 的設置該函數指針的方法,讓用戶來自定義異常處理邏輯:

private: #ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG  static void (* __malloc_alloc_oom_handler)(); // 函數指針 #endif public:  static void (* __set_malloc_handler(void (*__f)()))() // 設置函數指針的public方法  {  void (* __old)() = __malloc_alloc_oom_handler;  __malloc_alloc_oom_handler = __f;  return(__old);  }


如果用戶沒有調用該方法來設置異常處理函數,那么就不做任何異常處理,僅僅是想標准錯誤流輸出一句out of memory並退出程序(對於使用new和C++特性的情況而言,則是拋出一個 std::bad_alloc() 異常), 因為該函數指針的缺省值為0,此時對應的異常處理是 __THROW_BAD_ALLOC :

// line 152 ~ 155 #ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG template <int __inst> void (* __malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0; #endif // in _S_oom_malloc and _S_oom_realloc __my_malloc_handler = __malloc_alloc_oom_handler; if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; } // in preprocess, line 41 ~ 50 #ifndef __THROW_BAD_ALLOC # if defined(__STL_NO_BAD_ALLOC) || !defined(__STL_USE_EXCEPTIONS) # include <stdio.h> # include <stdlib.h> # define __THROW_BAD_ALLOC fprintf(stderr, "out of memory\n"); exit(1) # else /* Standard conforming out-of-memory handling */ # include <new> # define __THROW_BAD_ALLOC throw std::bad_alloc() # endif #endif

 

SGI STL 內存配置失敗的異常處理機制就是這樣子了,提供一個默認的處理方法,也留有一個用戶自定義處理異常的接口。

 

3. SGI STL 內置輕量級內存池的實現

第一級配置器 __malloc_alloc_template 僅僅只是對 malloc 的一層封裝,沒有考慮可能出現的內存碎片化問題。內存碎片化問題在大量申請小塊內存是可能非常嚴重,最終導致碎片化的空閑內存無法充分利用。SGI 於是在第二級配置器 __default_alloc_template 中 內置了一個輕量級的內存池。 對於小內存塊的申請,從內置的內存池中分配。然后維護一些空閑內存塊的鏈表(簡記為空閑鏈表,free list),小塊內存使用完后都回收到空閑鏈表中,這樣如果新來一個小內存塊申請,如果對應的空閑鏈表不為空,就可以從空閑鏈表中分配空間給用戶。具體而言SGI默認最大的小塊內存大小為128bytes,並設置了128/8=16 個free list,每個list 分別維護大小為 8, 16, 24, …, 128bytes 的空間內存塊(均為8的整數倍),如果用戶申請的空間大小不足8的倍數,則向上取整。

SGI STL內置內存池的實現請看 __default_alloc_template 中被定義為 private 的這些成員變量和方法(去掉了部分預處理代碼和互斥處理的代碼):

private: #if ! (defined(__SUNPRO_CC) || defined(__GNUC__))  enum {_ALIGN = 8}; // 對齊大小  enum {_MAX_BYTES = 128}; // 最大有內置內存池來分配的內存大小  enum {_NFREELISTS = 16}; // _MAX_BYTES/_ALIGN // 空閑鏈表個數 # endif  static size_t _S_round_up(size_t __bytes) // 不是8的倍數,向上取整  { return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1)); } __PRIVATE:  union _Obj { // 空閑鏈表的每個node的定義  union _Obj* _M_free_list_link;  char _M_client_data[1]; };  static _Obj* __STL_VOLATILE _S_free_list[]; // 空閑鏈表數組  static size_t _S_freelist_index(size_t __bytes) { // __bytes 對應的free list的index  return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1);  }  static void* _S_refill(size_t __n); // 從內存池中申請空間並構建free list,然后從free list中分配空間給用戶  static char* _S_chunk_alloc(size_t __size, int& __nobjs); // 從內存池中分配空間  static char* _S_start_free; // 內存池空閑部分的起始地址  static char* _S_end_free; // 內存池結束地址  static size_t _S_heap_size; // 內存池堆大小,主要用於配置內存池的大小


函數 _S_refill 的邏輯是,先調用 _S_chunk_alloc 從內存池中分配20塊小內存(而不是用戶申請的1塊),將這20塊中的第一塊返回給用戶,而將剩下的19塊依次鏈接,構建一個free list。這樣下次再申請同樣大小的內存就不用再從內存池中取了。有了 _S_refill ,用戶申請空間時,就不是直接從內存池中取了,而是從 free list 中取。因此 allocate 和 reallocate 在相應的free list為空時都只需直接調用 _S_refill 就行了。其中 _S_refill 和 _S_chunk_alloc 這兩個函數是該內存池機制的核心。 __default_alloc_template 對外提供的 public 的接口有 allocate , deallocate 和 reallocate 這三個,其中涉及內存分配的 allocate 和 reallocate 的邏輯思路是,首先看申請的size(已round up)對應的free list是否為空,如果為空,則調用 _S_refill 來分配,否則直接從對應的free list中分配。而 deallocate 的邏輯是直接將空間插入到相應free list的最前面。

這里默認是依次申請20塊,但如果內存池空間不足以分配20塊時,會盡量分配足夠多的塊,這些處理都在 _S_chunk_alloc 函數中。該函數的處理邏輯如下(源代碼這里就不貼了):

 

1) 能夠分配20塊

從內存池分配20塊出來,改變 _S_start_free 的值,返回分配出來的內存的起始地址

 

2) 不足以分配20塊,但至少能分配一塊

分配經量多的塊數,改變 _S_start_free 的值,返回分配出來的內存的起始地址

 

3) 一塊也分配不了

首先計算新內存池大小 size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4) 
將現在內存池中剩余空間插入到適當的free list中 
調用 malloc 來獲取一大片空間作為新的內存池: 
– 如果分配成功,則調整 _S_end_free 和 _S_heap_size 的值,並重新調用自身,從新的內存池中給用戶分配空間; – 否則,分配失敗,考慮從比當前申請的空間大的free list中分配空間,如果無法找不到這樣的非空free list,則調用第一級配置器的allocate,看oom機制能否解決問題

 

SGI STL的輕量級內存池的實現就是醬紫了,其實並不復雜。

 

4. SGI STL 內存池在多線程下的互斥訪問

最后,我們來看看SGI STL中如何處理多線程下對內存池互斥訪問的(實際上是對相應的free list進行互斥訪問,這里訪問是只需要對free list進行修改的訪問操作)。在SGI的第二級配置器中與內存池互斥訪問相關的就是 _Lock 這個類了,它僅僅只包含一個構造函數和一個析構函數,但這兩個函數足夠了。在構造函數中對內存池加鎖,在析構函數中對內存池解鎖:

//// in __default_alloc_template # ifdef __STL_THREADS  static _STL_mutex_lock _S_node_allocator_lock; // 互斥鎖變量 # endif class _Lock {  public:  _Lock() { __NODE_ALLOCATOR_LOCK; }  ~_Lock() { __NODE_ALLOCATOR_UNLOCK; } }; //// in preprocess #ifdef __STL_THREADS # include <stl_threads.h> // stl 的線程,只是對linux或windows線程的一個封裝 # define __NODE_ALLOCATOR_THREADS true # ifdef __STL_SGI_THREADS # define __NODE_ALLOCATOR_LOCK if (threads && __us_rsthread_malloc) \  { _S_node_allocator_lock._M_acquire_lock(); } // 獲取鎖 # define __NODE_ALLOCATOR_UNLOCK if (threads && __us_rsthread_malloc) \  { _S_node_allocator_lock._M_release_lock(); } // 釋放鎖 # else /* !__STL_SGI_THREADS */ # define __NODE_ALLOCATOR_LOCK \  { if (threads) _S_node_allocator_lock._M_acquire_lock(); } # define __NODE_ALLOCATOR_UNLOCK \  { if (threads) _S_node_allocator_lock._M_release_lock(); } # endif #else /* !__STL_THREADS */ # define __NODE_ALLOCATOR_LOCK # define __NODE_ALLOCATOR_UNLOCK # define __NODE_ALLOCATOR_THREADS false #endif

 

由於在 __default_alloc_template 的對外接口中,只有 allocate 和 deallocate 中直接涉及到對free list進行修改的操作,所以在這兩個函數中,在對free list進行修改之前,都要實例化一個_Lock 的對象 __lock_instance ,此時調用構造函數進行加鎖,當函數結束時,的對象 __lock_instance 自動析構,釋放鎖。這樣,在多線程下,可以保證free list的一致性。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM