STL源码分析--空间配置器 第一级配置器

一、SGI STL配置器简介

SGI STL的配置器与众不同,它与标准规范不同。如果要在程序中明确使用SGI配置器,那么应该这样写:

[cpp] view plaincopyprint?

  1. vector<int,std::alloc> iv;

他的名字是alloc,而且不接受任何参数。标准配置器的名字是allocator,而且可以接受参数。

SGI STL的每一个容器都已经指定了缺省配置器:alloc。我们很少需要自己去指定空间配置器。比如vector容器的声明:

[cpp] view plaincopyprint?

  1. template <class T, class Alloc = alloc>  // 预设使用alloc为配置器
  2. class vector {
  3. //...
  4. }

二、SGI标准的空间配置器

其实SGI也定义了一个符合部分标准,名为allocator的配置器,但是它自己不使用,也不建议我们使用,主要原因是效率不佳。它只是把C++的操作符::operator new和::operator delete做了一层简单的封装而已。下面仅仅贴出代码:

[cpp] view plaincopyprint?

  1. #ifndef DEFALLOC_H
  2. #define DEFALLOC_H
  3. #include <new.h>
  4. #include <stddef.h>
  5. #include <stdlib.h>
  6. #include <limits.h>
  7. #include <iostream.h>
  8. #include <algobase.h>
  9. template <class T>
  10. inline T* allocate(ptrdiff_t size, T*) {
  11. set_new_handler(0);
  12. T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
  13. if (tmp == 0) {
  14. cerr << "out of memory" << endl;
  15. exit(1);
  16. }
  17. return tmp;
  18. }
  19. template <class T>
  20. inline void deallocate(T* buffer) {
  21. ::operator delete(buffer);
  22. }
  23. template <class T>
  24. class allocator {
  25. public:
  26. typedef T value_type;
  27. typedef T* pointer;
  28. typedef const T* const_pointer;
  29. typedef T& reference;
  30. typedef const T& const_reference;
  31. typedef size_t size_type;
  32. typedef ptrdiff_t difference_type;
  33. pointer allocate(size_type n) {
  34. return ::allocate((difference_type)n, (pointer)0);
  35. }
  36. void deallocate(pointer p) { ::deallocate(p); }
  37. pointer address(reference x) { return (pointer)&x; }
  38. const_pointer const_address(const_reference x) {
  39. return (const_pointer)&x;
  40. }
  41. size_type init_page_size() {
  42. return max(size_type(1), size_type(4096/sizeof(T)));
  43. }
  44. size_type max_size() const {
  45. return max(size_type(1), size_type(UINT_MAX/sizeof(T)));
  46. }
  47. };
  48. class allocator<void> {
  49. public:
  50. typedef void* pointer;
  51. };
  52. #endif

三、SGI特殊的空间配置器alloc

通常,C++中用new操作符来分配内存都包括两个阶段:

(1)调用::operator new配置内存

(2)调用构造函数来构造对象内容

同理,delete操作也包括两个阶段:

(1)调用析构函数将对象析构

(2)调用::operator delete释放内存

为了精密分工,SGI allocator将两个阶段分开

内存配置操作由alloc:allocate负责,内存释放由alloc:deallocate负责;对象构造操作由::contructor()负责,对象析构由::destroy()负责。

配置器定义在头文件<memory>中,它里面又包括两个文件:

[cpp] view plaincopyprint?

  1. #include <stl_alloc.h>        // 负责内存空间的配置和器释放
  2. #include <stl_construct.h>        // 负责对象的构造和析构

下图显示了其结构:

图一 头文件 <memory>结构

1、对象的建构和结构函数construct()和destroy()

图二显示了这两个函数的结构和功能。他们被包含在头文件stl_construct.h中。

图二 函数construct()和destroy()示意图

函数construct()使用了定位new操作符,其源代码:

[cpp] view plaincopyprint?

  1. template <class T1, class T2>
  2. inline void construct(T1* p, const T2& value) {
  3. new (p) T1(value);    // 定为new操作符placement new; 在指针p所指处构造对象
  4. }

函数destroy则有两个版本。

第一个版本较简单,接受一个指针作为参数,直接调用对象的析构函数即可,其源代码:

[cpp] view plaincopyprint?

  1. template <class T>
  2. inline void destroy(T* pointer) {
  3. pointer->~T();   // 调用析构函数
  4. }

