STL源码学习——空间配置器

前言

最近开始找实习了,对于STL的实现,一知半解,挺多认识都停留在表层,所以还是想看一看源码,所以找了侯捷的《STL源码剖析》——源码之前,了无秘密。

我并不打算第一遍就把它全部弄懂,好的知识和代码总是需要反复咀嚼才能领悟其思想,也许以后会再读很多遍。

最近时间比较紧张,准备面试,查漏补缺,所以都没什么时间写博客,本来希望赶赶进度自己把这本书大致地看一遍就好,但是现在还是些了博客,强迫自己慢下来,把学的东西整理总结一下。

正如刚才所说,我并不打算一遍就把STL弄懂,但至少看过一遍后,对整理的设计有所理解,可能在细节的处理方面暂时无需过多关注。

接下来我们讨论的都是SGI STL。

STL六大组件及其关系

这里写图片描述

Container通过allocator取得存储空间;

Algorithms通过iterator存取container中的内容;

Functor协助Algorithms完成不同的策略变化;

Adapter可以修饰或者套接Functor。

allocator

下面我们就开始看看STL的空间配置器allocator。

1.std::allocator

SGI STL 的头文件defalloc.h中有一个符合标准的名为allocator的内存分配器,它只是简单地将::operator new 和::operator delete做了一层薄薄的封装。在SGI STL的容器和算法部分从来没有用到这个内存分配器。

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

上面的std::allocator并没有在效率上做强化,SGI则有自己的配置器来提高效率。

一般,当用户用new构造一个对象的时候,其实内含两种操作:
1)调用::operator new申请内存;
2)调用该对象的构造函数构造此对象的内容

当用户用delete销毁一个对象时,其实内含两种操作:
1)调用该对象的析构函数析构该对象的内容;
2)调用::operator delete释放内存

为精密分工,STL allocator将这两个阶段区分开来,这样做是有好处的,后面我们会谈到。

SGI STL中对象的构造和析构由::construct()和::destroy()负责;内存的申请和释放由alloc:allocate()和alloc:deallocate()负责;此外,SGI STL还提供了一些全局函数,用来对大块内存数据进行操作。

上一段提到的三大模块分别由stl_construct.h、stl_alloc.h、stl_uninitialized.h 负责。

下面我们将分别看看这3个部分。

3.构造和析构基本工具:construct()和destroy()

#include <new.h> //欲使用placement new,需要包含此头文件
template <class T1, class T2>
inline void construct(T1* p, const T2& value) {
  new (p) T1(value);//调用T1::T1(value),将初值设置到p所指空间上
}

destroy()有两个版本:

//版本1
template <class T>//析构单个元素  
inline void destroy(T* pointer) {  
    pointer->~T();  
}  
//版本2
//析构[first, last)之间的元素
template <class ForwardIterator>  
inline void destroy(ForwardIterator first, ForwardIterator last) {  
  __destroy(first, last, value_type(first));//通过泛型的类型识别技术来得到元素类型(由__type_traits<>来求取)
}

template <class ForwardIterator, class T>  
inline void __destroy(ForwardIterator first, ForwardIterator last, T*) {  
  //通过元素型别来判断析构函数是否无关紧要(trivial) 并调用对应的函数进行析构  
  typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;  
  __destroy_aux(first, last, trivial_destructor());  
}  

//如果元素的析构函数是必要的,那么逐个调用析构函数(比如含有指针成员的元素)  
template <class ForwardIterator>  
inline void  
__destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) {  
  for ( ; first < last; ++first)
    destroy(&*first);  
}  

//如果元素的析构函数是无关紧要的,就什么也不做(比如int。。) 
template <class ForwardIterator> 
inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {}

//下面是第二个版本针对char*和wchar_t*的特化
inline void destroy(char*, char*) {}  
inline void destroy(wchar_t*, wchar_t*) {}  

关于__type_traits和特化,这里暂不讨论。

暂时只要知道,__type_traits可以获得通过迭代器型别来判断元素的数值型别。

