STL内存管理

  过年在家无事看了《STL源码剖析》,开学了将看过的东西总结一下,以防忘记。

  先从STL的内存管理开始总结。掌管STL内存控制的是一个叫空间适配器(alloc)的东西。STL有两个空间适配器,SGI标准空间适配器(allocate)和SGI特殊的空间适配器(alloc),前者只是对c++的new和delete进行了简单的封装。下面主要介绍的是alloc空间适配器。

  一般而言,c++的内存配置和释放操作是这样的:

class Foo {...}

Foo* pf = new Foo;

delete pf;

  其中new操作符包含了两个阶段:1、operator new配置空间;2、调用Foo构造函数构造内容。同理,delete操作符也包含调用析构函数和释放空间两个过程。STL内存管理参考了这种方法,将空间配置和构造内容分开。内存配置和内存释放分别由alloc::allocate() 和 alloc::deallocate() 负责,对象构造和析构分别由alloc::construct() 和 alloc::destroy()负责。

  下面先介绍construct() 和 destroy() 函数。

template <class T1, class T2>
inline void construct(T1* p, const T2& value){
  new (p) T1(value); // 这里的new是placement new,是operator new 的重载版本,只调用对象的构造函数,并不进行内存的分配. operator new如上所说,会有两步。   
}
template <class T>
inline void destroy(T* pointer){
    pointer->~T(); // 调用析构函数
}

  从代码中可以看出,负责对象构造和析构的alloc::construct() 和 alloc::destroy()函数就是对构造函数T()和析构函数~T()的封装。(注意:上面代码并没有涉及任何的内存配置与释放,单纯的调用构造函数和析构函数,这就是constructor()里面为什么用placement new 而不是 operator new 的原因。)另外,alloc重载第二个版本的alloc::destroy(),接受两个iterator参数,用来批量地析构对象,并且用type_traits编程技法判断对象是否有tirvial destructor来判断是否需要显示的调用destroy函数,以此来加快批量析构对象的速度。其中type_traits简言之就是用泛化的手法提取对象的型别信息,用来判断该对象到底有没有trivial destructor,trivial destructor现在你可以理解为没有显示地定义析构函数,例如系统内置的int型变量,后面等有时间再总结一下type_traits和iterator_traits的实现原理。

  前面总结了内存配置的第二部分----对象的构造和析构,下面总结一下第一部分----空间的配置与释放。

  SGI以malloc()和free()完成内存配置与释放。为了解决小型区块所可能造成的内存破碎问题,SGI设计了双层级配置器,第一级直接使用malloc()和free()配置和释放内存空间,第二级配置器采用如下策略:当配置区块超过128 bytes 时,视为足够大,调用第一级配置器,当小于128 bytes 时,采用memory pool的整理方式。下面主要总结第二级配置器。

  如前所述,SGI第二级配置器的策略为:如果区块足够大,超过128 bytes 时,就移交第一级配置器处理。当区块小于128 bytes 时,则以memory pool管理----每次配置一大块内存的时候,都对应维护一个自由链表(free-list)。下次若有相同大小的内存需求时,直接从free-list中取,当有内存被释放时,回收到free-list当中。第二级配置器维护16个free-list,分别管理8,16,24...128 bytes 大小的区块,客户端请求的区块会自动调整到上述大小,如 20 bytes 调整到 24 bytes。下面是free-list的节点。

union obj{
    union obj* free_list_link;
    char client_data[1];  // 客户端看到的是这个字段    
}

这里之所以用union而不同struct,是为了节约空间,union中的变量共用同一个存储空间,obj里面就存储着一个地址,指向下一个可用区块,而那个区块的首个字节就是obj,也可以说这个地址也指向下一个obj。可以这么理解:可用区块的第一个字节构成了一个链表,维护了同样大小的区块,而第一个区块的地址就放在free-list中。当free-list里面某个大小的区块无可用区块时,就由chunk_alloc() 函数向内存池请求空间。上述总结可能没有那么直观,下面结合部分代码具体解释。由于代码较多,故只挑选关键代码附以自己的理解,全部代码可参考《STL源码剖析》。注:源码中使用union的做法只是为了节约空间,但是现在内存大小已不是人们关心的主要问题,又加之union在实际使用中会出现各种奇怪问题,所以大部分情况下还是推荐有struct代替。

// __NFREELISTS == 16 表示配置器所维护的16个free_list; volatile表示不允许编译器对free_list变量进行优化; free_list里面放的是obj的地址,也就是说该大小下可使用的第一个区块。
static obj * volatile free_list[__NFREELISTS];
static void * allocate(size_t n){
  obj * volatile * my_free_list; // 是指向obj*的指针
  obj * result;
  // 如果大于128就调用第一级适配器
  if(n > (size_t) __MAX_BYTES){
      return(malloc_alloc::allocate(n));
  }
  // 找到16个当中合适的free_list
  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;
  }
  *my_free_list = result -> free_list_link;// 这里是理解的关键,其实result已经指向了第一个可用区块,区块的第一个字节是下一个可用区块也是下一个obj的地址,所以这句话就已经移除了第一个可用区块。
  return result;
}

  空间释放函数deallocate()首先判断是否大于128 bytes,大于交由第一级配置器处理,小于就找出对应的free_list将之收回。在理解了allocate()的代码之后,deallocate()里面的关键代码也就不难理解了:

my_free_list = free_list + FREELIST_INDEX(n);
q -> free_list_link = *my_free_list; // q指向要收回的区块
*my_free_list = q;

  重新填充 free_list 的 refill() 函数就是用chunk_alloc函数从内存池中取出新的空间(缺省为20个某个大小的新区块,如内存池空间不够也可能小于20),并构造成链表的形式挂在当前大小的free_list后面。在理解了新区块的第一个字节为下一区块以及下一obj的地址这一概念之后,refill()的代码也不难理解,故不在下面贴代码了。

  前面一直提到的chunk_alloc()函数,它的工作是为free_list从内存池中取空间。它的逻辑为若内存池空间完全满足需求,则为free_list配置20个新区块,若内存池剩余空间不能完全满足需求,但足够供应一个及以上的区块,则为free_list配置力所能及的区块数目,若内存池剩余空间连一个区块大小都无法满足,就先把内存池中的剩余零头分配给适当的free_list,然后释放维护更大区块的free_list中的取尚未用的区块,将空间还给内存池,以供内存池为当前free_list配置空间,若,山穷水尽,到处都没有内存了,那就只能求助于第一级配置器,如果他再搞不定,那就只能抛出异常了。在理解前面代码的基础上,chunk_alloc的代码也不难理解。

  本章后面还介绍了五个STL的全局函数,作用于未初始化的空间上,方便后面的容器实现。

  STL的内存管理大体就是这样,主要用了malloc 和 free对内存进行配置,等回头看看TCMalloc对malloc 和 free 改进再总结一记吧。

posted on 2015-05-30 16:16  zpjjtzzq  阅读(1491)  评论(0编辑  收藏  举报