您的位置:首页 > 其它

STL学习笔记之空间配置器

2015-06-09 18:59 459 查看
最近看了看侯捷的《STL源码剖析》,打算看完之后写写笔记,毕竟很多东西看起来看懂了,却并不一定能够将其描述清楚,说到底还是没有彻底弄明白,最近博客也基本不怎么写了,所以还是决定写一写,这也算是写博客的乐趣之一吧。这一系列笔记,更主要是写给自己看的:)

1. 初探allocator

其实像我这样的一般人几乎接触不到allocator这种东西,因为这个模板参数是有默认值的,普通用户完全不需要和他打交道。但观察一下allocator这个东西的设计思路,还是可以学到不少东西。先从一个简单的allocator源代码看起:

#ifndef _JJALLOC_
#define _JJALLOC_

#include <new>
#include <cstddef>
#include <cstdlib>
#include <climits>
#include <iostream>

namespace JJ
{
// 使用operator new分配空间
template<class T>
inline T* _allocate(ptrdiff_t size, T*)
{
std::set_new_handler(0);
T *tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
if (tmp == 0)
{
std::cerr << "out of memory" << std::endl;
exit(1);
}
return tmp;
}
// 使用operator delete回收空间
template<class T>
inline void _deallocate(T* buffer)
{
::operator delete(buffer);
}
// 在指定内存上构造一个对象
template<class T1, class T2>
inline void _construct(T1* p, const T2& value)
{
// placement new
new (p) T1(value);
}
// 析构一个对象
template<class T>
inline void _destroy(T* ptr)
{
ptr->~T();
}
// 遵循allocator的标准定义相关结构
template<class T>
class allocator
{
public:
typedef T           value_type;
typedef T*          pointer;
typedef const T*    const_pointer;
typedef T&          reference;
typedef const T&    const_reference;
typedef size_t      size_type;
typedef ptrdiff_t   difference_type;

template<class U>
struct rebind
{
typedef allocator<U> other;
};

pointer allocate(size_type n, const void* hint=0)
{
return _allocate((difference_type)n, (pointer)0);
}

void deallocate(pointer p, size_type n)
{
_deallocate(p);
}

void construct(pointer p, const T& value)
{
_construct(p, value);
}

void destroy(pointer p)
{
_destroy(p);
}

pointer address(reference x)
{
return (pointer)&x;
}

const_pointer const_address(const_reference x)
{
return (const_pointer)&x;
}

size_type max_size() const
{
return size_type(UINT_MAX/sizeof(T));
}
};
}

#endif
上面的代码之中的几个点:

1. set_new_handler

set_new_handler的函数原型如下:

typedef void (*new_handler)();
new_handler set_new_handler (new_handler new_p) throw();


使用set_new_handler可以设置一个函数new_p,当使用new/operator new分配内存失败时,new_p将被调用。new_p将尝试使得更多内存空间可用,以使得接下来的内存分配操作能够成功。如果new_p指向NULL(默认就是NULL),那么将会抛出bad_alloc异常,这也是为什么我们默认使用new失败的时候将会抛出bad_alloc异常的原因;

2. 几个new/delete操作

我们使用的new叫做new operator,包括两个步骤,一是调用operator new来分配指定大小的内存空间,然后调用构造函数;所以如果只是进行空间分配操作,那么使用operator new就可以了,就好比C的malloc函数;如果已经分配好了空间,想在上面构造一个对象,那么可以使用placement new,上面的_construct函数里面调用的就是placement new;

3. 如何使用这个allocator?

定义vector时有一个模板参数用于指定allocator,如下所示:

#include "jjalloc.h"
#include <vector>
#include <iostream>
using namespace std;

int main(int argc, char **argv)
{
int ia[5] = {0, 1, 2, 3, 4};
unsigned int i;
vector<int, JJ::allocator<int> > iv(ia, ia+5);
for (i = 0; i < iv.size(); ++i)
{
cout << iv[i] << " ";
}
cout << endl;

return 0;
}


2. 构造与析构

在stl_construct.h中定义了构造和析构的相关函数:

// 调用placement new,根据__value在__p上构造一个对象
template <class _T1, class _T2>
inline void _Construct(_T1* __p, const _T2& __value) {
new ((void*) __p) _T1(__value);
}

// 调用placement new在__p上构造一个对象,使用默认构造函数
template <class _T1>
inline void _Construct(_T1* __p) {
new ((void*) __p) _T1();
}

// 析构一个对象
template <class _Tp>
inline void _Destroy(_Tp* __pointer) {
__pointer->~_Tp();
}