第二个版本,其参数接受两个迭代器,将两个迭代器所指范围内的所有对象析构掉。而且,它采用了一种特别的技术:依据元素的型别,判断其是否有trivial destructor(无用的析构函数)进行不同的处理。这也是为了效率考虑。因为如果每个对象的析构函数都是trivial的,那么调用这些毫无作用的析构函数会对效率造成影响。

下面看其源代码:

[cpp] view plaincopyprint?

  1. // 如果元素的数值型別(value type)有 non-trivial destructor…
  2. template <class ForwardIterator>
  3. inline void
  4. __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) {
  5. for ( ; first < last; ++first)
  6. destroy(&*first);//调用析构函数
  7. }
  8. // 如果元素的数值型別(value type)有 trivial destructor…
  9. template <class ForwardIterator>
  10. inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {}//不调用析构函数
  11. // 判断元素的数值型別(value type)是否有 trivial destructor,分别调用上面的函数进行不同的处理
  12. template <class ForwardIterator, class T>
  13. inline void __destroy(ForwardIterator first, ForwardIterator last, T*) {
  14. typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
  15. __destroy_aux(first, last, trivial_destructor());
  16. }
  17. // 以下是 destroy() 第二版本,接受两个迭代器。它会设法找出元素的数值型別,
  18. // 进而利用 __type_traits<> 求取最适当措施。
  19. template <class ForwardIterator>
  20. inline void destroy(ForwardIterator first, ForwardIterator last) {
  21. __destroy(first, last, value_type(first));
  22. }

第二版本还针对迭代器为char*和wchar_t*定义了特化版本:

[cpp] view plaincopyprint?

  1. inline void destroy(char*, char*) {}
  2. inline void destroy(wchar_t*, wchar_t*) {}

2、空间的配置和释放,std::alloc

对象构造前的空间分配和析构后的空间释放,定义在头文件<stl_alloc.h>中。其设计思想是:

  • 向system heap要求空间。
  • 考虑多线程状态。
  • 考虑内存不足时的应变措施。
  • 考虑过多“小额区块”可能造成的内存碎片问题。

考虑到小型区块可能造成的内存破碎问题,SGI设计了双层级配置器,第一级配置器直接使用malloc()和free(),第二级则视情况采用不同的策略。而且采用了复杂的内存池memory pool整理方式。整个设计究竟是只开放第一级配置器还是同事开放第二级配置器取决于宏__USE_MALLOC是否被定义。

[cpp] view plaincopyprint?

  1. # ifdef __USE_MALLOC
  2. ...
  3. typedef __malloc_alloc_template<0> malloc_alloc; <span style="font-family:KaiTi_GB2312;">//令 alloc为第一级配置器</span>
  4. typedef malloc_alloc alloc;
  5. # else
  6. ...
  7. //令 alloc 为第二级配置器
  8. typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;
  9. #endif /* ! __USE_MALLOC */

SGI并未定义它。

无论alloc被定义为第一级或者是第二级配置器,SGI还为它包装一个接口如下,使配置器的接口能够符合STL规格:

[cpp] view plaincopyprint?

  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. static T *allocate(void)
  7. { return (T*) Alloc::allocate(sizeof (T)); }
  8. static void deallocate(T *p, size_t n)
  9. { if (0 != n) Alloc::deallocate(p, n * sizeof (T)); }
  10. static void deallocate(T *p)
  11. { Alloc::deallocate(p, sizeof (T)); }
  12. };

SGI STL容器全部是使用这个simple_alloc接口。

第一级和第二级配置器之间的关系如图三所示。

图三 第一级配置器和第二级配置器

第一级和第二级配置器的包装接口和运用方式如下:

第一级配置器__malloc_alloc_template剖析

第一级配置器直接使用malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重配置操作,并实现出类似C++ new handler机制。它有独特的out-of-memory内存处理机制:在抛出std::bad_alloc异常之前,调用内存不足处理例程尝试释放空间,如果用户没有定义相应的内存不足处理例程,那么还是会抛出异常。详细实现见函数oom_malloc(),oom_realloc()。

内存不足处理例程保存在函数指针__malloc_alloc_oom_handler里面。

下面列出代码:

