1.简介
一般容器都需要一定空间存放数据,allocator就是用来配置空间的,SGI的allocator配置的对象时内存。
一个allocator通常包含两个部分,一是内存配置和内存释放(allocate的deallocate),二是对象构造和析构(construct和destory)。
2.一个简单的空间配置器
#ifndef _JJALLOC_
#define _JJALLOC_
#include<new>//for placement new
#include<cstdlib>//for ptrdiff_t,size_t
#include<climits>//for UINT_MAX
#include<iostream>//for cerr
namespace JJ
{
template<class T>
inline T* _allocate(ptrdiff_t size,T*){
//当operator new申请一个内存失败的时候.
//如果存在客户指定的处理函数,则调用处理函数(new_handler),如果不存在则抛出一个异常
//参数为0,则抛出异常
set_new_handler(0);
T* tmp=(T*)(::operator new((size_t)(size*sizeof(T))));
if(tmp==0){
cerr<<"out of memory"<<endl;
exit(1);
}
return tmp;
}
template<class T>
inline void _deallocate(T* buffer){
::operator delete(buffer);
}
template<class T1,class T2>
inline void _construct(T1*p,const T2& value){
new(p) T1(value);
}
template<class T>
inline void _destroy(T* ptr){
ptr->~T();
}
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;
//rebind allocator of type U
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););
};
};
} //end of namespace JJ
#endif _JJALLOC_
重点是JJ空间中四个模板函数:
- __allocate函数模板采用了operator new来配置内存,即分配一块未构造的、原始的内存空间。
- __deallocate利用operator delete释放上面分配的内存。
- __construct采用定位new在给定的内存地址上构造对象。
- __destory执行对象的析构动作。
四个部分构成了空间配置器的基础。
3.SGI空间配置器
SGI提供了两个空间配置器,其中标准的allocator与上面提供的简单空间配置器类似,效率并不高,也不推荐使用。
下面介绍SGI特殊空间配置器
3.1 SGI特殊空间配置器
SGI特殊的空间配置器称为alloc,alloc也是由之前讲述的两部分构成:一是内存配置和内存释放(allocate的deallocate),二是对象构造和析构(construct和destory)。
3.1.1构造和析构
以下源码在 <stl_construct.h>。
构造工具和之前介绍的简单空间配置器并无区别,主要区别在析构操作上,提供了两个个版本的析构工具,一个是销毁单个对象,另外一个是销毁多个:
template <class _Tp>
inline void _Destroy(_Tp* __pointer) {
__pointer->~_Tp();
}
template <class _ForwardIterator>
inline void _Destroy(_ForwardIterator __first, _ForwardIterator __last) {
__destroy(__first, __last, __VALUE_TYPE(__first));
}
第二个版本通过 __VALUE_TYPE判断是否有对象是否有trivial destructor(无关痛痒的析构函数),然后调用不同的__destroy_aux,如果有执行它的析构函数(_Destroy(&*__first)),如果没有就什么也不做,因此提高了效率:
template <class _ForwardIterator>
void
__destroy_aux(_ForwardIterator __first, _ForwardIterator __last, __false_type)
{
for ( ; __first != __last; ++__first)
_Destroy(&*__first);
}
template <class _ForwardIterator>
inline void __destroy_aux(_ForwardIterator, _ForwardIterator, __true_type) {}
关于如何获得对象类型以及如何判断是否有trivial destructor,下一篇博文在介绍。
3.1.2空间的配置和释放
以下源码在 <stl_alloc.h>
SGI在空间配置上考虑了以下问题:
- 向system heap申请空间
- 考虑多线程的影响
- 考虑内存不足的影响
- 考虑过多小型区块可能造成的内存碎片问题
c++的内存配置基本操作是:operator new和operator delete,其相当于C语言的malloc和free函数。
考虑内存碎片的问题,SGI配置器采用双层级配置器:
- 第一级采用malloc和free函数。
- 第二级视情况采用不同策略,当申请内存大于128kB时,直接采用第一级配置器;当小于时,采用内存池整理方式。
- 是否开放第二级取决于 __USE_MALLOC是否定义。
不论采用第一级还是第二级,SGI都为他们包装了一个接口以供使用(simple_alloc)。
第一级配置器
第一级配置器采用malloc、free、realloc等执行实际内存操作,而不是使用operator new,因此不能直接运用c++ new-handler机制(即如果operator new无法完成操作,在抛出bad_alloc之前会先调用客户指定的处理程序,此程序称为new-handler)。需要自己实现new-handler机制:
static void (* set_malloc_handler(void (*f)()))()
{
void (* old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = f;
return(old);
}
template <int inst>
void * __malloc_alloc_template<inst>::oom_malloc(size_t n)
{
void (* my_malloc_handler)();
void *result;
for (;;) {
my_malloc_handler = __malloc_alloc_oom_handler;
if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }
(*my_malloc_handler)(); //调用new-handler
result = malloc(n);
if (result) return(result);
}
}
如果malloc不成功,会改用oom_malloc不断申请,如果客户端未设置new-handler,则抛出异常。
第二级配置器
第二级配置器维护16个free-lists,分别管理8、16、24、32…120、128大小的区块,客户端配置内存时过程如下:
- 先从free-list中申请满足大小要求并且最小的区块,申请成功就返回给客户端并且该free-list的指针指向下一个区块。
- 如果在该free-list中没有申请到,就调用refill从内存池中申请20区块大小填充该free-list。
内存池的操作如下:
3. 如果内存池水量充足,就调出20个区块返回。
4. 如果水量不足,但还够一个以上区块,就调出这些区块然后返回。
5. 如果水量连一个区块都不足以提供,就从系统heap申请内存。在申请前,先把内存池中的零头分配给free-list,比如内存池中还剩35字节,就分配给区块大小为32的free-list。
6. 从系统申请40+n的内存,然后将第一个区块交给客户端,将剩余19个区块交给free-list维护,剩下的20+n个留给内存池。
7. 如果整个系统heap都不够了,就查找是否还有足够大且没有使用的free-list,然后释放这些区块(大的放入内存池,小的零头被编入free-list,通过递归调用自己实现)。
参考:STL源码剖析