目录
1 JJ::allocator——STL源码解析
2 配置器std::alloc
2.1 二级配置器——__default_alloc_template
2.1.1 区块大小和空闲链块总数
2.1.2 union obj和free_list
2.1.3 从内存池获取区块放置free_list中
2.1.4 refill和chunk_alloc
2.1.5 allocate,deallocate,reallocate
3 全局函数construct,deconstruct
3.1 :stl_alloc.h,stl_construct.h
3.1.1 stl_alloc.h——配置器
3.1.2 stl_construct.h——construct()和destroy()
3.2 construct和destroy
3.2.1 construct
3.2.2 destroy
4 全局函数uninitialized_copy,uninitialized_fill,uninitialized_fill_n
4.1 uninitialized_fill_n
4.2 uninitialized_fill
4.3 uninitialized_copy
allocator | value_type |
pointer | |
const_pointer | |
reference | |
const_reference | |
size_type | |
difference_type | |
rebind |
allocator为什么需要这些成员函数,可以试着从new和delete的角度来分析
(1)new分配堆空间,需要size,type两个参数,即分配size大小的内存,类型为type。
(2)new分配完内存后,返回指向分配空间的首地址
(3)delete回收new分配的空间
为什么要实现这些必要的接口,除了从功能的角度,还可以从复用的角度来看。
标准就是为了复用,allocator是为上层容器提供服务的,所以它要稳定,一旦它不稳定,对于上层结构的冲击是巨大的——就像网络协议栈
稳定即意味着提供稳定不变的接口——不管内部实现是什么,要接口符合标准就行
空间配置器——只做空间分配和释放工作,不做初始化对象工作。(单一职责)
1 JJ::allocator——STL源码解析
#include <new> //for placement new
/*
1、ptrdiff_t,与机器相关的数据类型,通常用来保存两个指针减法操作的结果,可正可负,是long int类型
2、siz_t,用于指明数组长度,是unsigned乐星
*/
#include <cstddef> //for ptrdiff_t, size_t
#include <cstdlib> //for exit()
#include <climits> //for UINT_MAX
#include <iostream> //for cerr
using namespace std;
namespace JJ
{
template <typename T>
inline T* _allocate(ptrdiff_t size, T*){
set_new_handler(0); //当new分配空间失败时,不做任何处理
T* tmp = (T*)(::operator new((size_t)(size * sizeof(T)))); //封装了全局new操作符,而且使用operator()
if (tmp == 0){
cerr << "out of memory" << endl;
exit(1);
}
return tmp;
}
template <typename T>
inline void _deallocate(T* buffer){
::operator delete(buffer); //使用operator()释放空间
}
template <class T1, class T2>
inline void _construct(T1* p, const T2& value){
/*
定位new运算符——在为对象分配内存时,可以指定内存的位置,括号中放置指定位置的指针
*/
new(p) T1(value); //placement new. invoke ctor of T1——在p指向的内存空间,调用T1的构造函数,并使用T2类型参数作为构造参数
}
template <class T>
inline void _destroy(T* ptr){
ptr->~T();
}
template <class T>
class allocator{
public:
/*
这种定义方式似乎使得程序的可读性变高了
但是我在项目中曾经遇到一个BUG——比如,随着版本的迭代,value_type的本质变了,不再是T
所以有的时候使用T本身可以带来不必要的麻烦
*/
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));
}
};
}
#include <vector>
#include <algorithm>
using std::vector;
using namespace JJ;
int main(int argc, char** argv) {
int array[5] = {1, 2, 3, 4, 5};
unsigned int i;
vector<int, JJ::allocator<int> > v(array, array + 5);
for_each(v.begin(), v.end(), [](int i){ cout << i << endl;});
return 0;
}
2 配置器std::alloc
双层级配置器std::alloc,128bytes是阈值
1、typedef __malloc_alloc_template<0> malloc_alloc; typdef malloc_alloc alloc;——第一级配置器
当要求的内存大于128bytes时,使用该配置器。
成员函数allocate调用malloc分配内存,返回一个void *指针
成员函数deallocate调用free释放内存,返回void
2、typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;——第二级配置器
使用内存池,由free_lists管理16个节点,每个节点管理的区块大小分别为——8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128bytes
每个节点管理着该节点对应的区块大小的链表,如果需要分配的节点在128bytes范围内,则找到最匹配节点(不满8的倍数,按照8的倍数分配),分配给客户端。
SGI为alloc包装了一个接口,使得其符合STL规范template <class T, class Alloc> class simple_alloc{ public: static T *allocate(size_t); static T *allocate(void); static void deallocate(T*, size_t); static void deallocate(T*); }; //这种定义方式多用在容器中 typedef simple_alloc<T, alloc> data_allocator;
2.1 二级配置器——__default_alloc_template
源码——128bytes为一个门限,大于128bytes就调用第一级配置器#include <iostream> #include <new.h> using namespace std; enum { __ALIGN = 8 }; //小型区块的上调边界 enum { __MAX_BYTES = 128 }; //小型区块上限 enum { __NFREELISTS = __MAX_BYTES / __ALIGN }; //free-lists 个数 template <bool threads, int inst> class default_alloc_template{ private: static size_t ROUND_UP(size_t bytes){ //~(__ALIGN - 1)= 11111000 //与的过程中,把0~7的值全部消除,只留下了8的倍数 //而且 + (__ALIGN - 1)保证了返回值必然 >= bytes return (((bytes) + __ALIGN - 1) & ~(__ALIGN - 1)); } private: union obj{ union obj * free_list_link; //在64位Windows系统gcc编译器上,指针的大小为8字节 char client_data[1]; //这里的值是地址低字节的值 }; private: //16个free-lists:8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128。 //这个有点类似hash-table的buckets-nodes结构 //每个数组节点管理一个链表,同一链表上的节点区块大小相等 //volatile告诉编译器这是个“易变化”的变量,随时可能改变。 //这样编译器就不会对它进行某些优化——比如不会从寄存器读取数据,而是从变量地址读取; // 比如不会调整指令顺序以优化CPU指令流水线 static obj * volatile free_list[__NFREELISTS]; //一个数组,效率高 static size_t FREELIST_INDEX(size_t bytes){ return ((bytes) + __ALIGN - 1) / __ALIGN - 1; //返回数组的索引——索引与请求的区块的小是一一对象的 } private: //返回一个大小为 n 的对象,并可能加入大小为 n 的其它区块到 free-list static void *refill(size_t n); //配置一大块空间,可容纳 nobjs 个大小为 "size" 的区块 //如果配置 nobjs 个区块有所不便,nobjs 可能会降低 static char *chunk_alloc(size_t size, int &nobjs); //Chunk allocation state static char *start_free; //内存池起始位置。只在chunk_alloc()中变化 static char *end_free; //内存池结束为止。只在chunk_alloc()中变化 static size_t heap_size; public: static void * allocate(size_t n); static void deallocate(void *p, size_t n); static void * reallocate(void *p, size_t old_sz, size_t new_sz); }; //staitc data memeber的定义与初值设定 template <bool threads, int inst> char *default_alloc_template<threads, inst>::start_free = 0; template <bool threads, int inst> char *default_alloc_template<threads, inst>::end_free = 0; template <bool threads, int inst> size_t default_alloc_template<threads, inst>::heap_size = 0; //初值为0 template <bool threads, int inst> typename default_alloc_template<threads, inst>::obj * volatile default_alloc_template<threads, inst>::free_list[__NFREELISTS] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; // allocate的算法 //(1)首先判断区块大小,大于128bytes就调用第一级配置器,小于128bytes就检查对应free-lists //(2)小于128bytes——在free_list上寻找适当的区块,返回当前索引 template <bool threads, int inst> void * default_alloc_template<threads, inst>::allocate(size_t n) { if (n > (size_t) __MAX_BYTES) { return malloc(n); //第一级配置器的allocate()内部直接使用malloc()分配 } obj * volatile * my_free_list; obj * result; //寻找free_list中适当的区块 my_free_list = free_list + FREELIST_INDEX(n); //my_free_list为数据节点的地址 result = *my_free_list; //解引用,获取数组当前索引的元素 if (result == 0) { //如果当前索引管理的链表为空,则直接分配,不从链表取 void * r = refill(ROUND_UP(n)); return r; } //指向管理的链表的首节点的下一个节点 *my_free_list = result->free_list_link; return (result); } template <bool threads, int inst> void default_alloc_template<threads, inst>::deallocate(void * p, size_t n) { //大于128就调用第一级配置器 if (n > (size_t) __MAX_BYTES){ free(p); return; } obj * q = (obj *)p; obj * volatile * my_free_list; //寻找对应的free list my_free_list = free_list + FREELIST_INDEX(n); //调整free list,回收区块 q->free_list_link = *my_free_list; //把待回收的区块插入到当前空闲区块的前面 *my_free_list = q; //使得索引对应的值为首部指针 } //如果free_list没有可用区块,调用refill(),准备为free_list重新填充空间 //新的空间将取自内存池(经由chunk_alloc())完成 //缺省取得20个新节点(新区块),但万一内存池空间不足,获得的节点数(区块数)可能小于20 template <bool threads, int inst> void * default_alloc_template<threads, inst>::refill(size_t n) //假设 n 已经调节为 8 的倍数 { int nobjs = 20; //调用chunk_alloc(),尝试取得nobjs个区块作为free_list的新节点 //注意参数nobjs是pass by reference char * chunk = chunk_alloc(n, nobjs); //chunk_alloc分配了一个块。这个块共有20个节点,每个节点大小为 n。 obj * volatile * my_free_list; obj * result; obj * current_obj, *next_obj; int i; //如果只获得一个区块,这个区块就分配给调用者用,free_list无新节点 if (1 == nobjs) return (chunk); //否则准备调整free_list,纳入新节点 my_free_list = free_list + FREELIST_INDEX(n); //以下在 chunk 空间内建立 free_list result = (obj*)chunk; //把块的第一个节点返回给客户端,剩下的链接到free_list //导引free_list指向新配置的空间(取自内存池) *my_free_list = next_obj = (obj *)(chunk + n); //这里它把请求的内存块解释为 union obj //即该内存块的前8个字节存放的是union obj //这样也就满足了前面一个union既可以指向下一个union obj,又可以指向内存块 //因为通过数组下标,即可访问obj后续的空闲内存块。 //将free_list的各节点串接起来 for (i = 1;; i++){ current_obj = next_obj; next_obj = (obj*)((char *)next_obj + n); if (nobjs - 1 == i){ current_obj->free_list_link = 0; //如果是最后一个节点,则指针指向空 break; }else{ current_obj->free_list_link = next_obj; } } return (result); } //内存池——memory pool //chunk_alloc操作的对象时内存池,从内存池中取得所需内存 //如果内存池空间不足,则重新malloc内存 //如果malloc不到,则从free_list寻找空闲的区块,赋给内存池,再对内存池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; //剩余空间共有 nobjs 个区块 total_bytes = size * nobjs; result = start_free; start_free += total_bytes; //把剩余空间都分配到free_list上 return (result); }else{ //内存池剩余空间连一个区块的大小都无法提供,则扩充内存池,重新调用chunk_alloc //分配所需空间的两倍 size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4); //为什么要向右移动4位或者说为什么要除以8 //让内存池的残余零头插入到free_list对应位置上 if (bytes_left > 0) { obj * volatile * my_free_list = free_list + FREELIST_INDEX(bytes_left); //这里索引用的是bytes_left,而不是size ((obj *)start->free)->free_list_link = *my_free_list; //使得新节点为索引所指链表的头结点 *my_free_list = (obj *)start_free; //指向插入新节点后的链表的头结点 } //链表节点管理的链表为空才会调用refill,refill调用chunk_alloc分配大块内存 //如果内存池中空间满足需求,则分配出去,否则,把剩余空间分配出去; //如果内存池空间不足一个size,则重新malloc两倍空间 //配置heap空间,用来补充内存池 start_free = (char *)malloc(bytes_to_get); //分配所需空间的两倍 if (0 == start_free){ //malloc()失败 int i; obj * volatile * my_free_list, *p; //malloc()失败,则从free_list上寻找可用空间(从大于size的区块的索引开始寻找) for (i = size; i <= __MAX_BYTES; i += __ALIGN){ my_free_list = free_list + FREELIST_INDEX(i); //遍历free_list,寻找比size更大的区块 p = *my_free_list; if (0 != p) { *my_free_list = p->free_list_link; start_free = (char *)p; //把链表中大于size且空闲的空间赋给内存池 end_free = start_free + i; return (chunk_alloc(size, nobjs)); //递归调用自己,修正nobjs } } //如果free_list上也没有满足需求的空闲区块 end_free = 0; start_free = (char *)malloc(bytes_to_get); //调用第一级配置器,借用out-of-memory机制 } heap_size += bytes_to_get; end_free = start_free + bytes_to_get; return (chunk_alloc(size, nobjs)); } } int main(int argc, char** argv){ return 0; }
2.1.1 区块大小和空闲链块总数
enum { __ALIGN = 8 }; //小型区块的上调边界 enum { __MAX_BYTES = 128 }; //小型区块上限 enum { __NFREELISTS = __MAX_BYTES / __ALIGN }; //free-lists 个数
为什么是8?
从union obj的内部结构来看,在32位下,sizeof(obj) = 4;在64位下,sizeof(obj) = 8;所以,最小的区块大小为8,且其他均为8的倍数。(个人认为是这样的原因,毕竟底层实现中,数据结构和算法还是耦合在一起的)
2.1.2 union obj和free_list
union obj{ union obj * free_list_link; //在64位Windows系统gcc编译器上,指针的大小为8字节 char client_data[1]; //这里的值是地址低字节的值 }; static obj * volatile free_list[__NFREELISTS]; //一个数组,效率高 static size_t FREELIST_INDEX(size_t bytes){ return ((bytes) + __ALIGN - 1) / __ALIGN - 1; //返回数组的索引——索引与请求的区块的小是一一对象的 }
书中说:union obj,从第一字段来看,obj可被视为一个指针,指向相同形式的另一个obj;从第二个字段来看,obj可被视为一个指针,指向实际区块。一物二用。(这样可以避免为了维护链表所必须的指针而造成内存的一种浪费)
union obj,从定义来看,char[0-3/7]和obj *公用同一段存储空间,char[0-3/7]所指空间已经被obj *的值所填充了,其本身并不能存储数据,所以,只看定义无法很快理解它的效果。需要结合实际应用。
char * chunk = chunk_alloc(n, nobjs); //chunk_alloc分配了一个块。这个块共有20个节点,每个节点大小为 n。 obj * volatile * my_free_list; obj * result; obj * current_obj, *next_obj; int i; //如果只获得一个区块,这个区块就分配给调用者用,free_list无新节点 if (1 == nobjs) return (chunk); //否则准备调整free_list,纳入新节点 my_free_list = free_list + FREELIST_INDEX(n); //以下在 chunk 空间内建立 free_list result = (obj*)chunk; //把块的第一个节点返回给客户端,剩下的链接到free_list //导引free_list指向新配置的空间(取自内存池) *my_free_list = next_obj = (obj *)(chunk + n);
在refill中用到了union obj *,这里就可以了解一物二用的原理了——当把一个大块内存空间解释为union obj时,只有内存空间的前几个字节被占用,后面的内存仍然是空闲的。这也正对应书中所画的图——union obj在大块内存的前面一小部分。
这样就可以使用union obj中指针指向另一个区块,而使用client_data来访问空闲区块
2.1.3 从内存池获取区块放置free_list中
//返回一个大小为 n 的对象,并可能加入大小为 n 的其它区块到 free-list static void *refill(size_t n); //配置一大块空间,可容纳 nobjs 个大小为 "size" 的区块 //如果配置 nobjs 个区块有所不便,nobjs 可能会降低 static char *chunk_alloc(size_t size, int &nobjs); //Chunk allocation state static char *start_free; //内存池起始位置。只在chunk_alloc()中变化 static char *end_free; //内存池结束为止。只在chunk_alloc()中变化 static size_t heap_size;
static char *start_free; static char *end_free; 分别指向分配的内存块的首部和尾部
refill调用chunk_alloc分配多个大块内存,分出一个给客户端,其他的放入free_list中
chunk_alloc从内存池中获取一整块内存,如果内存池没有,则重新malloc到内存池——>如果malloc失败则从free_list上请求大于size的空闲区块——>如果free_list也没有,则使用第一级配置器来分配内存(其有out-of-memory机制可以跑出异常或释放出一些内存)。
2.1.4 refill和chunk_alloc
1、refill的算法
(1)调用chunk_alloc获取大块内存
(2)把第一块内存返回给客户端
(3)剩下的内存插入到free_list成链
2、chunk_alloc算法
(1)chunk_alloc操作的对象时内存池,从内存池中取得所需内存
(2)如果内存池空间不足,则重新malloc内存
(3)如果malloc不到,则从free_list寻找空闲的区块,赋给内存池,再对内存池chunk_alloc
(4)都不行,则调用第一级配置器的allocate分配内存,利用其out-of-memory机制解围
2.1.5 allocate,deallocate,reallocate
当free_list中需求的区块空间不足时,调用allocate,reallocate中会调用refill来获取空间
1、allocate的算法
(1)首先判断区块大小,大于128bytes就调用第一级配置器,小于128bytes就检查对应free-lists
(2)小于128bytes——在free_list上寻找适当的区块,返回当前索引
2、deallocate的算法
(1)大于128bytes则调用第一级配置器
(2)寻找当前回收的区块大小对应的索引,把该区块插入该索引管理的链表首部
3 全局函数construct,deconstruct
类有构造函数,有析构函数
当声明了一个类对象,空间配置器为它分配空间,并调用类的构造函数把内存空间初始化
即内存空间的配置与释放和对象内容的构造与析构是分开的。
3.1 <memory>:stl_alloc.h,stl_construct.h
配置器的std::alloc定义于<memory>中,内含两个文件
#include <stl_alloc.h> //负责内存空间的配置和释放
#include <stl_constuct.h> //负责对象内容的构造与析构
3.1.1 stl_alloc.h——配置器
负责内存空间的配置与释放
3.1.2 stl_construct.h——construct()和destroy()
construct()负责对象的构造
destroy()负责对象的析构
3.2 construct和destroy
3.2.1 construct
construct全局函数通过在分配了内存空间的p指针上,调用构造函数来构造对象。——使用placement new:new(p) T(value)template <class T1, class T2> inline void construct(T1* p, const T2& value){ new (p) T2(value); }
3.2.2 destroy
有两种
1、通过指针调用析构函数
template <class T> inline void destroy(T* pointer){ pointer->~T(); }
2、判断元素的数值型别是否有trivial destructor
(1)有,则destroy不做任何事
(2)无,则通过指针调用析构函数
template <class ForwardIterator> inline void destroy(ForwardIterator first, ForwardIterator last, T*) { __destroy(first, last, value_type(first)); } template <class ForwardIterator, class T> inline void __destroy(ForwardIterator first, ForwardIterator last, T*) { typedef typename _type_traits<T>::has_trival_destructor trivial_destructor; __destroy_aux(first, last, trivial_destructor()); } template <class ForwardIterator> inline void __destroy_aux(ForwardIterator first, ForwardIterator last, T*) { for (; first < last; ++first) destroy(&*first); }
4 全局函数uninitialized_copy,uninitialized_fill,uninitialized_fill_n
三个函数都具有“ commit or rollback ”语意——要么构造出所有必要元素,要么(当有任何一个copy constructor失败时)不构造任何东西。
uninitialized_copy()
uninitialized_fill()
uninitialized_fill_n()
PS:
虽然这些函数不属于配置器的范畴,但与对象初值设置有关。
对于容器的大规模元素初值设置很有帮助。对于效率的考虑也面面俱到——最差情况下调用construct(),最佳情况使用C标准memmove()直接进行内存数据移动。
4.1 uninitialized_fill_n
从first开始,使用x,初始化往后n个对象
template <class ForwardIterator, class Size, class T> inline ForwardIterator uninitialized_fill_n(ForwardIterator first, Size n, const T& x){ return __uninitialized_fill_n(first, n, x, value_type(first)); } template <class ForwardIterator, class Size, class T, class T1> inline ForwardIterator __uninitialized_fill_n(ForwardIterator first, Size n, const T& x, T1*) { typedef typename __type_trait<T1>::is_POD_type is_POD; return __uninitialized_fill_n_aux(first, n, x, is_POD()); } //POD型别 //调用高阶函数fill_n实现——可能直接memcopy了 template <class ForwardIterator, class Size, class T> inline ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first, Size n, const T& x, __true_type){ return fill_n(first, n, x); } //非POD型别 //采用安全的方法——调用构造函数 template <class ForwardIterator, class Size, class T> inline ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first, Size n, const T& x, __false_type){ ForwardIterator cur = first; for (; n > 0; --n, ++cur) construct(&*cur, x); //传入指针,通过指针调用构造函数 return cur; }
1、首先萃取出迭代器first的value_type,然后判断型别是否为POD型别
POD意指Plain Old Data,即标量型别或传统的C struct。
POD型别必然拥有trivial ctor/dtor/copy/assignment函数,因此可以对POD型别采用最高效的初值填写手法。
4.2 uninitialized_fill
用x,初始化从first到last的所有对象
形式与uninitialized_fill_n差不多
4.3 uninitialized_copy
把first到last的对象复制到result开始的容器中
template <class InputIterator, class ForwardIterator> inline ForwardIterator uninitialized_copy(InputIterator first, InputIterator last, ForwardIterator result){ return __uninitialized_fill_n(first, n, x, value_type(first)); } template <class InputIterator, class ForwardIterator, class T> inline ForwardIterator __uninitialized_copy(InputIterator first, InputIterator last, ForwardIterator reslut, T*){ typedef typename __type_trait<T1>::is_POD_type is_POD; return __uninitialized_copy_aux(first, last, result, is_POD()); } //POD型别 //调用高阶函数fill_n实现——可能直接memcopy了 template <class InputIterator, class ForwardIterator> inline ForwardIterator __uninitialized_copy_aux(InputIterator first, InputIterator last, ForwardIterator reslut, __true_type){ return copy(first, last, result); } //非POD型别 //采用安全的方法——调用构造函数 template <class InputIterator, class ForwardIterator> inline ForwardIterator __uninitialized_copy_aux(InputIterator first, InputIterator last, ForwardIterator reslut, __false_type){ ForwardIterator cur = result; for (; n > 0; --n, ++cur) construct(&*cur, *first); //传入指针,通过指针调用构造函数 return cur; }
对于char*和wchar_t*两种型别,可以采用最具效率的做法memmove(直接移动内存内容)来执行复制行为inline char* uninitialized_copy(const char* first, const char* last, char* result) { memmove(result, first, last - first); return result + (last - first); //返回最后位置?? } 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); }