SGI STL空间配置器(STL源码剖析)
空间配置器的标准接口(根据STL规范)
allocator::value_type
——————————————————————————————————————
SGI STL 的配置器与众不同, 名称是alloc而不是allocator, 而且不接受任何参数。
vector<int , std::allocator<int> > iv; //in VC or CB
但是通常都是使用默认的空间配置器,而SGI STL已经为每一个容器都指定了缺省的空间配置器。所以使用的时候无太大区别。
template<class T, class Alloc = alloc>
————————————————————————————————————————
SGI空间配置器分析:
C++的new操作符和delete操作符进行内存配置时,new:先配置内存,然后构造对象。delete:先析构对象,然后释放内存。SGI STL将内存配置、释放内存与构造、析构分开。前者由<stl_alloc.h>中的allocate()和deallocate()负责,后者由<stl_construct.h>中的construct()和destroy()负责。 这两个头文件包含于<memory>中(STL标准规定,配置器定义在memory中)。同时<memory>还包含了一个文件<stl_unitialized.h>,定义了一些全局函数用来填充、复制大块内存数据。
1.构造和析构。
构造:
template<class T1, class T2>
PS:placement new运算,并不分配内存,而是将已分配的内存调用构造函数来进行初始化。
析构:
析构函数第一个版本接受一个指针,将指针所指之物删除,可以直接调用析构函数。而第二个版本,接受两个迭代器,将其之间的所有对象全部析构。由于我们不知道这个范围的大小,有时候范围很大而每个对象的析构不是必要的,就会造成浪费。因此需要进行一个判断,根据得到的结果来判断是否需要析构它(判断用到了traits方法)。
下面是最终的析构调用情况,其中__false_type和__ture_type是两个空的struct,只用于标识,使其通过函数重载在编译的时候确定调用哪一个版本。
template<class ForwardIterator>
同时这个析构函数对char*和wchar_t*有特化版本:将不调用他们的析构函数。(这是没有必要调用析构函数的例子么?)
2. 空间的配置和释放: std::alloc
std::alloc为了效率,设计了双层级配置器,第一级直接使用C的malloc()和free()。第二级则视配置区块的大小选择配置器,如果区块大,则直接调用第一级,如果区块过小,则采用memory pool 整理的方法。
SGI将其简单的包装了一个接口,simple_alloc类。使其符合STL规格。而在使用的时候全部使用的simple_alloc接口(缺省使用)。
第一级配置器:
调用malloc()和realloc(),如果配置不成功,则调用oom_malloc(), oom_realloc()(PS:oom= out of memory)。在oom_XXX的处理时,通过不断调用“内存不足处理例程”,期望在某次调用之后能够分配到。设计“内存不足处理例程”是客户端的责任。(effective C++ 对new-handler的行为有一个条款,还没认真看)
第二级配置器:
由于太多小额的区块会造成内存的碎片,同时也会带来额外的负担,因此通过第二级配置器的分配,适当的将小额内存分配出去。当区块小(<128bytes)的时候,采用内存池管理。第二级配置器维护了16个free-lists,各自管理从8~128bytes的小额区块,每一个区块以8bytes递增(将每一个小额的区块需求上调至8的倍数,以便管理)。每一次需要时候就将适当的内存从free-lists中间拔出来。客户释放后就回收到free-lists中。当需要的free-lists中没有可用的区块的时候,调用refill()函数为free-lists填充新的空间(取自内存池,用chunk_alloc()函数)。内存池的处理十分复杂,看的一头雾水。
SGI配置器使用方法:
template <class T, class Alloc = alloc>
其中第二个template参数接受缺省的alloc,可以是第一级配置器也可以是第二级配置器。SGI STL已经把它设定为第二级配置器。
3. 内存基本处理工具
在头文件<stl_uninitialized>中,定义了3个全局函数,uninitialized_copy(),uninitialized_fill(),uninitialized_fill_n(). 看到这些名字应该就知道它们是什么作用了,它们负责在已分配好的空间进行构造。这三个函数都具有”commit or rollback”语意。要么构造出所有元素,要么不构造任何东西。
在具体的实现中,这三个函数也用到了traits技法,通过traits萃取出元素的特性,如果是POD(Plain Old Data)型别,说明其拥有trivial ctor/ dtor/ copy/ assignment函数,因此可以使用copy()、fill()、fill_n()等算法,如果不是POD型别就只能够循环调用construct()函数了。这里用到的也是函数重载的方法,和上面destroy用到的方法是一样的。
注意:在unitialized_copy()实现的时候,针对char*和wchar_t*两种型别,可以采用最有效率的memmove()来复制,因此需要两份特化版本。