构造函数没有什么特别之处,而析构函数destroy()就比较巧妙了。destroy并不是盲目地对这个范围内所有元素依次调用析构函数,为提高效率,它先通过泛型的类型解析,在_destory()中得到元素类型,再通过元素类型的_type_traits::has_trivial_destructor trivial_destructor来判断元素类型的析构函数是否是无关紧要(trivial)的,如果是,那么trivial_destructor()值为true_type,否则为false_type(这里的true_type,false_type是一种类型而不是值)。之后再通过一个_destroy_aux()的重载对两种情况分别处理。

如果是false_type,也就是元素类型的析构函数是必要的,于是老老实实依次调用每个元素的析构函数,否则,什么也不做。这对于销毁大范围的元素来说,如果析构函数无关痛痒,那么效率上将会有很大提升。

除此之外,stl_construct还为一些基本类型的对象提供了特化版本的destroy函数,这些基本类型分别是char, int, float, double, long。当destroy的参数为这些基本类型时,destroy什么都不做。

以上的构造和析构函数被设计为【全局函数】。

4.空间的配置和释放std::alloc()

SGI通过malloc()和free()来完成内存的配置和释放(在前面的allocator中,内存的分配和释放使用的是::operator new()和::operator delete()函数,它们的内部其实也是调用C语言的malloc和free来实现的,在alloc中,内存的分配和释放直接使用malloc()和free()两个函数)

为缓解小型区块可能造成的内存破碎问题,SGI设计了两级配置器:

一级配置器直接从堆中获取和释放内存(malloc和free),效率和前面的allocator相当。

二级适配器采用内存池(memory pool)技术,对用户的小区块申请进行了优化,当用户申请大区块时(128bytes),它将其交予一级配置器。当用户申请小区块时(小于128bytes),将于内存池打交道,内存池通过自由链表(free list)来管理小区块,当内存池不足时,会一次性向堆中申请足够大的空间。

用户可以通过宏来控制使用哪一级配置器(默认为二级配置器)。

一级配置器:__malloc_alloc_template

第一层配置器直接通过malloc来分配内存,并在此之上建立内存不足处理例程。

当调用malloc和realloc申请不到内存空间的时候,会改调用oom_malloc()和oom_realloc(),这两个函数会反复调用用户传递过来的out of memory handler处理函数,直到能用malloc或者realloc申请到内存为止。如果用户没有传递__malloc_alloc_oom_handler,__malloc_alloc_template会抛出__THROW_BAD_ALLOC异常。所以,内存不足的处理任务就交给类客户去完成。

static void* allocate(size_t __n)
  {
    void* __result = malloc(__n);
    if (0 == __result) __result = _S_oom_malloc(__n);//内存不足 调用处理例程
    return __result;
  }

  static void deallocate(void* __p, size_t /* __n */)
  {
    free(__p);
  }

  static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
  {
    void* __result = realloc(__p, __new_sz);
    if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
    return __result;
  }

二级配置器:__default_alloc_template

这个分配器采用了内存池的思想,有效地避免了内碎片的问题(内碎片是已被分配出去但是用不到的内存空间,外碎片是由于大小太小而无法分配出去的空闲块)。

其实小区块带来的不仅是内碎片,配置时的额外负担也是一个大问题(比如你需要一块32bytes的空间,除此之外需要有一些额外的空间分配出来,用以记录内存的大小)。

二级配置器的做法:

如果申请的内存块大于128bytes,就将申请的操作移交__malloc_alloc_template分配器去处理;

如果申请的区块大小小于128bytes时,就从通过内存池和自由链表(free_list)来处理。它每次配置一块大的内存交给自由链表维护,用户每次申请的内存都从链表中获取,并且在释放时交还给自由链表。SGI STL将用户申请的128bytes以内的内存自动上调到8的倍数,并维护16个free_list,各free_list负责的大小分别为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128bytes。比如用户申请10bytes内存,将被上调到16bytes,并且从负责管理16bytes内存的free_list中取出一个节点(也就是一块内存),如果free_list中当前没有节点,则从内存池中分配足够内存(由chuck_alloc()完成),并且填充到free_list中(如果内存池中也没内存了,就调用malloc()从heap中分配内存来扩充内存池,如果真的一点内存都找不到了,就调用一级配置器,因为那里有异常处理函数!)

分配器用空闲链表的方式维护内存池中的空闲空间,这样一来,每个节点需要额外的指针来指向下一个节点,岂不是造成另一种负担?

