【STL源码剖析】第二章 空间配置器(allocator)

第二章 空间配置器(allocator)

具备次配置力(sub-allocation)的SGI空间配置器

SGI标准的空间配置器

SGI定义了一个符合部分标准,名为allocator的配置器,效率不高,只把c++的::operator new和::operator delete做了一层薄薄的包装,SGI没有用过 。

SGI特殊的空间配置器,std::alloc

allocator只是基层内存配置/释放行为(也就是::operator new和::operator delete)的一层薄薄包装,并没有考虑到任何效率上的强化。

一般而言,我们所习惯的C++内存配置操作和释放操作是这样的:

  
  class Foo{...};
  Foo pf=new Foo;       //配置内存,然后构造对象
  delete pf;           //将对象析构,然后释放内存

这里new构造一个对象时,包含两阶段操作:(1)调用::operator new配置内存;(2)调用该对象的构造函数构造对象内容。delete销毁一个对象时,也包含两阶段操作:(1)调用该对象的析构函数将对象析构;(2)调用::operator delete释放内存。

STL allocator将上述两阶段操作区分开来。内存配置由alloc::allocator()负责,内存释放操作由alloc::deallocator()负责;对象构造由::constructor()负责,对象析构由::destroy()负责。

STL标准告诉我们,配置器定义于之中,SGI中包含以下两个文件:

#include //负责内存空间的配置和释放

#include //负责对象内容的构造与析构

【STL源码剖析】第二章 空间配置器(allocator)_第1张图片

析构和构造的基本工具,construct()和destroy()

下面是#include的部分内容:

  #include                 //placement new头文件
  template <class T1, class T2>  
  inline void construct(T1* p, const T2& value) {  
    new (p) T1(value);            //placement new;调用T1::T1(value)
  }  
  
  //以下是destroy()第一个版本,接受一个指针  
  template <class T>  
  inline void destroy(T* pointer) {  
      pointer->~T();  
  }  
  
  //destory()第二版本,接受两个迭代器。此函数没法找出元素数值型别
  template <class ForwardIterator>  
  inline void destroy(ForwardIterator first, ForwardIterator last) {  
    __destroy(first, last, value_type(first));  
  }  
  

上述destroy()的第一版本接受一个指针,将该指针所指的对象析构掉。第二版本接受first和last两个迭代器,将这两个迭代器范围内的对象析构掉。在第二版本中运用了traits编程技法,traits会得到当前对象的一些特性,再根据特性的不同分别对不同特性的对象调用相应的方法。在第二版本中,STL会分析迭代器所指对象的has_trivial_destructor特性的类型(只有两种:true_type和false_type),如果是true_type,STL就什么都不做;如果是false_type,就会调用每个对象的析构函数来销毁这组对象。

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

C++内存配置基本操作是::operator new(),内存释放基本操作是::operator delete()。这两个全局函数相当于C的malloc()和free()函数。正是如此,SGI以malloc和free完成内存的分配和释放。

考虑到小型区块可能造成的内存破碎问题,SGI设计了双层配置器,第一级配置器直接使用malloc()和free(),第二级配置器采取以下策略:当配置区块超过128bytes时,调用第一级配置器;当配置区块小于128bytes时,采用内存池方式。

【STL源码剖析】第二章 空间配置器(allocator)_第2张图片

第一级配置器

第一级空间配置器使用malloc()、free()、realloc()等c函数执行实际内存配置、释放、重配等操作,当分配的空间大小超过128 bytes的时候使用第一级空间配置器;

  static void * allocate(size_t n)  
  {  
      void *result = malloc(n);   //直接使用malloc()  
      if (0 == result) result = oom_malloc(n);  
      return result;  
  }  
    
  static void deallocate(void *p, size_t /* n */)  
  {  
      free(p);    //直接使用free()  
  }  
    
  static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)  
  {  
      void * result = realloc(p, new_sz);     //直接使用realloc()  
      if (0 == result) result = oom_realloc(p, new_sz);  
      return result;  
  }  

当alloc()和realloc()申请不到内存时,会调用oom_malloc()和oom_realloc(),这两个函数不断调用“内存不足处理函数”,直到获得足够内存为止。如果用户没有传递“内存不足处理函数”,会抛出__THROW_BAD_ALLOC异常。

