空间适配器
自定义一个allocate
#ifndef JJALLOC_H_INCLUDED #define JJALLOC_H_INCLUDED #include<new> #include<cstddef>//定义了一些标准宏及类型。 #include<cstdlib>//常用函数库 #include<climits>//定义数据类型的最大最小值 #include<iostream>//输入输出流 namespace JJ { //配置模板类空间的函数,传入数据大小,申请内存 template<class T> inline T* _allocate(ptrdiff_t size,T*) //ptrdiff_t是C/C++标准库中定义的一个与机器相关的数据类型。ptrdiff_t类型变量通常用来保存两个指针减法操作的结果。 { std::set_new_handler(nullptr); //set_new_handler(0)主要是为了卸载目前的内存分配异常处理函数,这样一来一旦分配内存失败的话,C++就会强制性抛出std:bad_alloc异常,而不是跑到处理某个异常处理函数去处理 T *tmp=(T*)(::operator new((size_t)(size *sizeof(T)))); //::operator new()这是一个重载函数,完成的操作一般只是分配内存,事实上系统默认的全局::operator new(size_t size)也只是调用malloc分配内存,并且返回一个void*指针。而构造函数的调用(如果需要)是在new运算符中完成的。 if(tmp==nullptr) { std::cerr<<"内存超出"<<std::endl; exit(1); } return tmp; } //归还先前配置的空间 template<class T> inline void _deallocate(T* buffer) { ::operator delete(buffer); //::operator delete()与operator new相反,释放内存。 } //通过传递目标地址,和目的内容,构造出目标对象。 template<class T1,class T2> inline void _construct(T1 *p,const T2& value) { new(p) T1(value); //placement new,在指针p所指向的内存空间创建一个类型为T1的对象。 } //摧毁:调用析构函数 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; template<class U> struct rebind { //T这个类里,可以存储着U类对象 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 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 // JJALLOC_H_INCLUDED
SGI标准的空间适配器
在<memory>包含以下两个头文件
#include<stl_alloc.h>//负责内存的配置和释放 #include<stl_construct.h>//负责对象的构造和析构
它们的调用关系是
构造和析构工具:constructor、destructor
#ifndef __SGI_STL_INTERNAL_CONSTRUCT_H #define __SGI_STL_INTERNAL_CONSTRUCT_H #include <new.h> __STL_BEGIN_NAMESPACE template <class T>//析构单个元素 inline void destroy(T* pointer) { pointer->~T(); } template <class T1, class T2> inline void construct(T1* p, const T2& value) { new (p) T1(value); //布局new(placement new) 在p地址处调用T1构造函数构造对象 } template <class ForwardIterator> inline 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) {} template <class ForwardIterator, class T> inline void __destroy(ForwardIterator first, ForwardIterator last, T*) { //通过元素型别来判断析构函数是否无关紧要(trivial) 并调用对应的函数进行析构 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));//通过泛型的类型识别技术来得到元素类型 } inline void destroy(char*, char*) {} inline void destroy(wchar_t*, wchar_t*) {} __STL_END_NAMESPACE #endif /* __SGI_STL_INTERNAL_CONSTRUCT_H */
constructor接受一个指针和一个初始值value。
destroy有两个版本:
- 接受一个指针,将指针所指之物析构掉,直接调用析构函数即可。
- 接受两个迭代器,因为不确定迭代器的范围,所以为了节约开销,先用value_type()获取迭代器所指对象的型别,再用__type_traits<T>判断该型别是否是无用的,如是则就什么也不做,否则访问每一个对象对其调用析构函数。
以上描述的是内存购置后对象的初始化与释放行为。
空间的配置与释放
- 向system heap要求空间
- 考虑多线程状态
- 考虑内存不足时的应变措施
- 考虑内存碎片问题
SGI采用双层配置器,第一级配置器直接采用malloc和free;第二级配置器视情况不同采用不同策略,当配置区块超过128B时,调用一级配置器,小于时用内存池。设计是否同时开启两级配置器取决于_USE_MALLC的定义。
第一级适配器__malloc_alloc_tmplate
#if 0 #include <new> #define __THROW_BAD_ALLOC throw bad_alloc #elif !defined(__THROW_BAD_ALLOC) #include <iostream> #include <stdlib.h> #define __THROW_BAD_ALLOC std::cerr<<"out of memory"<<std::endl; exit(1) #endif //第一级空间配置器 template<int inst> class __malloc_alloc_template { private: //处理内存不足的情况 //oom: out of memory //当内存不足时,申请内存 static void *oom_malloc(size_t); //当内存不足时,重新规划内存 static void *oom_realloc(void *, size_t); //oom下尝试释放内存的例程 static void (* __malloc_alloc_oom_handler)(); public: //配置内存 static void * allocate(size_t n) { //直接调用malloc void *result = malloc(n); //第一级配置器直接使用malloc() //无法满足需求时,直接使用oom_malloc() if(0 == result) result = oom_malloc(n); return result; } //释放内存 static void deallocate(void *p, size_t ) { //这里直接调用free free(p); } //重新规划内存 static void *reallocate(void *p, size_t , size_t new_sz) { //直接调用realloc void * result = realloc(p, new_sz); //无法满足需求时,直接使用oom_realloc() if(0 == result) result = oom_realloc(p, new_sz); return result; } //模仿std::set_new_handler();可以在这里定义自己oom句柄 static void (*set_malloc_handler(void (*f)()))()//函数返回值为函数指针,参数是void (*f)(),即函数指针 { void (*old)() = __malloc_alloc_oom_handler; __malloc_alloc_oom_handler = f; return (old); } }; //malloc_alloc out_of_memory handling //将指针指向的地址为0 template<int inst> void (*__malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0; //oom下申请内存 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)();//企图释放内存 result = malloc(n); if(result) return result; } } //oom下重新规划内存 template<int inst> void * __malloc_alloc_template<inst>::oom_realloc(void *p, size_t n) { void (*my_malloc_handler)(); void *result; //与oom_malloc类似,不断尝试释放,配置,再释放,再配置 for(;;) { my_malloc_handler == __malloc_alloc_oom_handler; if(0 == my_malloc_handler) { __THROW_BAD_ALLOC; } (*my_malloc_handler)();//企图释放内存 result = realloc(p, n); if(result) return result; } } //这里直接将inst指定为0了 typedef __malloc_alloc_template<0> malloc_alloc;
第一级适配器使用malloc,realloc,free来分配内存,当系统内存无法被满足时,调用类似于c++ new handler来分配内存,也就是::operator new无法分配内存时,在丢出std::bad_malloc之前调用new headler,如果客户端未设定new-handler,则会直接抛异常退出程序。
第二级适配器__default_alloc_template
二级配置器维护着16个自由链表,他们维护的内存大小分别是8,16,24到128,其结构如下
union obj { union obj* free_list_link; char client_data[1]; };
由于union的缘故,obj可视为一个指针,指向与第一个形式相同的obj,从第二个字段看,obj也可视为一个实际所指的内存区,如此可节省存储额外空间的指针。
template <bool threads, int inst> class __default_alloc_template { private: //区块上调枚举 #if ! (defined(__SUNPRO_CC) || defined(__GNUC__)) enum {_ALIGN = 8}; enum {_MAX_BYTES = 128}; enum {_NFREELISTS = 16}; // _MAX_BYTES/_ALIGN # endif private: //区块上调函数,接收到申请的大小,我们向上取8的倍数 static size_t 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: # if defined(__SUNPRO_CC) || defined(__GNUC__) || defined(__HP_aCC) static _Obj* __STL_VOLATILE _S_free_list[]; # else //16个 free-list static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS]; # endif //根据申请的大小,确定出在自由链表的下标 static size_t FREELIST_INDEX(size_t __bytes) { return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1); } //返回大小为__n的对象 static void* refill(size_t __n); //配置一大块空间,可以容纳__nobjs大小个__size的区块 //可能会随着内存不足,而降低__nobjs数量 static char* chunk_alloc(size_t __size, int& __nobjs); static char* start_free; //内存池起始 static char* end_free; //内存池结束 static size_t heap_size; //区块数 public: /* __n must be > 0 */ static void* allocate(size_t __n) { ... }; /* __p may not be 0 */ static void deallocate(void* __p, size_t __n) { ... } static void* reallocate(void* __p, size_t __old_sz, size_t __new_sz); }; //这四个函数是静态数据成员的定义与初始设定 //__threads是线程设定,书中不讨论 template <bool __threads, int __inst> char* __default_alloc_template<__threads, __inst>::_S_start_free = 0; template <bool __threads, int __inst> char* __default_alloc_template<__threads, __inst>::_S_end_free = 0; template <bool __threads, int __inst> size_t __default_alloc_template<__threads, __inst>::_S_heap_size = 0; template <bool __threads, int __inst> typename __default_alloc_template<__threads, __inst>::_Obj* __STL_VOLATILE __default_alloc_template<__threads, __inst> ::_S_free_list[ # if defined(__SUNPRO_CC) || defined(__GNUC__) || defined(__HP_aCC) _NFREELISTS # else __default_alloc_template<__threads, __inst>::_NFREELISTS # endif ] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; };
空间配置函数allocate
作为一个空间配置器,自然拥有配置器的标准接口函数allocate()
,此函数首先判断区块大小,大于128字节就调用一级配置器,小于128字节就检查对应的自由链表,如果对应链表可用,那么久拿来用,反之,将区块大小上调至8倍数边界,然后调用refill()
,重新填充空间。
static void * allocate(size_t n) { obj *volatile * my_free_list; obj *result; //如果申请大小n大于128,直接调用一级配置器 if(n>(size_t)__MAX_BYTES) { //调用一级配置器的空间配置函数。 return (malloc_alloc::allocate(n)); } //通过大小确定自由链表的对应下标 my_free_list=free_list+FREELIST_INDEX(n); result=*my_free_list; //如果返回为空 if(result==0) { void *r =refill(ROUND_UP(n)); return r; } //对自由链表进行调整 *my_free_list=result->free_list_link; return (result); }
空间释放函数deallocate
作为一个空间配置器,自然拥有配置器的标准接口函数deallocate()
,此函数首先判断区块大小,大于128字节就调用一级配置器,小于128字节就检查对应的自由链表,回收空间。
static void * deallocate(void *p,size_t n) { obj *q=(obj *)p; obj * volatile * my_free_list; //如果申请大小n大于128,直接调用一级配置器 if(n>(size_t)__MAX_BYTES) { //调用一级配置器的空间配置函数。 malloc_alloc::deallocate(p,n); return ; } //通过大小确定自由链表的对应下标 my_free_list=free_list+FREELIST_INDEX(n); //回收 q->free_list_link=*my_free_list; *my_free_list=q; }
重新填充free lists
回头讨论先前说过的allocate()。当它发现自由链表中没有可用区块时,会调用refill它会将chunk_alloc输出的空间维护成自由链表。缺省取得20个新区块,如果内存池空间不足,获得区块数可能小于20.
template <bool threads,int inst> void * __default_alloc_template<threads,inst>::refill(size_t n) { int nobjs=20; //调用chunk_alloc(),尝试取得nobjs个区块作为自由链表的新节点。 //传参nobjs是个引用 char *chunk=chunk_alloc(n,nobjs); //自由链表 obj *volatile * my_free_list; obj *result; //用于连接的前后节点 obj * current_obj,*next_obj; int i; //如果当前需求只有一个,我们直接返回给调用者,不用添加到自由链表里 if(nobjs==1) return (chunk); //取得目的自由链表 my_free_list=free_list+FREELIST_INDEX(n); //显示转换,将result指向新的来的区块头部,后面返回给调用者 result=(obj *)chunk; //chunk+n 是指chunk后移n位,就是下一块区块 *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) { //把尾部指向0/nullptr current_obj->free_list_link=0; break; } else { current_onj->free_list_link=next_obj; } } return(result); }
内存池
从内存池中取出空间给free lists,主要是chunk_alloc,它是向堆申请空间给内存池。
template <bool threads,int inst> char * __default_alloc_template<threads,inst>::chunk_alloc(size_t size,int & nobjs) { char * result; //需求的内存大小 size_t total_bytes=size*nobjs; //计算内存剩余空间 size_t bytes_left=end_free-start_free; //容量足够 if(bytes_left >= total_bytes) { result=start_free; //将剩余的开始位置偏移调整 start_free+=total_bytes; return(result); } //容量不足以全部产出,但是还可以‘挤’出几块块 else if (bytes_left >= size) { //计算还可以‘挤’出几块 nobjs=bytes_left/size; //新的需求大小 total_bytes=size*nobjs; result=start_free; //将剩余的开始位置偏移调整 start_free+=total_bytes; return(result); } //一块也‘挤’不出 else { //这里获得要使用malloc申请的内存大小,建议申请的大小是当前需求的两倍加上ROUND_UP(heap_size>>4),不过目前无法理解右移4位的意思 size_t bytes_to_get=2*total_bytes+ROUND_UP(heap_size>>4); //如果剩余一丁点内存 if(bytes_left>0) { //用剩余内存大小去找到对应的自由链表 obj * volatile * my_free_list=free_list+FREELIST_INDEX(bytes_left); //数组对应自由链表头->剩余内存大小的块->原本的自由链表 ((obj *)start_free)->free_list_link=*my_free_list; *my_free_list=(obj*)start_free; } //使用malloc申请大小 start_free=(char *)malloc(bytes_to_get); //申请失败 if(start_free==0) { int i; obj * volatile * my_free_list,*p; //每次i+8 for (i = size;i <= (size_t) __MAX_BYTES;i += (size_t) __ALIGN) { //从下标0到15,检索所有自由链表 my_free_list = free_list + FREELIST_INDEX(i); p = *my_free_list; //如果某个链表还有区块 if (0 != p) { //我们取一块区块出来 *my_free_list = p -> free_list_link; //把这块当作新的内存池可用空间 start_free = (char*)p; end_free = start_free + i; //再调用自身,看看这个区块的内存是否足够 return(chunk_alloc(size, nobjs)); } } end_free = 0; //调用一级配置器,看看oom下是否可以尽力完成 start_free = (char*)malloc_alloc::allocate(bytes_to_get); } heap_size += bytes_to_get; end_free = start_free + bytes_to_get; //递归自己,修正nobjs return(chunk_alloc(size, nobjs)); } } }
通过end_free和start_free来判断内存池的容量是否充足,如果充足就把20个块分配出去,如果不够20个快的容量但是够一个以上的块的容量,就把能分配实际的块容量分配出,修改nobjs,如果一个块的容量都不够,就通过malloc向heap申请空间,申请的数量为需求量的两倍。
工作原理:
假设程序一开始,客端就调用chunk_alloc(32,20)
,对应的free_list[3]空空如也,此时内存池为空,于是malloc()
配置40个32字节的区块。并把第一个交给调用者,剩下19个交给free_list[3]维护,剩下20个交给内存池。客端接着调用chunk_alloc(64,20)
,对应的free_list[7]空空如也,内存池是20*32字节大小的连续内存,可以分成10块64字节大小的区块,把第一块返回给调用者,剩下9块加入到对应的自由链表free_list[7],此时内存池为空。接下来调用chunk_alloc(96,20),free_list[11]为空,必须要求内存池支持,于是malloc配置40+n(附加量)个96B的区块,交出第一个,剩下19个给free_list,剩下20个给内存池。
若果system heap无可用空间,chunk_alloc就找free_list中尚未使用的且足够大的块,找到就挖出该块,找不到就调用一级配置器,一级配置器也是调用malloc,但是他有new-handler机制,可能释放其他空间来满足需要。
总的来讲:
分配内存的空间如果大于128B,就调用一级适配器;否则调用二级适配器,第一次调用二级适配器时内存池容量为空,会调用malloc申请需求2倍的空间,其中一半留给内存池下次分配使用,在此分配时,如果调整后对应的free_list为空则就相内存池申请,如果向内存池申请(malloc)失败则会扫描free_list寻找未用的空闲大块,若找到分配,否则调用一级适配器malloc,但是一级适配器有new-handler机制可能会释放其他空间来满足申请。
基本内存处理工具
uninitialized_copy,uninitialized_fill,uninitialized_fill_n都是在配置的区块上构造元素,他们都有一个共同的语意:要么构造成所有元素要么一个元素都不构造。如果其中一个复制构造函数抛异常那么会释放其他所有已构造元素。
两种特化char*和wchar_t*才用移动赋值memmove来复制。