我们看看free-list节点的结构:

union obj  
{  
    union obj* free_list_link; //用于维护空闲内存,指向下一个空闲节点  
    char client_data[1];    //用于用户使用  
}  

它巧妙地使用union实现一物二用:

当节点在free-list中时,obj中存的是指针;
当节点被用户取用时,obj中存的是用户数据。

这里暂时只讲它的思想,具体源码就不分析了:-D

SGI STL 为了方便用户访问,为两种分配器包装了一个接口:

template<class Tp, class Alloc>
class simple_alloc {

public:
    static Tp* allocate(size_t n)
      { return 0 == n ? 0 : (Tp*) Alloc::allocate(n * sizeof (Tp)); }
    static Tp* allocate(void)
      { return (Tp*) Alloc::allocate(sizeof (Tp)); }
    static void deallocate(Tp* p, size_t n)
      { if (0 != n) Alloc::deallocate(p, n * sizeof (Tp)); }
    static void deallocate(Tp* p)
      { Alloc::deallocate(p, sizeof (Tp)); }
};

它们都只是简单的转调用而已。

5.基本内存处理工具

STL定义了5个全局函数,作用于未初始化空间上,除了上面的内存配置器之外,还有:uninitialized_copy(), uninitialized_fill()和uninitialized_fill_n()。这三类函数的实现代码在头文件stl_uninitialized。 (或者可以包含memory头文件)
(它们分别对应于高层次函数copy(), fill(), fill_n())

uninitialized_copy():

template <class InputIter, class ForwardIter>
inline ForwardIter
uninitialized_copy(InputIter first, InputIter last,
                     ForwardIter result)
{
  return __uninitialized_copy(first, last, result,
                              value_type(result));
  //利用value_type取出first的value type
}

uninitialized_copy()会将迭代器first和last之间的对象拷贝到迭代器result开始的地方。

它调用的__uninitialized_copy(first, last, result,value_type(__result))会判断迭代器result所指的对象是否是POD类型(POD类型是指拥有constructor, deconstructor, copy, assignment函数的类),如果是POD类型,则调用算法库的copy实现(最有效率的复制手段);否则遍历迭代器first~last之间的元素,在result起始地址处一一构造新的元素(最保险安全的做法)。

另外,针对char* 和 wchar_t* ,可以采用最高效的做法memmove(直接移动内存内容)来执行复制行为,所以SGI为它们设计了一份特化的版本。

如针对const char*的特化版本:

inline char* uninitialized_copy(const char* first, const char* last, char* result)
{
    memmove(result, first, last - first);
    return result + (last - first);
}

uninitialized_fill():

template <class ForwardIter, class T>
inline void uninitialized_fill(ForwardIter first,
                               ForwardIter last, 
                               const T& x)
{
  __uninitialized_fill(first, last, x, value_type(first));
}

uninitialized_fill()会将迭代器_first和_last范围内的所有元素初始化为x。

它调用的__uninitialized_fill(first, last, x, value_type(first))会判断迭代器first所指的对象是否是POD类型的,如果是POD类型,则调用算法库的fill实现;否则一一构造。

uninitialized_fill_n():

template <class ForwardIter, class Size, class T>
inline ForwardIter 
uninitialized_fill_n(ForwardIter first, Size n, const T& x)
{
  return __uninitialized_fill_n(first, n, x, value_type(first));
}

uninitialized_fill_n()会将迭代器_first开始处的n个元素初始化为x。

它调用的__uninitialized_fill_n(first, n, x, value_type(first))会判断迭代器_first所指对象是否是POD类型,如果是,则调用算法库的fill_n实现;否则一一构造。

结语

这部分反复看了几遍,晚上通过写博客总结,收获还是蛮大的,至少整个的思路清晰了许多。

STL的内存分配以及迭代器是理解STL的关键,本文粗略地介绍了SGI STL的空间配置器,对于一些更加细节的东西,没有进行探讨,只是希望给初学者提供清晰的思路,而不是一开始就陷入太多细节的问题中。

接下来有空我将总结下迭代器,它将涉及到traits编程技法,可能需要多读几遍,才能消化下来。

参考文献

《STL源码剖析》第二章:空间配置器 侯捷 著

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值