1. 程式人生 > >C++ 空間配置器(allocator)

C++ 空間配置器(allocator)

name 碎片 inux set 特性 改變 ptr comm 二級

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 中取。因此 allocatereallocate 在相應的free list為空時都只需直接調用 _S_refill 就行了。其中 _S_refill_S_chunk_alloc 這兩個函數是該內存池機制的核心。 __default_alloc_template 對外提供的 public 的接口有 allocate , deallocatereallocate 這三個,其中涉及內存分配的 allocatereallocate 的邏輯思路是,首先看申請的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 的對外接口中,只有 allocatedeallocate 中直接涉及到對free list進行修改的操作,所以在這兩個函數中,在對free list進行修改之前,都要實例化一個_Lock 的對象 __lock_instance ,此時調用構造函數進行加鎖,當函數結束時,的對象 __lock_instance 自動析構,釋放鎖。這樣,在多線程下,可以保證free list的一致性。

C++ 空間配置器(allocator)