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 new和operator delete的封装,毫无效率,建议不要使用,事实上,SGI STL根本没有用标准的空间配置器。
1.2.2. SGI特殊的空间配置器std::alloc
标准的空间配置器std::allocator将内存分配/释放和构造/析构对象分开(allocate/deallocate用于内存分配/释放,construct/destroy用于构造/析构对象),SGI特殊的空间配置器std::alloc仅仅完成内存分配/释放。
1.2.3. 构造和析构基本工具:construct()和destroy()
相比标准的destroy(),SGI的destroy在多个元素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 destructor。SGI的destroy依次释放某个容器中的元素,当元素析构函数没有做什么事情时不调用它,以提高效率。这又是一次traits机制和重载机制的成功运用。
1.2.4. 空间的配置和释放,std::alloc
SGI对于std::alloc的设计哲学如下:
1) 向system heap要求空间;
2) 考虑多线程状态;
3) 考虑内存不足时的应变措施;
4) 考虑过多“小型区块”可能造成的内存碎片(fragment)问题。
考虑“小型区块”可能造成的内存破碎问题,SGI设计了两级配置器,第一级直接使用malloc和free;第二级则视情况采用不同的策略:当配置 超过128bytes时,视为“足够大”,调用第一级配置器;当配置区块小于128bytes时,视为“过小”,采用复杂的memory pool整理方式(即从memory pool寻求空间)。
实际运用中采用simple_alloc对alloc进行再次封装,而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剖析
第一级配置器采用malloc和realloc分配内存,当存在空间不足时调用oom_malloc和oom_realloc处理,两者循环调用oom_malloc_handler和oom_realloc_handler来释放内存并再次调用malloc和realloc分配内存。oom_malloc_handler和oom_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,128等16种大小,当客户申请的空间区块不等于其中一种时向上取整。
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 lists。chunk_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类似,只不过指定目标范围的方式不同。
POD(Plain Old Data),即标量类型或传统的C Struct类型,POD类型必须拥有trivial ctor/dtor/copy/assignment函数,即某种类型的数据能简单按位拷贝完成复制,则其类型就是POD。uninitialized_fill_n利用traits机制和function template参数推导机制完成对POD和non-POD分别fill(前者直接按位拷贝,后者间接调用copy-constructor)。