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的一致性。