// 析构迭代器__first和__last之间的对象,实际上通过destroy函数,调用了对应的析构函数
template <class _ForwardIterator>
void
__destroy_aux(_ForwardIterator __first, _ForwardIterator __last, __false_type)
{
for ( ; __first != __last; ++__first)
destroy(&*__first);
}

// __destroy_aux重载函数,这里是对于trivial析构函数,不进行任何处理,提高效率
template <class _ForwardIterator>
inline void __destroy_aux(_ForwardIterator, _ForwardIterator, __true_type) {}

// 根据__type_traits萃取出类型_Tp的析构函数是否是trivial的,编译器根据类型自动选择对应的__destroy_aux
template <class _ForwardIterator, class _Tp>
inline void
__destroy(_ForwardIterator __first, _ForwardIterator __last, _Tp*)
{
typedef typename __type_traits<_Tp>::has_trivial_destructor
_Trivial_destructor;
__destroy_aux(__first, __last, _Trivial_destructor());
}

template <class _ForwardIterator>
inline void _Destroy(_ForwardIterator __first, _ForwardIterator __last) {
__destroy(__first, __last, __VALUE_TYPE(__first));
}

inline void _Destroy(char*, char*) {}
inline void _Destroy(int*, int*) {}
inline void _Destroy(long*, long*) {}
inline void _Destroy(float*, float*) {}
inline void _Destroy(double*, double*) {}
#ifdef __STL_HAS_WCHAR_T
inline void _Destroy(wchar_t*, wchar_t*) {}
#endif /* __STL_HAS_WCHAR_T */

// --------------------------------------------------
// Old names from the HP STL.

template <class _T1, class _T2>
inline void construct(_T1* __p, const _T2& __value) {
_Construct(__p, __value);
}

template <class _T1>
inline void construct(_T1* __p) {
_Construct(__p);
}

template <class _Tp>
inline void destroy(_Tp* __pointer) {
_Destroy(__pointer);
}

template <class _ForwardIterator>
inline void destroy(_ForwardIterator __first, _ForwardIterator __last) {
_Destroy(__first, __last);
}

__STL_END_NAMESPACE


这里值得一提的主要是析构部分使用的一些技巧。首先解释一下所谓的trivial destructor,值得就是调用不调用都无所谓的析构函数,那么处于效率方面的考虑,在这样的情况下肯定选择什么都不做(如果进行十万百万次这样的函数调用,是不是就白白浪费了大好的时光了?)而且这里是在编译器就通过函数的重载来决定是否要调用析构函数。

具体是通过__type_traits来萃取出类型是否具有trivial destructor的,这里在后面的文章会提到这些细节。现在所要了解的就是通过__type_traits可以萃取出类型的destructor特性(trivial or non-trivial),然后通过函数重载来决定具体进行什么样的操作。

The implicitly-declared destructor for class T is trivial if all of the following is true:
The destructor is not virtual (that is, the base class destructor is not virtual)
All direct base classes have trivial destructors
All non-static data members of class type (or array of class type) have trivial destructors
A trivial destructor is a destructor that performs no action. Objects with trivial destructors
don't require a delete-expression and may be disposed of by simply deallocating their storage.
All data types compatible with the C language (POD types) are trivially destructible.


3. 两级空间配置器

SGI STL提供两级空间配置器,第一级空间配置器使用malloc/free函数,当分配的空间大小超过128 bytes的时候使用第一级空间配置器;第二级空间配置器使用了内存池技术,当分配的空间大小小雨128 bytes的时候,将使用第二级空间配置器。

大量分配小块的内存空间会带来问题:一是从运行库的堆管理器中取得的内存(比如通过malloc获得的内存),会有一部分空间用于存储管理信息,用于管理各个内存块,这样内存的使用率就降低了;二是过多的小块内存会带来内存碎片问题;采用合适的内存池技术可以避免这些问题。

SGI STL的第二级内存配置器维护了一个free-list数组,分别用于管理8,16,24,32,40,48,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的倍数。

enum {_ALIGN = 8};
enum {_MAX_BYTES = 128};
enum {_NFREELISTS = 16}; // _MAX_BYTES/_ALIGN
// 将待分配的空间大小向上调整为8的倍数
static size_t
_S_round_up(size_t __bytes)
{ return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1)); }

__PRIVATE:
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1];    /* The client sees this.        */
};
private:
// 定义free_list数组
static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS];
// 根据空间大小取得free_list数组的对应下标
static  size_t _S_freelist_index(size_t __bytes) {
return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1);
}