第二级配置器

第二级空间配置器多了一些机制,避免太多小额区块造成内存的碎片。小额区块带来的不仅是内存碎片,配置时的额外负担也是一个问题。SGI第二级配置器的做法是,如果区块够大,超过128bytes时,就移交第一级配置器。当区块小于128bytes时,则以内存池(memory pool)管理,此法又称为层次配置(sub-allocation):每次配置一大块内存,并维护对应自由链表(free-list)。下次如若再有相同大小的内存需求,就直接从free-lists中拨出。如果客户端释还了小额区块,就由配置器回收到free-lists中,配置器除了负责配置也方便回收。

SGI STL的第二级内存配置器主动将任何小额区块的内存需求量上调至8的倍数(例如客户端需求30bytes。就自动调整为32bytes),并维护了一个free-list数组,分别用于管理8, 16, 24, 32,40,56,64,72,80,88,96,104,112,120,128 bytes的小额区块,free-list的节点结构如下:

  
  union obj
  {
      union obj* free_list_link;
      char client_data[1];
  };

​ 这里使用union结构,是为了节省空间,也就是说,当节点位于free-list时,通过free_list_link指向下一块内存,而当节点取出来分配给用户使用的时候,整个节点的内存空间对于用户而言都是可用的,这样在用户看来,就完全意识不到free_list_link的存在,可以使用整块的内存了。

​ 在分配内存时,会将大小向上调整为8的倍数,因为free-list中的节点大小全是8的倍数。

空间配置函数allocate()

  1. 如果申请内存大于128 bytes,就调用第一级配置器,否则说明申请内存小于128 bytes,转到2

  2. 根据申请内存的大小n在16个free lists中找出其对应的my_free_list

  3. 如果对应的my_free_list中没有空闲区块,分配器首先将申请内存的大小上调至8的倍数n,调用refill(),准备重新填充my_free_list

  4. 否则说明有可用的空闲区块,更新my_free_list并将第一块空闲区块返回

【STL源码剖析】第二章 空间配置器(allocator)_第3张图片

空间释放函数deallocate()

该函数首先判断区块大小,大于128bytes就调用第一级配置器,小于128bytes就找出对应的free list,将区块回收。

【STL源码剖析】第二章 空间配置器(allocator)_第4张图片

重新填充free lists

先前讨论allocate()。当发现free list中没有可用区块了时,就调用refill(),准备为free list重新填充空间。新的空间取自内存池(经由chunk_allock()完成)。缺省取得20个新节点(新区块),但万一内存池空间不足,获得的节点数(区块数)可能小于20。

内存池

从内存池中取空间给free list使用,是chunk_alloc()函数工作:

  1. 如果内存池剩余空间大于或等于20*n的内存空间,则从这个空间中取出n*20大小的内存空间,更新start_free并返回申请到的内存空间的起始地址,否则转到2)

  2. 如果内存池剩余空间足够分配一个及以上的区块,则分配整数倍于n的内存空间,更新start_free,由nobjs返回实际分配到的区块个数,并返回申请到的内存空间的起始地址,否则转到3)

  3. 内存池中无法提供一个大小为n的区块,此时如果内存池中还有一些残余内存(这些内存大小小于n),则将这些内存插入到其对应大小的空闲分区链中

  4. 调用malloc向运行时库申请大小为(2*20*n + 附加量)的内存空间, 如果申请成功,更新start_free,end_free和heap_size,并递归调用chunk_alloc(),修正nobjs,否则转到5)

  5. 4)中调用malloc失败,这时分配器依次遍历区块足够大的freelists,只要有一个未用区块,就释放该区块,递归调用chunk_alloc(),修正nobjs

  6. 如果出现意外,到处都没有内存可用了,则调用第一级配置器,看out-of-memory机制能否尽点力

【STL源码剖析】第二章 空间配置器(allocator)_第5张图片

内存处理基本工具

STL定义了五个全局函数,作用于未初始化空间上。这样的功能对于容器的实现很由帮助。前两个函数是用于构造的construct()和用于析构的destroy()。另外三个函数uninitialized_copy(),uninitialized_fill()和uninitialized_fill_n()函数,分别对应copy(),fill()和fill_n()函数——这些都是STL算法。

你可能感兴趣的:(STL源码剖析)