[cpp] view plaincopyprint?

  1. #if 0
  2. #   include <new>
  3. #   define  __THROW_BAD_ALLOC throw bad_alloc
  4. #elif !defined(__THROW_BAD_ALLOC)
  5. #   include <iostream.h>
  6. #   define  __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1)
  7. #endif
  8. // malloc-based allocator. 通常比稍后介绍的 default alloc 速度慢,
  9. //一般而言是 thread-safe,并且对于空间的运用比较高效(efficient)。
  10. //以下是第一级配置器。
  11. //注意,无「template 型别参数」。至于「非型别参数」inst,完全没派上用场。
  12. template <int inst>
  13. class __malloc_alloc_template {
  14. private:
  15. //以下都是函式指标,所代表的函式将用来处理内存不足的情况。
  16. // oom : out of memory.
  17. static void *oom_malloc(size_t);
  18. static void *oom_realloc(void *, size_t);
  19. static void (* __malloc_alloc_oom_handler)();
  20. public:
  21. static void * allocate(size_t n)
  22. {
  23. void  *result =malloc(n);//第一级配置器直接使用 malloc()
  24. // 以下,无法满足需求时,改用 oom_malloc()
  25. if (0 == result) result = oom_malloc(n);
  26. return  result;
  27. }
  28. static void deallocate(void *p, size_t /* n */)
  29. {
  30. free(p); //第一级配置器直接使用 free()
  31. }
  32. static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)
  33. {
  34. void  *  result  =realloc(p, new_sz);//第一级配置器直接使用 rea
  35. // 以下,无法满足需求时,改用 oom_realloc()
  36. if (0 == result) result = oom_realloc(p, new_sz);
  37. return  result;
  38. }
  39. //以下模拟 C++的 set_new_handler(). 换句话说,你可以透过它,
  40. //指定你自己的 out-of-memory handler
  41. static void (* set_malloc_handler(void (*f)()))()
  42. {
  43. void  (*  old)()  =  __malloc_alloc_oom_handler;
  44. __malloc_alloc_oom_handler = f;
  45. return(old);
  46. }
  47. };
  48. // malloc_alloc out-of-memory handling
  49. //初值为 0。有待客端设定。
  50. template <int inst>
  51. void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
  52. template <int inst>
  53. void * __malloc_alloc_template<inst>::oom_malloc(size_t n)
  54. {
  55. void  (* my_malloc_handler)();
  56. void  *result;
  57. for (;;)  {
  58. //不断尝试释放、配置、再释放、再配置…
  59. my_malloc_handler = __malloc_alloc_oom_handler;
  60. if  (0  ==  my_malloc_handler)  {  __THROW_BAD_ALLOC; }
  61. (*my_malloc_handler)();//呼叫处理例程,企图释放内存。
  62. result = malloc(n);  //再次尝试配置内存。
  63. if  (result)  return(result);
  64. }
  65. }
  66. template <int inst>
  67. void * __malloc_alloc_template<inst>::oom_realloc(void *p, size_t n)
  68. {
  69. void  (* my_malloc_handler)();
  70. void  *result;
  71. for (;;)  {  //不断尝试释放、配置、再释放、再配置…
  72. my_malloc_handler = __malloc_alloc_oom_handler;
  73. if  (0  ==  my_malloc_handler)  {  __THROW_BAD_ALLOC; }
  74. (*my_malloc_handler)();//呼叫处理例程,企图释放内存。
  75. result = realloc(p, n);//再次尝试配置内存。
  76. if  (result)  return(result);
  77. }
  78. }
  79. //注意,以下直接将参数 inst指定为 0。
  80. typedef __malloc_alloc_template<0> malloc_alloc;

需要注意的是:设计“内存不足处理例程”是客端的责任。关于设计“内存不足处理例程”的具体做法可参考《Effective
C++(第二版)》条款7。

时间: 2024-11-03 21:31:31

STL源码分析--空间配置器 第一级配置器的相关文章

STL源码分析--空间配置器的底层实现 (二)

STL源码分析-空间配置器 空间配置器中门道 在STL中的容器里都是使用统一的空间配置器,空间配置器就是管理分配内存和销毁内存的.在STL将在heap空间创建一个对象分为两个步骤,第一是申请一块内存,第二是在这块内存中初始化一个对象.首先申请空间是由malloc提供,初始化一个对象时由constructor管理.销毁一个对象也是由两步骤完成,第一是销毁空间上的对象,第二是释放这块内存. 同时,STL的空间配置器分为两级内存,如果申请的内存空间大于128KB,那么就使用第一级空间配置,如果小于,那

STL源码分析--仿函数 &amp; 配接器

STL源码分析-仿函数 & 配接器 仿函数就是函数对象.就实现观点而言,仿函数其实就是一个"行为类似函数"的对象.为了能够"行为类似函数",其类别定义中必须自定义(或说改写.重载)function call 运算子(operator()),拥有这样的运算子后,我们就可以在仿函数的对象后面加上一对小括号,以此调用仿函数所定义的operator().仿函数作为可配接的关键因素. 配接器在STL组件的灵活组合运用功能上,扮演着轴承.转换器的角色,adapter的定

STL源码剖析——空间的配置与释放

C++的内存配置基本操作是  ::operator new(),内存释放的基本操作是 ::operator delete().这两个全局函数相当于C的malloc()和free()函数.是的,正是如此,STL正是以malloc()和free()完成内存的配置与释放. 但是考虑到小型区块所可能造成的内存破碎问题,STL中设计了双层级配置器, 第一级配置器直接使用malloc()和free(),第二级配置器则视情况采用不用的策略:当配置区块超过128bytes时,视之为"足够大",便调用第

stl源码分析之hash table

本文主要分析g++ stl中哈希表的实现方法.stl中,除了以红黑树为底层存储结构的map和set,还有用哈希表实现的hash_map和hash_set.map和set的查询时间是对数级的,而hash_map和hash_set更快,可以达到常数级,不过哈希表需要更多内存空间,属于以空间换时间的用法,而且选择一个好的哈希函数也不那么容易. 一. 哈希表基本概念 哈希表,又名散列表,是根据关键字直接访问内存的数据结构.通过哈希函数,将键值映射转换成数组中的位置,就可以在O(1)的时间内访问到数据.举

stl源码分析之vector

上篇简单介绍了gcc4.8提供的几种allocator的实现方法和作用,这是所有stl组件的基础,容器必须通过allocator申请分配内存和释放内存,至于底层是直接分配释放内存还是使用内存池等方法就不是组件需要考虑的事情.这篇文章开始分析gcc4.8 stl的容器源码实现.stl的容器分为序列式容器和关联式容器,前者包括vector,list,queue以及stack等常用数据结构,后者包含了map,set以及hash table等比较高级的结构,本文就从使用最广泛也是最基础的vector开始

stl源码分析之list

本文主要分析gcc4.8版本的stl list的源码实现,与vector的线性空间结构不同,list的节点是任意分散的,节点之间通过指针连接,好处是在任何位置插入删除元素都只需要常数时间,缺点是不能随机访问,查询复杂度是O(n),n为list中的元素个数.所以list非常适合应用与数据插入删除频繁的场景. 一. list节点 list节点定义如下, struct _List_node_base { _List_node_base* _M_next; _List_node_base* _M_pre

stl源码分析之priority queue

前面两篇介绍了gcc4.8的vector和list的源码实现,这是stl最常用了两种序列式容器.除了容器之外,stl还提供了一种借助容器实现特殊操作的组件,谓之适配器,比如stack,queue,priority queue等,本文就介绍gcc4.8的priority queue的源码实现. 顾名思义,priority queue是带有优先级的队列,所以元素必须提供<操作符,与vector和list不同,priority queue允许加入元素,但是取出时只能取出优先级最高的元素. 一. pri

STL源码分析--仿函数 &amp; 模板的模板参数 &amp; 临时对象

STL源码分析-使用的一些特殊语法 关于泛型编程中用到的一些特殊语法,这些语法也适用于平常的模板编程 1.  类模板中使用静态成员变量 Static成员变量在类模板中并不是很特殊,同时这个变量不属于对象,属于实例化以后的这个类类型.每一个实例化对应一个static变量 2.  类模板中可以再有模板成员 3.  模板参数可以根据前一个模板参数而设定默认值 4.  类模板可以拥有非类型的模板参数 所谓非类型的模板参数就是内建型的模板参数 Template <class T,class Alloc =

STL 源码分析 # stl_iterator &amp; stl_iterator_base #

STL 源码分析 # stl_iterator_base && stl_iterator # 这里能很清楚的看到各个基础类型的继承关系 template <class _Tp, class _Distance> struct input_iterator { typedef input_iterator_tag iterator_category; typedef _Tp value_type; typedef _Distance difference_type; typede