空间的分配:就是从对应的free-list节点链表中取出一个节点返回给用户,当然,如果没有可用的节点的话就要通过refill来分配新的节点了,后面会有描述:
static void* allocate(size_t __n)
{
void* __ret = 0;
// 如果大于128 bytes,则使用第一级空间配置器
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n);
}
else {
// 通过大小取得free-list数组下标,随后取得对应节点的指针
// 相当于&_S_free_list[_S_freelist_index(__n)]
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
_Obj* __RESTRICT __result = *__my_free_list;
// 如果没有可用的节点,则通过_S_refill分配新的节点
if (__result == 0)
__ret = _S_refill(_S_round_up(__n));
else {
// 将当前节点移除,并当做结果返回给用户使用
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
}
}

return __ret;
};


从free-list中摘取节点返回给用户使用的示意图如下:(配图来自《STL源码剖析》一书)



而空间的回收,则是把内存重新加入到free-list对应的节点链表上去。

那么,当对应的free-list链表中没有可用节点的时候,refill进行了怎样的操作呢?默认操作时通过_S_chunk_alloc从内存池中取得20个新的节点添加到free-list链表中,当然,内存池中的内存不够用也是会出现的情况之一,这时候能分多少就分多少节点,再万一内存池一个节点都提供不了了,那就内存池需要新增空间了,如果失败,再抛出bad_alloc异常。

template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20;
// 通过内存池分配内存,第二个参数为传引用方式
char* __chunk = _S_chunk_alloc(__n, __nobjs);
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
// 如果只分配了一个节点,那么直接返回给用户就是了
if (1 == __nobjs) return(__chunk);
// 如果分配了不止一个节点,那么多余的我们要放到free-list里面去
__my_free_list = _S_free_list + _S_freelist_index(__n);

/* Build free list in chunk */
__result = (_Obj*)__chunk;
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
for (__i = 1; ; __i++) {
__current_obj = __next_obj;
__next_obj = (_Obj*)((char*)__next_obj + __n);
if (__nobjs - 1 == __i) {
// 最后一个节点的_M_free_list_link指针指向NULL,并跳出循环
__current_obj -> _M_free_list_link = 0;
break;
} else {
__current_obj -> _M_free_list_link = __next_obj;
}
}
return(__result);
}


4. 内存池

通过_S_chunk_alloc,从内存池中分配空间给free-list:

