2.1 空间配置器的标准接口

根据 STL 规范,以下是 allocator 的标准接口:

  1. allocator::value_type
  2. allocator::pointer
  3. allocator::const_pointer
  4. allocator::reference
  5. allocator::const_reference
  6. allocator::size_type
  7. allocator::difference_type
  8. allocator::rebind //一个嵌套的模板类,class rebind<U>拥有唯一成员other
  9. //是一个typedef,代表allocator<U>
  10. allocator::allocator()
  11. allocator::allocator(const allocator&)
  12. template<class U>allocator::cllocator<const allocator<U>&) //泛化的拷贝构造
  13. allocator::~allocator()
  14. pointer allocator::address(reference x)const //返回某个对象的地址
  15. const_pointer allocator::address(const_reference x)const //返回某个const对象的地址
  16. //配置空间,足以存储n个T对象,第二个参数是个提示。
  17. pointer allocator::allocate(size_type n, const void* = 0)
  18. void allocator::deallocate(pointer p, size_type n)
  19. size_type allocator::max_size()const //返回可配置的最大容量
  20. //等同于 new((void*)p) T(x)
  21. void allocator::construct(pointer p, const T& x)
  22. //等同于p->~T()
  23. void allocator::destroy(pointer p)

2.2 具备次配置力的 SGI 空间配置器

SGI STL 的配置器与众不同也与规范不同,其名称是alloc而非allocator,而且不接受任何参数:

  1. vector<int, std::allocator<int>>iv; //in VC or CB
  2. vector<int, std::alloc>iv; //in GCC

因为 SGI STL 在声明时已经为每个容器都指定了其默认的空间配置器为alloc,e.g.

  1. template<class T, class Alloc = alloc>
  2. class vector{};

2.2.2 SGI 空间配置器 std::alloc

allocator只是基层内存配置/释放行为,即::operator new::operator delete的一层薄薄的封装,效率不佳。SGI 采用的实际上是std::alloc

  • 内存配置alloc::allocate()负责,
  • 内存释放alloc::deallocate()负责,
  • 对象构造::construct()负责,
  • 对象析构::destroy()负责。

image.png

2.2.3 构造和析构基本工具

image.png

  1. #include <new.h> //欲使用placement new,需先包含此文件
  2. template cclass T1,class T2>
  3. inline void construct ( T1* p, const T2& value) {
  4. new (p)T1(value); //placement new;调用Tl::T1(value) ;
  5. }
  6. //以下是destroy ())第一版本,接受一个指针
  7. template <class T>
  8. inline void destroy (T* pointer) {
  9. pointer->~T ();//调用dtor ~T()
  10. }
  11. //以下是 destroy ()第二版本,接受两个迭代器。
  12. //此函数设法找出元素的数值型别,进而利用_type_traits<>求取最适当措施
  13. template <class ForwardIterator>
  14. inline void destroy (Forwarditerator firstForwardIterator last){
  15. __destroy (first, last, value_type(first));
  16. }
  17. //判断元素的数值型别(value_type)是否有trivial destructor
  18. template <class ForwardIterator, class T>
  19. inline void__destroy (ForwardIterator firstForwardIterator lastT*){
  20. typedef typename __type_traits<T> :: has_trivial_destructor trivial_destructor;
  21. __destroy_aux ( first, last, trivial_destructor());
  22. }
  23. //如果元素的数值型别( value type)有non-trivial destructor
  24. template <class ForwardIterator>
  25. inline void
  26. __destroy_aux (Forwardlterator firstForwardIterator last_false_type){
  27. for (; first < last ; ++first)
  28. destroy(&*first);
  29. }
  30. //如果元素的数值型别( value type)有trivial destructor
  31. template <class ForwardIterator>
  32. inline void _destroy_aux (ForwardIteratorForwardIterator_true_type){}
  33. //以下是destroy ( )第二版本针对迭代器为char*和wchar_t*的特化版
  34. inline void destroy(char* , char* ) { }
  35. inline void destroy ( wchar_t* , wchar_t* ) { }

