SGI STL空间配置器
这篇文章是对《STL源码剖析》第二章空间配置器的笔记,并没有记录地非常细致,主要记录一些点,如果想看细节读原文是最好的
为什么需要空间配置器
首先我们要理解为什么我们需要空间配置器,从一个简单的自己实现的 vector 开始
vector(int size = 10) {
  _first = new T[size];
  _last = _first;
  _end = _first + size;
}
void push_back(const T& val) {
  if (full()) expend();
  *_last++ = val;
}
在这样一个简单的 vector 当中我们使用 new T[size] 的方式去构造一个容器并且开辟他所需要的空间,然后在 push_back() 时我们使用 *_last++ = val; 给对应元素赋值。整个逻辑看起来是可行的,但是存在一定问题。
- 通过 new T[size]构造容器,这样的话,实际在构造的容器的时候就已经调用了 T 的默认构造函数,构成了 size 个 T 对象了,即使在构造的时候我们用户还不想要放入元素
- 使用 *_last++ = val;其实这里本身上是对一个默认构造好的对象再赋值 (调用赋值运算符 operator= )。而如果 T 是一个重对象 (比如 std::string, vector, 数据库连接等等),那么这个过程就会显得很 "重",引起了多余的构造和析构开销等等。
简单的空间配置器
因此我们希望能够将分配空间和构造对象这两个操作给分开,使得我们可以推迟对象的构造实际,提高效率
template <class T>
inline T* _allocate(ptrdiff_t size, T*) {
  set_new_handler(0);   // operator new 自定义分配空间失败后调用的方法,可以自定义做一些释放操作等等...
  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) {
  ::operator delete(buffer);
}
template <class T1, class T2>
inline void _construct(T1 * p, const T2& value) {
  new (p) T1(value);
  // placement new. invoke ctor of T1.
}
template <class T>
inline void _destroy(T * ptr) {
  ptr->~T();
}
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;
  // rebind allocator of type U
  template <class U>
  struct rebind {
    typedef allocator<U> other;
  };
  // hint used for locality. ref.[Austern],p189
  pointer allocate(size_type n, const void* hint = 0) {
    return _allocate((difference_type)n, (pointer)0);
  }
  void deallocate(pointer p, size_type n) { _deallocate(p); }
  void construct(pointer p, const T& value) { _construct(p, value); }
  void destroy(pointer p) { _destroy(p); }
  pointer address(reference x) { return (pointer)&x; }
  const_pointer const_address(const_reference x) { return (const_pointer)&x; }
  size_type max_size() const { return size_type(UINT_MAX / sizeof(T)); }
};
总结一下,一个 allocator 一般实现 4 种方法
- allocate 分配指定空间大小
- deallocate 释放对应空间
- construct 在指定位置上构造对象
- destroy 调用指定对象的析构方法
这里面比较要注意的点是
- 在 _allocate()中使用了::operator new((size_t)(size * sizeof(T)))方法分配空间
::operator new((size_t)(size * sizeof(T))) 是一个全局方法,可以重载,他和 new 的不同就是,实际上 new 是有两个步骤的: (1) 使用 ::operator new 分配空间, (2) 调用类型对应构造方法;而对应的 ::operator new() 他就相当于是使用 C 的 malloc() 和 free()
而 ::operator new 他还有一个 C++ new handler 机制,你可以要求系统在分配空间无法满足的情况下,在 throw std::bad_alloc 异常之前,调用一个你指定的方法,就是这里的 set_new_handler(0);
- 另外一个问题就是如何在指定的位置中构造一个对象
这个问题可以在 _construct 中使用 placement new 来解决,new (p) T(val) 这里的 p 是对应的地址空间,然后调用对象的构造方法,对应的 _destroy() 也是调用了对应方法的析构函数,而没有真正的释放空间,释放空间在 _deallocate 中通过 ::operator delete() 实现
这样的实现方法是可行的,而在 SGI STL 中他使用了一些方法提高性能
首先,为了精密分工,SGI STL allocator 将空间的配置和对象的构建与析构分为了两个部分来处理
- alloc::allocate()负责对空间的分配,释放使用- alloc::deallocate()-- #include <stl_alloc.h>
- ::conststruct()负责对象的构建,- ::destroy()负责对象的析构 -- #include <stl_construct.h>
然后配置器是定义在 <memory> 中,内部包含以上这两个头文件
SGI STL 对象的构建与析构
我们先来看在 <stl_construct.h> 中,在 SGI STL 的实现中,引入了内部实现的 value_type 和 trivaial_destructor() 来判断对应的对象有 trivial destructor,如果有,那么其实就不需要调用其析构函数,只有在没有 trivial destructor 的时候才会调用析构函数
一个类型的析构函数是 trivial 的(平凡的),当它满足以下条件:
- 该类型有一个用户未自定义的析构函数(编译器自动生成的)。
- 该析构函数是 = default,且不做任何事情。
- 所有成员变量的析构函数也都是 trivial 的。
- 没有虚函数(因为有虚函数会导致析构函数为虚的)。
- 没有基类,或者基类的析构函数也必须是 trivial 的。
简单的说就是编译器不需要为他生成额外的析构逻辑,从而带来性能上的提升。
// 以下是 destroy() 第一版本,接受一個指標。
template <class T>
inline void destroy(T* pointer) {
  pointer->~T(); // 喚起 dtor ~T()
}
// 以下是 destroy() 第二版本,接受兩個迭代器。此函式設法找出元素的數值型別,
// 進而利用 __type_traits<> 求取最適當措施。
template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last) {
  __destroy(first, last, value_type(first));
}
// 判斷元素的數值型別(value type)是否有 trivial destructor
template <class ForwardIterator, class T>
{
  inline void __destroy(ForwardIterator first, ForwardIterator last, T*) typedef
      typename __type_traits<T>::has_trivial_destructor trivial_destructor;
  __destroy_aux(first, last, trivial_destructor());
}
// 如果元素的數值型別(value type)有 non-trivial destructor…
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) {
  for (; first < last; ++first) destroy(&*first);
}
// 如果元素的數值型別(value type)有 trivial destructor…
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {}
看完了在 constructor 中利用 trivial destructor 对对象的析构优化后,我们看看在空间的分配和释放方面有那些可以提升的点
SGI STL 空间的配置和释放
实际上,SGI STL 是使用 malloc() 和 free() 来配置和释放空间的,然后他提出了一个点,就是设计了 双层配置器,主要的逻辑是这样的
第一級配置器直接使用 malloc() 和 free(),第二級配置器則視情況採用不同的策略:當配置區塊超過 128bytes,視之為「足夠大」,便呼叫第二級配置器;當配置區塊小於 128bytes,視之為「過小」,為了降低額外負擔(overhead,見 2.2.6 節)
这里实际上就是第二级配置器就相当与是开了一个内存池,里面开辟的是一个整的比较大的空间,然后对应小的空间就从这个大的空间中拿,不至于当分配太多小型区块所造成的内存碎片问题。
然后整个设计是只使用第一层配置器还是也使用第二次配置器,取决于 __USE_MALLOC 来定义
# ifdef __USE_MALLOC
...
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc; // 令 alloc 為第一級配置器
# else
...
// 令 alloc 為第二級配置器
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;
#endif /* ! __USE_MALLOC */
当中 ____malloc_alloc_template<0> 就是第一层配置器,然后 __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> 为第二层配置器
之后将其包装成为 simple_alloc 使其能够符合 STL 标准,然后是将原本 allo 使用 byte 进行分配的单位,包装成了使用对应 T 对象的大小来分配的单位。

ok, 到这我们知道了对应的大致设计,现在我们来具体看对应配置器是如何实现的,从第一层配置器开始
第一级配置器
其实这部分是很直接的,本质就是调用 malloc() 和 free() 然后就是他有自己实现的 set_malloc_handler 机制,这个机制就是和 ::operator new() 的相似,只是因为第一级配置器并不是使用 ::operator new() 所以要自己模拟
// 以㆘模擬 C++ 的 set_new_handler(). 換句話說,你可以透過它,
// 指定你自己的 out-of-memory handler
static void (* set_malloc_handler(void (*f)()))()
{
  void (* old)() = __malloc_alloc_oom_handler;
  __malloc_alloc_oom_handler = f;
  return(old);
};
然后整体逻辑就是 allocate() 如果说分配失败,那么他会调用 oom_allocate() 方法,这个方法就是不断循环分配,然后如果说分配失败就调用用户传来的 __malloc_alloc_oom_handler 然后再次尝试,然后其他的代码就不贴了,可以在书中找到详细的代码
第二级配置器
第二级配置器就是重点的部分,在他内部,就是会有 16 个 free list,每一个 list 保存着对应大小的一些区块,并且每一个区块的大小,为了方便管理,都是会将其上调到 8 的倍数(例如客户端要求 30 字节,他会给其 32 字节的区块)。
enum { __ALIGN = 8 };
// ROUND_UP() 將 bytes ㆖調至 8 的倍數。
static size_t ROUND_UP(size_t bytes) { return (((bytes) + __ALIGN - 1) & ~(__ALIGN - 1)); }
free_list 的结构与空间的分配和回收
对应的 16 个 free lists, 各自管理大小分别为 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 bytes 的小区块

整个结构差不多是这样的,static obj * volatile free_list[__NFREELISTS]; 就比如说 free_list[0] 指向的就是一堆空间大小是 8 bytes 的区块。
然后看一下 free_list 每个节点的结构 obj *
union obj {
  union obj * free_list_link;
  char client_data[1]; /* The client sees this. */
};
这里就是利用了 union 的技巧,一个 obj 可以有两个身份,一个是 free_list_link 用来保存下一个区块的地址,又或者是用户存储的数据,当是用户存储的数据的时候,他对应维护的指针就断了,因此也就不在对应的 free_list 当中了。总结一下就是说 free_list[i] 是以一种链表的形式组织的

对于取一个空间就是从
- 用户需要的 size n 扩大到 8 的倍数,通过 FREELIST_INDE(n)找到对应大小的区块是在那个 free_list 中存
- 更新 free_list[i]执行第一个区块的 next,然后返回第一个区块,这就是分配的空间
// n must be > 0
static void* allocate(size_t n) {
  obj* volatile* my_free_list;
  obj* result;
  // 大於 128 就呼叫第一級配置器
  if (n > (size_t)__MAX_BYTES) {
    return (malloc_alloc::allocate(n));
  }
  // 尋找 16 個 free lists ㆗適當的㆒個
  my_free_list = free_list + FREELIST_INDEX(n);
  result = *my_free_list;
  if (result == 0) {
    // 沒找到可用的 free list,準備重新填充 free list
    void* r = refill(ROUND_UP(n));
    return r;
  }
  // 調整 free list
  *my_free_list = result->free_list_link;
  return (result);
};
注意,这里是 static 毕竟就是要使用 allocate 来分配空间... 如果使用 allocate 都需要一个对象 .... 就有问题了
对应的 FREELIST_INDE() 方法
static size_t FREELIST_INDEX(size_t bytes) { return (((bytes) + __ALIGN - 1) / __ALIGN - 1); }
同样的,如果释放一个空间也是,使用头插法的方式返回区块
重新填充 free_list, refill()
从前面的我们可以看见说,当不断 allocate 的情况下, free_list 是会没有空间的,这个时候 free_list[i] = 0, 然后我们会使用 refill() 给对应 free_list 添加空间,新的空间是从记忆池中 chunk_alloc() 获取,预计获得 20 个新区块,但是如果记忆池中空间不足,可能区块数小于 20
// 傳回一个個大小為 n 的物件,並且有時候會為適當的 free list 增加節點.
// 假設 n 已經適當㆖調至 8 的倍數。
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n) {
  int nobjs = 20;
  // 呼叫 chunk_alloc(),嘗試取得 nobjs 個區塊做為 free list 的新節點。
  // 注意參數 nobjs 是 pass by reference。
  char* chunk = chunk_alloc(n, nobjs);  
  obj* volatile* my_free_list;
  obj* result;
  obj *current_obj, *next_obj;
  int i;
  // 如果只獲得一個區塊,這個區塊就撥給呼叫者用,free list 無新節點。
  if (1 == nobjs) return (chunk);
  // 否則準備調整 free list,納入新節點。
  my_free_list = free_list + FREELIST_INDEX(n);
  // 以下在 chunk 空間內建立 free list
  result = (obj*)chunk;
  // 這一塊準備傳回給客端
  // 以下導引 free list 指向新配置的空間(取自記憶池)
  *my_free_list = next_obj = (obj*)(chunk + n);
  
  // 以下將 free list 的各節點串接起來。
  for (i = 1;; i++) {
    // 從 1 開始,因為第 0 個將傳回給客端
    current_obj = next_obj;
    next_obj = (obj*)((char*)next_obj + n);
    if (nobjs - 1 == i) {
      current_obj->free_list_link = 0;
      break;
    } else {
      current_obj->free_list_link = next_obj;
    }
  }
  return (result);
}
这个逻辑是很清晰的,主要注意的点是:首先 char* chunk = chunk_alloc(n, nobjs);  这个 chunk 是 char* 的类型,所以 *my_free_list = next_obj = (obj*)(chunk + n);  这个部分中 (chunk + n) 就是说移动了 n 个字节(char是一字节),然后再转换成为 (obj*) 作为对应 free_list 指向的第一个区块
同理,在将给的 chunk 然后把哥哥节点串起来的过程其实也是 next_obj = (obj*)((char*)next_obj + n); 要先把 next_obj 转换成为 char* 再 + n 使得划分出新的大小为 n 的区块,然后连接起来
记忆池
然后记忆池部分,就是,free_list 的空间是从记忆池中来的,而记忆池的空间是从 malloc 中申请大的空间来的
// 2.2.10 記憶池(memory pool)
// 假設 size 已經適當上調至 8 的倍數。
// 注意參數 nobjs 是 pass by reference。
template <bool threads, int inst>
char* __default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs) {
  char* result;
  size_t total_bytes = size * nobjs;
  size_t bytes_left = end_free - start_free;  // 記憶池剩餘空間
  if (bytes_left >= total_bytes) {
    // 記憶池剩餘空間完全滿足需求量。
    result = start_free;
    start_free += total_bytes;
    return (result);
  } 
  
  else if (bytes_left >= size) {
    // 記憶池剩餘空間不能完全滿足需求量,但足夠供應一个個(含)以上的區塊。
    nobjs = bytes_left / size;
    total_bytes = size * nobjs;
    result = start_free;
    start_free += total_bytes;
    return (result);
  } 
  
  else {
    // 記憶池剩餘空間連一个個區塊的大小都無法提供。
    size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
    
    // 以下試著讓記憶池中的殘餘零頭還有利用價值。
    if (bytes_left > 0) {
      // 記憶池內還有一些零頭,先配給適當的 free list。
      // 首先尋找適當的 free list。
      obj* volatile* my_free_list = free_list + FREELIST_INDEX(bytes_left);
      // 調整 free list,將記憶池㆗的殘餘空間編入。
      ((obj*)start_free)->free_list_link = *my_free_list;
      *my_free_list = (obj*)start_free;
    }
    // 配置 heap 空間,用來挹注記憶池。
    start_free = (char*)malloc(bytes_to_get);
    if (0 == start_free) {
      // heap 空間不足,malloc() 失敗。
      int i;
      obj *volatile *my_free_list, *p;
      // 試著檢視我們手㆖擁有的東西。這不會造成傷害。我們不打算嘗試配置
      // 較小的區塊,因為那在多行程(multi-process)機器㆖容易導致災難
      // 以㆘搜尋適當的 free list,
      // 所謂適當是指「尚有未用區塊,且區塊夠大」之 free list。
      // 所以这里检查的 i 是从 size 开始查看的
      for (i = size; i <= __MAX_BYTES; i += __ALIGN) {
        my_free_list = free_list + FREELIST_INDEX(i);
        p = *my_free_list;
        if (0 != p) {  // free list 內尚有未用區塊,且足够大
          // 調整 free list 以釋出未用區塊到 chunk 中
          *my_free_list = p->free_list_link;
          start_free = (char*)p;
          end_free = start_free + i;
          // 遞迴呼叫自己,為了修正 nobjs。
          return (chunk_alloc(size, nobjs));
          // 注意,任何殘餘零頭終將被編入適當的 free-list ㆗備用。
        }
      }
      end_free = 0;  // 如果出現意外(山窮水盡,到處都沒記憶體可用了)
      // 呼叫第㆒級配置器,看看 out-of-memory 機制能否盡點力
      start_free = (char*)malloc_alloc::allocate(bytes_to_get);
      // 這會導致擲出異常(exception),或記憶體不足的情況獲得改善。
    }
    heap_size += bytes_to_get;
    end_free = start_free + bytes_to_get;
    // 遞迴呼叫自己,為了修正 nobjs。
    return (chunk_alloc(size, nobjs));
  }
}
上述的 chunk_alloc() 函式以 end_free - start_free 來判斷記憶池的水量。
如果水量充足,就直接撥出 20 個區塊傳回給 free list。如果水量不足以提供 20 個區塊,但還足夠供應一個以上的區塊,就撥出這不足 20 個區塊的空間出去。這時候其 pass by reference 的 nobjs 參數將被修改為實際能夠供應的區塊數。如果記憶池連一个個區塊空間都無法供應,對客端顯然無法交待,此時便需利用 malloc() 從 heap ㆗配置記憶體,為記憶池注入活水源頭以應付需求。
新水量的大小為需求量的兩倍,再加上一個隨著配置次數增加而愈來愈大的附加量。舉個例子,假設程式一開始,客端就呼叫 chunk_alloc(32,20),於是malloc() 配置 40 個 32bytes 區塊,其中第 1 個交出,另 19 個交給 free_list[3]維護,餘 20 個留給記憶池。
接㆘來客端呼叫 chunk_alloc(64,20),此時 free_list[7] 空空如也,必須向記憶池要求支援。記憶池只夠供應 (32*20)/64=10 個 64bytes 區塊,就把這 10 個區塊傳回,第 1 個交給客端,餘 9 個由 free_list[7] 維護。此時記憶池全空。
接下來再呼叫 chunk_alloc(96, 20),此時 free_list[11] 空空如也,必須向記憶池要求支援,而記憶池此時也是空的,於是以 malloc() 配置 40+n(附加量)個 96bytes 區塊,其中第 1 個交出,另 19 個交給 free_list[11] 維護,餘 20+n(附加量)個區塊留給記憶池……。
萬一山窮水盡,整個 system heap 空間都不夠了(以至無法為記憶池注入活水源頭),malloc() 行動失敗,chunk_alloc() 就会處尋找有無「尚有未用區塊,且區塊夠大」之 free lists。找到的話就挖一塊交出,找不到的話就呼叫第一級配置器。
第一級配置器其實也是使用 malloc() 來配置記憶體,但它有 out-of-memory處理機制(類似 new-handler 機制),或許有機會釋放其他的記憶體拿來此處使用。如果可以,就成功,否則發出 bad_alloc 異常
所以实际上,整个第二层配置器,本事还是在一个由 memory pool 得到的一个大的整块的内存中,以指针的方式实现了灵活的逻辑关系

 
                     
                    
                 
                    
                 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号