template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size,
int& __nobjs)
{
char* __result;
// 需要分配的空间大小
size_t __total_bytes = __size * __nobjs;
// 内存池中剩余的空间大小
size_t __bytes_left = _S_end_free - _S_start_free;

// 如果剩余大小满足要求,那么直接操作对应的指针即可
if (__bytes_left >= __total_bytes) {
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
}
// 剩余大小不够,但是至少还能分配一个节点
else if (__bytes_left >= __size) {
// 能够分配的节点数
__nobjs = (int)(__bytes_left/__size);
__total_bytes = __size * __nobjs;
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
}
// 一个节点的空间都不够了
else {
// 新申请的空间为2倍大小
size_t __bytes_to_get =
2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
// Try to make use of the left-over piece.
// 如果还有剩余的空间,加入对应的free-list节点的链表
if (__bytes_left > 0) {
_Obj* __STL_VOLATILE* __my_free_list =
_S_free_list + _S_freelist_index(__bytes_left);

((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
*__my_free_list = (_Obj*)_S_start_free;
}
// 分配新的空间
_S_start_free = (char*)malloc(__bytes_to_get);
// 如果操作失败
if (0 == _S_start_free) {
size_t __i;
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __p;
// Try to make do with what we have.  That can't
// hurt.  We do not try smaller requests, since that tends
// to result in disaster on multi-process machines.
// 看看free-list数组中,是否有更大尺寸的可用节点
for (__i = __size;
__i <= (size_t) _MAX_BYTES;
__i += (size_t) _ALIGN) {
__my_free_list = _S_free_list + _S_freelist_index(__i);
__p = *__my_free_list;
// 如果有可用节点则摘一个下来给内存池使用
if (0 != __p) {
*__my_free_list = __p -> _M_free_list_link;
_S_start_free = (char*)__p;
_S_end_free = _S_start_free + __i;
// 递归调用自身,修正__nobjs
return(_S_chunk_alloc(__size, __nobjs));
// Any leftover piece will eventually make it to the
// right free list.
}
}
// 如果没有可用节点,则只能指望第一级空间配置器了
_S_end_free = 0;	// In case of exception.
_S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
// This should either throw an
// exception or remedy the situation.  Thus we assume it
// succeeded.
}
_S_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;
// 递归调用自身,修正__nobjs
return(_S_chunk_alloc(__size, __nobjs));
}
}


_S_chunk_alloc的流程总结如下:

1. 内存池有足够大小的空间,则分配申请的空间;

2. 内存池没有足够大小的空间,但是至少还能分配一个节点的空间,则能分多少分多少;

3. 内存池一个节点都腾不出来了,向系统的heap申请2倍于要求大小的空间,在此之间,如果内存池剩余有空间,则放到free-list中去;

4. 如果向heap申请空间失败,那么只能看free-list中更大的节点是否有可用空间了,有则用之,同时递归调用自身修正__nobjs;

5. 如果free-list也没有可用节点了,那么转向第一级空间配置器申请空间;

6. 再不行,第一级空间配置器就要抛出bad_alloc异常了;

注意如果有需求的话,内存池中会不断的通过malloc申请新的内存,最后内存池所拥有的内存也会越来越大,当然最后进程结束的时候,这些内存都会由操作系统收回。

5. 内存基本处理工具

STL提供了五个全局函数用于处理空间,分别为:

1. construct 用于构造;

2. destroy 用于析构;

3. uninitialized_copy(first, last, result) 将[first,last)范围内的对象复制到result处;

4. uninitiated_fill(first, last, X) 将[first,last)范围内的内存用对象X的副本填充;

5. uninitiated_fill_n(first, n, X) 将first开始的n个连续的内存空间用X的副本填充;

前面提到对于destroy的实现,如果对象的析构函数是trivial的,那么什么都不用做,同样的,对于uninitialized_copy和uninitiated_fill / uninitiated_fill_n,如果对象时POD类型,那么可以直接通过复制内存的方式来实现,对于普通的POD类型,通过上层的copy函数来实现复制填充,对于char*/wchar_t*,则提供对应的特化版本,通过memmove实现(和memcpy相比,memmove支持重叠内存操作);如果不是POD类型,那么就只能通过construct实现了。

关于POD类型:

可见,POD类类型就是指class、struct、union,且不具有用户定义的构造函数、析构函数、拷贝算子、赋值算子;不具有继承关系,
因此没有基类;不具有虚函数,所以就没有虚表;非静态数据成员没有私有或保护属性的、没有引用类型的、没有非POD类类型的
(即嵌套类都必须是POD)、没有指针到成员类型的(因为这个类型内含了this指针)。


简单的说直接的内存复制操作对POD类型没有影响,比如用memset进行初始化,但这对于非POD类型是不可行的,比如存在虚函数的情况下。

判断一个类型是否是POD类型,也是通过__type_traits萃取出来的。

// POD类型,通过高层的copy实现
template <class _InputIter, class _ForwardIter>
inline _ForwardIter
__uninitialized_copy_aux(_InputIter __first, _InputIter __last,
_ForwardIter __result,
__true_type)
{
return copy(__first, __last, __result);
}

// 非POD类型,通过调用_Construct进行构造对象
template <class _InputIter, class _ForwardIter>
_ForwardIter
__uninitialized_copy_aux(_InputIter __first, _InputIter __last,
_ForwardIter __result,
__false_type)
{
_ForwardIter __cur = __result;
__STL_TRY {
for ( ; __first != __last; ++__first, ++__cur)
_Construct(&*__cur, *__first);
return __cur;
}
__STL_UNWIND(_Destroy(__result, __cur));
}

// 中间函数,通过__type_traits<_Tp>::is_POD_type萃取出POD类型
template <class _InputIter, class _ForwardIter, class _Tp>
inline _ForwardIter
__uninitialized_copy(_InputIter __first, _InputIter __last,
_ForwardIter __result, _Tp*)
{
typedef typename __type_traits<_Tp>::is_POD_type _Is_POD;
return __uninitialized_copy_aux(__first, __last, __result, _Is_POD());
}

// 用户接口函数
template <class _InputIter, class _ForwardIter>
inline _ForwardIter
uninitialized_copy(_InputIter __first, _InputIter __last,
_ForwardIter __result)
{
return __uninitialized_copy(__first, __last, __result,
__VALUE_TYPE(__result));
}

// char* 特化版本
inline char* uninitialized_copy(const char* __first, const char* __last,
char* __result) {
memmove(__result, __first, __last - __first);
return __result + (__last - __first);
}

// wchar_t* 特化版本
inline wchar_t*
uninitialized_copy(const wchar_t* __first, const wchar_t* __last,
wchar_t* __result)
{
memmove(__result, __first, sizeof(wchar_t) * (__last - __first));
return __result + (__last - __first);
}


具体的实现方法的选择示意图如下:



uninitiated_copy实现版本选择

参考

std::set_new_handler http://www.cplusplus.com/reference/new/set_new_handler/

trivial destructor http://en.cppreference.com/w/cpp/language/destructor

POD http://zh.wikipedia.org/wiki/POD_(%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1)

原文链接:http://www.programlife.net/stl-allocator.html,作者:代码疯子
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: