《STL源码剖析》学习笔记之二 空间配置器

1.   空间配置器

为何称为空间配置器而不是内存配置器呢?因为空间不一定是内存,空间也可以是磁盘或其它辅助存储介质,这意味着你可以写一个allocator,直接向磁盘取空间。

1.1.       空间配置器的标准接口

 

1.1.1.      设计一个简单的空间配置器

 

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

SGI STL的配置器与标准规范不同,其名称是alloc而非allocator,且不接受任何参数,即写法只能如下:

vector<int, std::alloc> iv;

而不能写成如下:

vector<int, std::alloc<int> > iv;

1.2.1.      SGI标准的空间配置器,std::allocator

SGI认为标准空间配置器仅仅只是operator newoperator delete的封装,毫无效率,建议不要使用,事实上,SGI STL根本没有用标准的空间配置器。

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

标准的空间配置器std::allocator将内存分配/释放和构造/析构对象分开(allocate/deallocate用于内存分配/释放,construct/destroy用于构造/析构对象),SGI特殊的空间配置器std::alloc仅仅完成内存分配/释放。

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

相比标准的destroy()SGIdestroy在多个元素destroy时,效率上具有优势。其定义如下:

// 如果元素的数值类型有non-trivial destructor

template<class ForwardIterator>

inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type)

{

       for(; first<last; ++first)

              destroy(&*first);

}

 

// 如果元素的数值类型有trivial destructor

template<class ForwardIterator>

inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __true_type)

{

}

 

template<class ForwardIterator, class T>

inline void __destroy(ForwardIterator first, ForwardIterator last, T*)

{

       typedef typename __type_traits<T>::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));

}

这里有必要解释一下trivial destructor,如果一个析构函数不需要做一些释放资源的事情,可以认为是trivial destructorSGIdestroy依次释放某个容器中的元素,当元素析构函数没有做什么事情时不调用它,以提高效率。这又是一次traits机制和重载机制的成功运用。

1.2.4.      空间的配置和释放,std::alloc

SGI对于std::alloc的设计哲学如下:

1)        system heap要求空间;

2)        考虑多线程状态;

3)        考虑内存不足时的应变措施;

4)        考虑过多“小型区块”可能造成的内存碎片(fragment)问题。

考虑“小型区块”可能造成的内存破碎问题,SGI设计了两级配置器,第一级直接使用mallocfree;第二级则视情况采用不同的策略:当配置 超过128bytes时,视为“足够大”,调用第一级配置器;当配置区块小于128bytes时,视为“过小”,采用复杂的memory pool整理方式(即从memory pool寻求空间)。

实际运用中采用simple_allocalloc进行再次封装,而alloc究竟是第一级配置器malloc_alloc<0>还是第二级配置器__default_alloc_template<0,0>取决于__USE_MALLOC是否被定义,如下:

#ifdef __USE_MALLOC

...

typedef __malloc_alloc_template<0> malloc_alloc;

typedef malloc_alloc alloc;

#else

...

// alloc为第二级配置器

typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;

#endif /*!__USE_MALLOC*/

1.2.5.      第一级配置器__malloc_alloc_template剖析

第一级配置器采用mallocrealloc分配内存,当存在空间不足时调用oom_mallocoom_realloc处理,两者循环调用oom_malloc_handleroom_realloc_handler来释放内存并再次调用mallocrealloc分配内存。oom_malloc_handleroom_realloc_handler由调用者通过调用以下函数指定:

static void (* set_malloc_handler (void (*f)()) ) () // 该函数头声明十分怪异?

// 函数头描述一个标签为set_malloc_handler的函数,形参为void (*)(),返回值为void(*)()

{

       void (*old)() = __malloc_alloc_oom_handler;

       __malloc_alloc_oom_handler = f;

       return (old);

}

这里遵循“内存不足处理例程”解决问题做法的特定模式——[Meyers98]条款7

1.2.6.      第二级配置器__default_alloc_template剖析

小额区块带来的不仅是内存碎片,配置时的额外开销(overhead)也是一个大问题(例如系统需要额外的空间记录内存大小等信息)。

第二级配置器采用16个自由链表free-lists来管理小额区块,每个free-list管理的区块大小相同,不同的free-list区块大小不同,区块大小按8字节进行对齐,即区块大小从小到大分别为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,12816种大小,当客户申请的空间区块不等于其中一种时向上取整。

free-lists采用一种巧妙的技术避免链表节点的额外开销(指向下一个节点的指针),思想是将未分配的区块(自由区块free-block)的第一个4个字节作为指向下一个free-block的指针,其节点定义如下:

union fl_node

{

       union fl_node * pNext;

       char data[1]; // 利用C++不禁止越界访问数组的特性访问整个空间区块

};

该定义对某个区块(未分配时是free-block)的头4个字节数据进行解释:在某种情况(未分配时)下是指向下一个free-block;在另一种情况(已分配)下是一个数组,数组的每个元素是char,数组名为data,数组长度为1

1.2.7.      空间配置器allocate

当申请的区块大于128byte时,调用第一级配置器;否则从free-lists中寻找适当的一个自由链表,取出链表头;没有合适的时,重新申请空间。

1.2.8.      空间释放函数deallocate

当释放的区块大于128byte时,调用第一级配置器;否则插入到合适的自由链表头。释放时,一定得知道释放的区块大小!

1.2.9.      重新填充free lists

free list中没有可用区块时,调用refill(),为free list重新填充空间。新的空间取自内存池,缺省取得20个新区块(连续空间,长度=20*区块大小),但万一内存池空间不足,获得区块数小于20

1.2.10.  内存池(memory pool

内存池通常是一块在堆中申请的连续空间,且是8的整数倍。chunk_alloc实现从内存池获取n个被申请区块给free lists,其中一个交出,n-1个给free listschunk_alloc设计遵循以下原则:

1)        尽力分配n个被申请区块;

2)        当内存池的空间不到n个被申请区块时,有多少就分配多少;

3)        当内存池的空间不到一个被申请区块时,将剩余空间送入到free lists中,用malloc从堆中申请空间注入到内存池中后再分配;

4)        当用malloc从堆中申请空间失败后,从free lists中取出一块比被申请区块大的自由区块注回内存池后再分配;

5)        free lists中也没有比被申请区块更大的自由区块时,调用第一级空间配置器的out-of-memory机制申请空间注入到内存池后再分配。

1.3.       内存基本处理工具

1.3.1.      uninitialized_copy

uninitialized_copy依次对范围内每个元素调用construct,从而构造整个范围内的全部元素。C++标准规格书要求其具有“commit or rollback”语意。

1.3.2.      uninitialized_fill

uninitialized_copy的区别是前者的源元素时多个,而后者是单个。

1.3.3.      uninitialized_fill_n

uninitialized_fill类似,只不过指定目标范围的方式不同。

PODPlain Old Data),即标量类型或传统的C Struct类型,POD类型必须拥有trivial ctor/dtor/copy/assignment函数,即某种类型的数据能简单按位拷贝完成复制,则其类型就是PODuninitialized_fill_n利用traits机制和function template参数推导机制完成对PODnon-POD分别fill(前者直接按位拷贝,后者间接调用copy-constructor)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值