construct()接受一个指针 p 和一个初值 value,该函数的用途就是将初值设定到指针所指的空间上,C++的placement new运算子可以完成这一任务。

destroy()有两个版本:

  • 第一版本:接受一个指针,将该指针所指之物析构掉,直接调用析构函数即可;
  • 第二版本:接受firstlast两个迭代器,准备将[first, last)范围内的所有对象析构掉。我们不知道该范围有多大,如果范围很大而每个对象的析构函数都是 trivial 的,那么调用这些 trivial 的构造函数效率很低。所以要先利用value_type()获得迭代器所指对象的型别,再利用__type_traits<T>判断该型别的析构函数是否 trivial:
    • 是 trivial 的(__true_type),则什么也不做,空函数体;
    • 不是 trivial 的(__false_type),以循环方式遍历整个范围并对每个对象调用第一个版本的destroy()

2.2.4 空间的配置与释放,std::alloc

C++的内存配置和释放基本操作是::operator new::operator delete,相当于 C 的malloc()free(),所以 SGI 以**malloc()****free()**完成内存的配置和释放

考虑到小型区块可能造成内存碎片问题,SGI 设计了双层配置器

  • 第一级配置器直接使用**malloc()****free()**
  • 第二级配置器视情况采用不同策略:
    • 配置区块超过 128 bytes 时,视为足够大,便调用第一级配置器;
    • 配置区块小于 128 bytes 时,视为过小,为了降低额外负担,采用复杂的 memory pool 整理方式。

image.png
整个配置器系统设计究竟只开放第一级配置器,或是同时开房第二级配置器,取决于__USE_MALLOC是否被定义。
无论alloc被定义为第一级或第二级配置器,SGI 还为它包装了一个接口如下:

  1. template<class T, class Alloc>
  2. class simple_alloc{
  3. public:
  4. static T *allocate (size_t n){
  5. return 0 == n? 0 : (T*) Alloc::allocate(n * sizeof (T));
  6. }
  7. static T *a1locate (void){
  8. return (T*) Alloc::allocate(sizeof(T));
  9. }
  10. static void deallocate (T *p, size_t n){
  11. if 0 != n)
  12. Alloc::deallocate(p, n*sizeof(T));
  13. }
  14. static void dea11ocate (T *p){
  15. Alloc : :deallocate(p, sizeof(T));
  16. }
  17. };

其内部四个成员其实都是单纯的转调用,调用传递给配置器(可能是一级或二级)的成员函数。该接口使得配置器的配置单位从 bytes 转为个别元素的大小**sizeof(T)**。SGI STL 容器全部采用这个simple_alloc接口:
image.png

2.2.5 第一级配置器 __malloc_alloc_template

  1. template<int inst>
  2. class _malloc_alloc_template
  3. {
  4. /* oom_alloc为静态函数成员,用于处理malloc时的内存不足问题
  5. oom_realloc为静态函数成员,用于处理realloc时的内存不足问题
  6. _malloc_alloc_handler为静态数据成员,为void(*)()类型的函数指针,用于用户自
  7. 己制定内存分配策略
  8. oom = out-of-memory
  9. */
  10. static void * oom_malloc(size_t);//out_of_memmory malloc
  11. static void * oom_realloc(void *, size_t);
  12. static void(*_malloc_alloc_oom_handler)();
  13. public:
  14. static void * allocate(size_t n)
  15. {
  16. void * result = malloc(n);//请求内存
  17. if (result == nullptr)//如果内存不足
  18. result=oom_malloc(n);//调用oom_malloc
  19. return result;
  20. }
  21. static void * reallocate(void * p, size_t n)
  22. {
  23. void *result = realloc(n);
  24. if (result == nullptr)
  25. result = oom_realloc(p, n);
  26. return result;
  27. }
  28. static void deallocate(void * p)
  29. {
  30. //使用free函数释放p地址后所分配的内存块
  31. free(p);
  32. }
  33. /*此静态成员函数接受一个void(*)()类型的函数指针作为参数,返回
  34. void(*)()类型的函数指针。其作用为用用户自己定制的内存调度方法替换
  35. _malloc_alloc_handler,由此实现类似C++的set_new_handler方法。
  36. */
  37. static void(* set_malloc_handler(void(*f)()))()
  38. {
  39. void(*old)() = _malloc_alloc_oom_handler;
  40. _malloc_alloc_oom_handler = f;
  41. return old;
  42. }
  43. };
  44. template<int inst>
  45. void(*_malloc_alloc_template<inst>::_malloc_alloc_oom_handler)() = 0;
  46. template<int inst>
  47. void * _malloc_alloc_template<inst>::oom_malloc(size_t n)
  48. {
  49. void(*my_oom_handler)();
  50. void * result;
  51. //无限循环,直至成功分配内存或用户没有定制内存分配策略
  52. for (;;)
  53. {
  54. my_oom_handler = _malloc_alloc_oom_handler;
  55. if (my_oom_handler == nullptr)//如果用户没有定制内存分配策略
  56. exit(1);
  57. (*my_oom_handler)();//使用用户定制的方法
  58. result = malloc(n);
  59. if (result)
  60. return result;
  61. }
  62. }
  63. template<int inst>
  64. void * _malloc_alloc_template<inst>::oom_realloc(void * p, size_t n)
  65. {
  66. //此函数的设计思路与oom_malloc如出一辙
  67. void(*my_oom_handler)();
  68. void * result;
  69. for (;;)
  70. {
  71. my_oom_handler = _malloc_alloc_oom_handler;
  72. if (my_oom_handler == nullptr)
  73. exit(1);
  74. (*my_oom_handler)();
  75. result = realloc(p,n);
  76. if (result)
  77. return result;
  78. }
  79. }

SGI 以**malloc()**而非**::operator new**来配置内存,因为 C++ 并未提供相应于**realloc()**的内存配置操作。因此 SGI 不能直接使用 C++ 的**set_new_handler()**在内存配置需求无法被满足的时候调用处理函数,而只能通过函数指针模拟出这一行为

HINT:SGI 的第一级配置器的allocate()realloc()都是在调用malloc()realloc()不成功后,改调用**oom_malloc()****oom_realloc()**。后两者内部都有循环,不断调用内存不足处理例程,期望在某次调用之后获得足够的内存完成任务。但是如果客户没有设定内存不足处理例程,这二者就会调用**__THROW_BAD_ALLOC**,丢出**bad_alloc**异常信息或利用**exit(1)**终止程序

2.2.6 第二级配置器 __default_alloc_template

第二级配置器增加了一些机制,避免太多小额区块造成内存碎片。配置空间时会有一个额外区块用于管理配置出来的内存,这是无法避免的,但是配置的区块越小,额外区块占比就越大,越显得浪费:
image.png
SGI 第二级配置器的做法是:

  • 如果区块超过 128 bytes,就移交第一级配置器处理;
  • 如果区块小于 128 bytes时,则以内存池(memory pool)管理,又称次级配置每次配置一大块内存并维护对应的 free-list。下次若再有相同大小的内存需求,直接从 free-lists 中拨出。如果释放了小额区块,由配置器回收到 free-lists 中。SGI 第二级配置器会自动将任何小额区块的内心需求量调整为 8 的倍数,并维护 16 个 free-lists,各自管理大小分别为8,16,24, 32, 40, 48, 56, 64, 75, 80, 88, 96, 104, 112, 120,128 bytes 的小额区块。

    free-lists 的结构如下:

    1. //一物两用,保存下一个free_list的指针,也保存实际区块的内容
    2. union obj
    3. {
    4. union obj * free_list_link;
    5. char client_data[1];
    6. }

    image.png
    这种实现原理有独特的艺术:由于 free-lists 中维护的是空闲节点,所以一旦被使用(不再空闲)就是以第二行的形式出现,当是空闲的时候就以第一行的形式出现作为链表中的节点(指针)。所以同一时刻只能是二者之一。 ```cpp enum{ALIGN = 8}; //小型区块的上调边界 enum{MAX_BYTES = 128}; //小型区块的上限 enum{NFREELISTS = MAX_BYTES/__ALIGN}; //free-lists 个数

//以下是第二级配置器 //notice:没有”template”类型参数,第二个参数没有派上用处 //第一个参数用于多线程环境下,目前不讨论 template class default_alloc_template { private: //ROUND_UP()将bytes提升至8的倍数 static size_t ROUND_UP(size_t bytes) { return ((bytes + ALIGN - 1) & ~(ALIGN - 1)); } private: union obj //free_lists节点构造 { union obj free_list_link; char client_data[1]; }; private: //16个free_lists static obj volatile free_list[NFREELISTS]; //根据区块大小,决定使用第n号free-list, n从1算 static size_t FREELIST_INDEX(size_t bytes) { return (((bytes) + ALIGN - 1) / ALIGN - 1); } //返回一个大小为n的对象,并可能加入大小为n的其他区块到free list static void refill(size_t n); //配置一大块空间,可容纳nobjs个大小为”size”的区块 //如果配置nobjs个区块有所不便,nobjs可能会降低 static char chunk_alloc(size_t size, int& nobjs);

  1. // chunk allocation state
  2. static char *start_free; //内存池起始位置
  3. static char *end_free; //内存池结束位置
  4. static size_t heap_size;

public: static void allocate(size_t n); static void deallocate(void p, size_t n); static void reallocate(void p, size_t old_sz, size_t new_sz); }; template char *__default_alloc_template::start_free = 0;

template char *__default_alloc_template::end_free = 0;

template size_t __default_alloc_template::heap_size = 0;

template typename default_alloc_template::obj * volatile default_alloc_template::free_list[__NFREELISTS] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

  1. <a name="T9gqI"></a>
  2. ## 2.2.7 空间配置函数 allocate()
  3. 作为一个配置器,`__default_alloc_template`拥有配置器的标准接口函数`allocate()`。此函数首先判断区块大小,大于 128 bytes 就调用第一级配置器,小于 128 bytes 就检查对应的 free-lists。若有可用区块就拿来用,若没有则将区块大小上调至 8 倍数边界,然后调用`refill()`,准备为 free-lists重新填充空间。
  4. ```cpp
  5. static void* allocate(size_t size){
  6. // 如果大于128bytes,交给第一级配置器
  7. if (size > (size_t)__MAX_BYTES){
  8. return malloc_alloc::allocate(size);
  9. }
  10. obj* result = nullptr;
  11. obj* volatile* my_free_list = nullptr;
  12. //寻找 16 个 free-lists 中合适的一个
  13. my_free_list = free_list + FREELIST_INDEX(size);
  14. result = *my_free_list;
  15. if (nullptr == result){
  16. //没有找到合适的,准备重新填充 free-list
  17. void* r = refill(ROUND_UP(size));
  18. return r;
  19. }
  20. // 调整 free-list
  21. *my_free_list = result->free_list_link;
  22. return result;
  23. }

image.png

2.2.8 空间释放函数 deallocate()

作为一个配置器,__default_alloc_template拥有配置器的标准接口函数deallocate()。首先判断区块大小,大于 128 bytes 就调用第一级配置器,小于 128 bytes 就找出对应的 free-lists,将区块回收

  1. static void deallocate(void* p, size_t n){
  2. if (n > (size_t)__MAX_BYTES){
  3. return malloc_alloc::deallocate(p, n);
  4. }
  5. obj* q = (obj*)p;
  6. obj* volatile* my_free_list = nullptr;
  7. // 寻找相应的free_list
  8. my_free_list = free_list + FREELIST_INDEX(n);
  9. // 头插法,回收区块
  10. q->free_list_link = *my_free_list;
  11. *my_free_list = q;
  12. }

image.png

2.2.9 重新填充 free lists

allocate()发现 free-lists 中没有可用区块时,就调用**refill()**,为 free-list 重新填充空间,新的空间将取自内存池(经过**chunk_alloc()**完成)默认获得 20 个新区块,如果内存池空间不足也可能小于 20。

  1. template<bool threads, int inst>
  2. static void* __default_alloc_template<thread, inst>::refill(size_t n){
  3. int nobjs = 20;
  4. //调用chunk_alloc(),尝试获得nobjs个区块作为free-list的新节点
  5. //注意参数是引用传递
  6. char* chunk = chunk_alloc(n, nobjs);
  7. obj* volatile* my_free_list = nullptr;
  8. obj* result = (obj*)chunk;
  9. obj* current_obj = nullptr;
  10. obj* next_obj = nullptr;
  11. int i = 0;
  12. //如果只获得一个区块,就分配给调用者使用,free-list无新增节点
  13. if (1 == nobjs) return chunk;
  14. //得到不止一个区块,准备调整free-list,纳入新节点
  15. my_free_list = free_list + FREELIST_INDEX(n);
  16. //分配出来的第一个区块要给客户使用,所以返回,然后加上每个区块大小的偏移量,就指向下一个区块了
  17. result = (obj*)chunk;
  18. *my_free_list = next_obj = (obj*)(chunk + n);
  19. for (i = 1; ; ++i){
  20. current_obj = next_obj;
  21. next_obj = (obj*)((char*)next_obj + n);
  22. if (i == nobjs - 1)
  23. {
  24. current_obj->free_list_link = nullptr;
  25. break;
  26. }
  27. current_obj->free_list_link = next_obj;
  28. }
  29. return (result);
  30. }

2.2.10 内存池(memory pool)

chunk_alloc()负责从内存池中取空间给 free-list 使用:

  1. // 配置一大块空间,可以容纳nobjs个size大小的区块
  2. // 如果配置nobjs个区块有所不便,nobjs会做出相应改变
  3. static char* chunk_alloc(size_t size, int& nobjs){
  4. char* result = nullptr;
  5. // 需要空间的总大小
  6. size_t total_bytes = size * nobjs;
  7. // 内存池剩余空间
  8. size_t bytes_left = end_free - start_free;
  9. // 如果内存池剩余空间满足需要空间
  10. if (bytes_left >= total_bytes){
  11. result = start_free;
  12. start_free = start_free + total_bytes;
  13. return (result);
  14. }else if (bytes_left >= size){
  15. // 如果剩余空间只够1个以上的块(但小于总需求)
  16. //就修正nobjs(得益于传引用)只返回当前能提供的区块数
  17. nobjs = bytes_left / size;
  18. result = start_free;
  19. start_free = start_free + total_bytes;
  20. return result;
  21. }else{
  22. // 如果连一块空间都不够,先声明一个将要向堆区申请的变量
  23. size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
  24. // 尝试将剩下的一点点空间再利用
  25. if (bytes_left > 0){
  26. // 将剩余空间配置给合适的free_list
  27. // 声明了一个指针,指向了一个 volatile 指针,该指针指向 obj
  28. obj* volatile* my_free_list = free_list + FREELIST_INDEX(bytes_left);
  29. ((obj*)start_free)->free_list_link = *my_free_list;
  30. *my_free_list = (obj*)start_free;
  31. }
  32. // 配置heap空间,补充内存池
  33. start_free = (char*)malloc(bytes_to_get);
  34. if (nullptr == start_free){
  35. //堆内存无法满足分配,检查free-lists中有没有空闲节点,将它释放并用作分配
  36. obj* volatile* my_free_list = nullptr;
  37. obj* p = nullptr;
  38. for (int i = size; i <= __MAX_BYTES; i += __ALIGN){
  39. my_free_list = free_list + FREELIST_INDEX(i);
  40. p = *my_free_list;
  41. if (nullptr != p){
  42. *my_free_list = p->free_list_link;
  43. start_free = (char*)p;
  44. end_free = start_free + i;
  45. //递归调用自己,以修正nobjs
  46. return chunk_alloc(size, nobjs);
  47. }
  48. }
  49. end_free = 0;//到处都没有内存可用了,调用第一级配置器看oom是否有用
  50. start_free = (char*)malloc_alloc::allocate(bytes_to_get);
  51. }
  52. // 修正内存池的结束位置和总大小
  53. end_free = start_free + bytes_to_get;
  54. heap_size += bytes_to_get;
  55. // 递归调用自己,修正nobjs
  56. return chunk_alloc(size, nobjs);
  57. }
  58. }

e.g. 见图,假设程序一开始,客端就调用chunk_alloc(32,20),于是malloc()配置 40 个 32 bytes区块,其中第 1 个交出,另 19 个交给free_list[3]维护,余 20 个留给内存池。接下来客端调用chunk_alloc(64,20),此时free_list [7]空空如也,必须向内存池要求支持。内存池只够供应(32*20)/64=10 个 64 bytes 区块,就把这 10 个区块返回,第 1 个交给客端,余9个由free_list [7]维护。此时内存池全空。接下来再调用chunk_alloc( 96,20)此时**free_list[11]**空空如也,必须向内存池要求支持,而内存池此时也是空的,于是以malloc()配置 40+n(附加量)个96 bytes区块,其中第 1 个交出,另 19 个交给free_list[11]维护,余 20+n(附加量)个区块留给内存池
image.png

2.3 内存基本处理工具

STL 有五个全局函数作用于未初始化空间上

  • construct()
  • destroy()
  • uninitialized_copy()——copy()
  • uninitialized_fill()——fill()
  • uninitialized_fill_n()——fill_n()

  • ```cpp template ForwardIterator uninitialized_copy(InputIterator first,
    1. InputIterator last, ForwardIterator result);

template ForwardIterator uninitialized_fill(ForwardIterator first, ForwardIterator last, const T& x);

template ForwardIterator uninitialized_fill_n(InputIterator first, Size n, const T& x);

  1. 三者都可以将**内存配置**与**对象构造行为**分离开。
  2. <a name="Bbzod"></a>
  3. ## 2.3.1 uninitialized_fill_n 实现
  4. ```cpp
  5. template<class ForwardIterator, class Size, class T>
  6. ForwardIterator uninitialized_fill_n(InputIterator first, Size n, const T& x){
  7. return __uninitialized_fill_n(first, n, x, value_type(first));
  8. }

首先萃取处迭代器first的value type,然后判断该型别是否是 POD 型别。
POD 型别:Plain Old Data,即标量型别或传统 C struct 型别。
POD 型别必然拥有trivial ctor/dtor/copy/assignment 函数。因此可以针对 POD 型别采用最有效率的初值填写手法,而且 non-POD 型别采取最保险安全的做法:

  • 针对 POD 型别:调用高阶函数fill_n()
  • 针对非 POD 型别:使用循环针对每个变量调用construct()->placement new~

2.3.2 uninitialized_copy 实现

大致与uninitialized_fill_n()相同,只不过在value_type为 POD 型别时,调用的是copy()函数。
而且针对**char*****wchar_t**,可以采用最具效率的**memmove()**直接移动内存内容来执行复制行为,所以针对这两种类型有特化版本。

2.3.3 uninitialized_fill 实现

value_type为 POD 型别时,调用的是fill()函数。


image.png
HINT:注意uninitialized_fill()的两个特化版本中用的是memmove()而不是memcpy()。这两者的功效是一样的,区别在于memcpy()的第二个参数有restrict限定词(意即memcpy()假定两块内存区域没有重叠),导致memcpy()在两个参数有重叠部分时结果不可预知,而memmove()可以保证完成任务。