文章首发于:My Blog 欢迎大佬们前来逛逛
1. SGI空间配置器
SGI STL的空间配置器是 alloc而非allocator,并且不接受任何参数:
vector<int,std::alloc> vec
我们通常使用缺省的空间配置器:
template <typename T,typename Alloc=alloc>//指定其默认空间配置器为 alloc
class vector
{
...
}
1.1 std::allocator
SGI的 allocator空间配置器 只是对 new 和delete 做了一层包装,效率及其不佳,不建议使用它
template <class T>
inline T* allocate(ptrdiff_t size, T*) {
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);
}
allocate:包装了 new
deallocate:包装了 delete
因此 SGI的 allocator 是基层内存配置释放的行为,只有包装,没有效率的优势
1.2 std::alloc
SGI 重要的空间配置器:std::alloc
我们常写的c++语法:
class foo{};
class* obj=new class();
delete obj
使用new创建对象与使用delete销毁对象。
使用new时的完整操作:
首先调用 ::operator new配置内存。
然后调用 构造函数 初始化对象。
同理delete的完整操作:
首先调用 析构函数 销毁对象。
然后调用 ::operator delete 释放内存。
因此alloc把new和delete 以及他们各自的两部分分离开:
alloc:allocate:配置内存
alloc:deallocate:释放内存
::construct:构造对象
::destroy:销毁对象
他们分别存在于这两个头文件中
#include <stl_alloc.h> //对象的配置与释放
#include <stl_construct>//对象的构造与析构
1.3 对象构造与析构:construct和destroy
来看 < stl_construct> 头文件中:
#include <new.h> // 引入 new 运算符
最基本的两大函数:
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); //T1::T1(value)
}
destroy直接负责某个对象的析构,即 ~T() 操作。
construct直接负责某个对象的创建,但是请注意这个new 是一个 placement new操作
形如: new (ptr) T(value) 是一个 placement new操作
括号里的参数ptr是一个指针,它指向一个内存缓冲器,placement new将在这个缓冲器上分配一个对象。
Placement new的返回值是这个被构造对象的地址(比如括号中的传递参数)。
placement new主要适用于:在对时间要求非常高的应用程序中,因为这些程序分配的时间是确定的;长时间运行而不被打断的程序;以及执行一个垃圾收集器 (garbage collector)。
比如:
int n=2;
new (&n) int(10);
std::cout<<n; // n = 10
其中这两个函数还具有其他的版本:
//接受两个迭代器,并设法找出元素的类型
template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last) {
__destroy(first, last, value_type(first));
}
destroy的第二个版本:接受两个迭代器,然后 value_type会自动推导出 此迭代器的所指的元素的类型
value_type 属于__type_traits的成员,会自动推导出迭代器所指元素的类型,在往后的 第二章 我们会学到
然后根据推导出的第三个参数的类型选择 适当的 __destroy函数。
template <class ForwardIterator, class T>
inline void __destroy(ForwardIterator first, ForwardIterator last, T*) {
// trivial_destructor 是__type_traits<T>::has_trivial_destructor的别名
typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
__destroy_aux(first, last, trivial_destructor());
}
思考一下:destroy有两个版本,第一个版本是一个对象的析构;第二个版本是 [first,last]范围 的析构
对于整个范围的析构:如果我们不知道这个范围有多大,则无论它是什么类型,我们都需要对范围的每一个元素执行析构函数,对于自定义类型:student,xxx等 具有我们显式定义的析构函数,我们必须这样做。
但是对于 int,char,long等内置类型,我们有必要对他们每次都调用析构函数(系统自带的)???
如果我们对 这些内置类型 进行一次又一次的析构,则是效率的巨大牺牲,因此有没有一种办法可以推断出即将析构的是内置类型还是自定义类型;
内置类型 _true_type类型 :直接什么也不做即可。
非内置类型 _false_type 类型:执行每个元素的析构函数。
因此上面的destroy执行的就是这样的操作。
对于判断它是什么类型的 value_type 的实现方法,我们在 迭代器一节 会给出方法。
如果是 内置类型 即**_true_type**类型:直接跳过即可
//无需析构
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {}
如果是 非内置类型 即**_false_type** 类型:执行析构函数
//需要析构destory
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) {
for ( ; first < last; ++first)
destroy(&*first);
}
在false_type版本中执行的这个destroy 就是destroy的第一个版本,即直接调用
pointer->~T(); // 析构
1.4 空间配置与释放:allocate和deallocate
C++的内存配置与释放:new和delete
这两个函数相当于C的malloc和free。
因此SGI的空间配置与释放就是依据 malloc和free进行的
SGI 的双层级配置器:
第一级配置器用于 配置大小超过 128 字节的空间。
第二级配置器用于 配置大小小于 128 字节的空间。
第一级配置器:__malloc_alloc_template
__malloc_alloc_tetemplate <int inst> //inst完全没用到
class __malloc_alloc_template
{
...
}
第二级配置器:__default_alloc_template
template <bool threads, int inst>
class __default_alloc_template
{
...
}
无论是第一级还是第二级,SGI提供了一个接口,使得配置器的接口得以抽象:
template<class T, class Alloc>
class simple_alloc {
public:
static T *allocate(size_t n)
{ return 0 == n? 0 : (T*) Alloc::allocate(n * sizeof (T)); }
static T *allocate(void)
{ return (T*) Alloc::allocate(sizeof (T)); }
static void deallocate(T *p, size_t n)
{ if (0 != n) Alloc::deallocate(p, n * sizeof (T)); }
static void deallocate(T *p)
{ Alloc::deallocate(p, sizeof (T)); }
};
注意:这个接口非常重要!
成员函数allocate和deallocate是可以选择调用 第一级还是第二级配置器 的实现方法的。
几乎所有的SGI STL容器全部使用这个接口:像是 vector,list等全部使用这个接口来处理空间的配置。
template <typename T,typename Alloc=alloc>
class vector
{
portected:
typedef simple_alloc<T,Alloc> data_allocator //数据空间配置器
...
void deallocate()
{
data_allocator::deallocate(xxxxx);
}
}
deallocate函数 调用 data_allocator ,从而调用 deallocate函数,实际上就是 simple_alloc 的两个deallocate函数,然后再根据是第一级配置器还是第二级配置器选择哪个版本的 deallocate函数。
1.4.1 第一级空间配置器
我们从**__malloc_alloc_template**类 中 一层一层的看:
static void * oom_malloc(size_t);
static void *oom_realloc(void *, size_t);
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
static void (* __malloc_alloc_oom_handler)();
#endif
oom:out of memmory 表示内存不足。
__malloc_alloc_oom_handler是一个函数指针,可以处理内存不足的情况
创建n个大小的空间,allocate会直接 malloc这个大小的空间,如果分派失败,则调用内存不足的处理方法
static void * allocate(size_t n)
{
void *result = malloc(n); //第一级配置器直接使用malloc分配内存
if (0 == result) result = oom_malloc(n);//无法满足要求,使用内存不足的处理方法
return result;
}
释放某个对象的空间,直接使用 deallocate 的free
static void deallocate(void *p, size_t /* n */)
{
free(p); //直接使用free释放内存
}
扩容,使用 包装的ralloc即可,同样会处理内存不足的处理情况
static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)
{
void * result = realloc(p, new_sz);
if (0 == result) result = oom_realloc(p, new_sz);
return result;
}
发生内存不足的错误时的函数指针的处理:
可以指定 f 为 你任意的处理错误的方法,然后赋予给 函数指针。
//可以通过它指定你自己的out of memory 处理方法
//仿真 set_new_handle
static void (* set_malloc_handler(void (*f)()))()
{
void (* old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = f;
return(old);
}
当出现内存不足时的处理方法:
oom_malloc 和 oom_realloc 会尝试在例程中压缩出空间,如果挤出了一点内存则返回此内存,如果一点内存都挤不出来,则出发 失败的异常。
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);
}
}
template <int inst>
void * __malloc_alloc_template<inst>::oom_realloc(void *p, 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 = realloc(p, n);
if (result) return(result);
}
}
# define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1)
失败直接退出
第一级空间配置器 利用 malloc free和ralloc实现出了对内存的分配与释放。
并且也实现了C++的 new handler机制
c++ 的new_handler机制: 当无法分配足够的内存时,在丢出std::bad_alloc之前,客端会调用一些指定的处理例程,称为 new_handler,然后尽全力帮你挤出内存,如果实在挤不出来则只能抛出异常。
但是 第一级空间配置器使用的是 malloc,而不是 new,所以无法实现 C++纯正的 set_new_handler机制,因此必须手动模拟一个 set_new_handler
正如上面的 oom_malloc 和 oom_ralloc 函数的实现
第一级空间配置器使用如下的别名:malloc_alloc
//malloc_alloc 为第一级空间配置器
typedef __malloc_alloc_template<0> malloc_alloc;
1.4.2 第二级空间配置器
__default_alloc_template 作为第二级配置器用来处理避免太多小额区块造成的运行负担。
区块越小,额外负担所占用的比例就越大,就越浪费
第二级配置器的做法:
如果区块大于 128 字节,则移交第一级配置器实现
如果区块小于 128 字节,则交由内存池管理,这种方法又叫做 次级配置,每次分配一大块内存,就会产生并且维护一个自由链表。
如果下次再有相同大小的区块需要分配,则直接从 free-list中取出
如果释放,则直接 回收 free-list 中的某一段
配置会自动将任何区块的内存需求量设置为 8 的倍数:30 -> 32
并且维护16块 free-list,每个块为8字节,每个块都是一个 free-list,free-list总大小即为128字节,每个free-list各自管理自己对应的 8个字节的小额区块
/*
自由链表 : free_list
*/
union obj {
union obj * free_list_link;
char client_data[1]; /* The client sees this. */
};
使用 union 来节省内存空间
free-list 指向 还没有被分配的内存空间的地址,如果已经分配,则不再指向他们
预定义 区块及free-list的个数
enum {__ALIGN = 8};//上调边界
enum {__MAX_BYTES = 128};//小型区块的上界
enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; //free_list个数
此函数表示将区块的内存需求量都上升为 8 的倍数(每块总共有8个字节,表示成能分多少块)
static size_t ROUND_UP(size_t bytes) {
return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
}
free-list的定义 : 是一个数组,数组的每一个元素都是一个指针。
根据区块大小,决定使用第几块 free-list
static obj * __VOLATILE free_list[__NFREELISTS]; //链表:指针数组
# endif
//决定使用第几号 free_list 从1开始
static size_t FREELIST_INDEX(size_t bytes) {
return (((bytes) + __ALIGN-1)/__ALIGN - 1);
}
数据成员:
// Chunk allocation state.
static char *start_free; //内存池起始位置
static char *end_free; //内存池结束位置
static size_t heap_size;
1.4.2.1 空间配置函数 allocate
allocate的执行过程:
如果需要分配的空间 n超过了128个字节的大小,则就转到第一级空间配置器的实现上。
然后再从16个 free-list中选择适当的一个。
如果free-list中有可用的区块,则直接拿来用;否则就把区块的大小上调至 8 的倍数,然后再refill重新填充 free-list。
然后调整 free-list 为选出第 n 个区块后的下一个free-list。
最后取出 result 指向的第 n 个区块的空间起始地址
/*
空间配置函数
*/
static void * allocate(size_t n)
{
obj * __VOLATILE * my_free_list;
obj * __RESTRICT result;
//大于128 就调用第一级配置器
if (n > (size_t) __MAX_BYTES) {
return(malloc_alloc::allocate(n));
}
//从16个 free_list中选择一个使用
my_free_list = free_list + FREELIST_INDEX(n);
result = *my_free_list;
if (result == 0) {
void *r = refill(ROUND_UP(n));//没有找到可用的free_list 准备重新填充
return r;
}
*my_free_list = result -> free_list_link;//调整free_list
return (result);//取出空间
};
1.4.2.2 空间释放函数 deallocate
回收区块:
如果回收大于 128 个字节,则转到第一级空间配置器
寻找对应的 free-list区块
把 p 指向的某一个区块插入到free-list中:
首先q的next地址为free-list对应的下一个区块
free-list的当前区块转为 q区块
其实就是完成了链表的中间插入。
//释放空间
static void deallocate(void *p, size_t n)
{
obj *q = (obj *)p;
obj * __VOLATILE * my_free_list;
//大于128调用第一级配置器
if (n > (size_t) __MAX_BYTES) {
malloc_alloc::deallocate(p, n);
return;
}
my_free_list = free_list + FREELIST_INDEX(n);//寻找对应的free_list
//调整list 回收区块
q -> free_list_link = *my_free_list;
*my_free_list = q;
// lock is released here
}
1.4.2.3 重新填充 refill
前面讲到从free-list中取出第 n 个区块的区块,万一free-list中已经没有了可以用的区块,则需要重新填充 free-list 自由链表
从内存池中取出nobjs个区块,作为free-list的新节点,nobjs是引用的方式传递的。
如果只获得了一个区块,则直接分配给调用者用,free-list无需新的区块,通过返回 chunk 给到allocate的返回值;否则准备调整free-list,纳入新的节点。
然后在 chunk 空间内建立 free-list
/*
返回一个大小为n的对象,并且为free_list增加节点
*/
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
int nobjs = 20;
//nobjs通过传递引用的方式 来取得nobjs个区块作为free_list的新节点
char * chunk = chunk_alloc(n, nobjs);
obj * __VOLATILE * my_free_list;
obj * result;
obj * current_obj, * next_obj;
int i;
if (1 == nobjs) return(chunk);//只获得一个区块,直接返回给调用者
//多个区块:调整free_list 纳入新节点
my_free_list = free_list + FREELIST_INDEX(n);
//在chunk空间里建立free_List
result = (obj *)chunk;
*my_free_list = next_obj = (obj *)(chunk + n);//free_list指向新配置的空间(取自内存池)
//将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);
}
1.4.2.4 内存池 chunk_alloc
/*
内存池中取空间给 free_list 使用
*/
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;//total_bytes全部分配
return(result);
}
else if (bytes_left >= size) {
//内存池剩余容量不能容纳全部,但是可以容纳 一个及以上的块的空间
//nobjs是引用类型,修改为实际能够供应的区块数
nobjs = bytes_left/size;//最大能容纳的n
total_bytes = size * nobjs;//最大能分配的空间
result = start_free;//起始
start_free += total_bytes;//最多能容纳的total_bytes全部分配
return(result);
}
else {
//一个块都容纳不了! 则尝试heap中配置
//新的空间大小为原始需要分配的空间大小的两倍
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;
}
start_free = (char *)malloc(bytes_to_get);
if (0 == start_free) {
int i;
obj * __VOLATILE * my_free_list, *p;
// Try to make do with what we have. That can't
// hurt. We do not try smaller requests, since that tends
// to result in disaster on multi-process machines.
for (i = size; i <= __MAX_BYTES; i += __ALIGN) {
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;
//递归调用自己,修复nobjs
return(chunk_alloc(size, nobjs));
// Any leftover piece will eventually make it to the
// right free list.
}
}
/*
一点内存都没有了
*/
end_free = 0; // In case of exception.
//最后调用第一级配置器,看看能否得到改善
start_free = (char *)malloc_alloc::allocate(bytes_to_get);
// This should either throw an
// exception or remedy the situation. Thus we assume it
// succeeded.
}
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
return(chunk_alloc(size, nobjs));
}
}
1.5 内存基本操作函数
STL有五个非常重要的全局函数:
用于构造的construct函数
用于析构的destroy函数
uninitialized_copy :copy
uninitialized_fill:fill
uninitialized_fill_n:fill_n
前两个已经在对象的构造与析构中讲过了
另外的三个函数将在本节讲解。
1.5.1 uninitialized_copy
uninitialized_copy函数能够使得内存的配置和对象的构造分离开。
template <class InputIterator, class ForwardIterator>
inline ForwardIterator
uninitialized_copy(InputIterator first, InputIterator last,
ForwardIterator result) {
return __uninitialized_copy(first, last, result, value_type(result));
}
将 first到 last 迭代器范围的元素拷贝到 result所指向的位置。
即 该函数将 [first,last)范围内的元素 复制一份,并把这些 复制品放入 result的空间里。
针对输入范围的每一个迭代器 i ,construct 都会执行如下操作:
new (ptr) T(val)
constuct(&*(result+(i-first)),*i),产生 *i 的复制品,放置于输出位置 result后的相对位置中
在实现任何一个容器的构造函数的时候,通常会有两个步骤:
配置内存空间的大小
使用 uninitialized_copy来在该内存区块上构造元素
uninitialized_copy 中调用的函数:
该函数 使用__type_traits 操作来推断出迭代器所指的元素类型是不是POD类型
如果是 POD __true_type:则拷贝时执行最简单的内存拷贝
如果是 非POD __false_type:则需要调用相应的构造函数来逐一拷贝
template <class InputIterator, class ForwardIterator, class T>
inline ForwardIterator
__uninitialized_copy(InputIterator first, InputIterator last,
ForwardIterator result, T*) {
typedef typename __type_traits<T>::is_POD_type is_POD;
return __uninitialized_copy_aux(first, last, result, is_POD());
}
template <class InputIterator, class ForwardIterator>
inline ForwardIterator
__uninitialized_copy_aux(InputIterator first, InputIterator last,
ForwardIterator result,
__true_type) {
return copy(first, last, result);//是POD类型
}
template <class InputIterator, class ForwardIterator>
ForwardIterator
__uninitialized_copy_aux(InputIterator first, InputIterator last,
ForwardIterator result,
__false_type) {
ForwardIterator cur = result;//不是POD类型
__STL_TRY {
for ( ; first != last; ++first, ++cur)
construct(&*cur, *first);//逐一进行拷贝
return cur;
}
__STL_UNWIND(destroy(result, cur));
}
1.5.2 uninitialized_fill
他的作用是为指定范围的元素赋予一个值:[first,last) 赋予初始值 val
与 uninitialized_copy类似,如果迭代器范围的每一个元素都指向一个未知的区域,那么会对迭代器范围的每一个元素都调用一次: construct(&*i,val),然后在这个位置上产生一个值val
同样使用value_type推断出迭代器所指元素的类型,然后传入__uninitialized_fill。
template <class ForwardIterator, class T>
inline void uninitialized_fill(ForwardIterator first, ForwardIterator last,
const T& x) {
__uninitialized_fill(first, last, x, value_type(first));
}
在__uninitialized_fill函数内部会使用 __type_traits推断类型是否为POD类型。
如果是POD类型,则直接采用最简单的fill 填充的方式(STL算法)
如果不是POD类型,则对每一个元素调用其构造函数。
使用 first!= last ,first++ 作为for循环的参数
template <class ForwardIterator, class T>
inline void
__uninitialized_fill_aux(ForwardIterator first, ForwardIterator last,
const T& x, __true_type)
{
fill(first, last, x);
}
template <class ForwardIterator, class T>
void
__uninitialized_fill_aux(ForwardIterator first, ForwardIterator last,
const T& x, __false_type)
{
ForwardIterator cur = first;
__STL_TRY {
for ( ; cur != last; ++cur)
construct(&*cur, x);
}
__STL_UNWIND(destroy(first, cur));
}
template <class ForwardIterator, class T, class T1>
inline void __uninitialized_fill(ForwardIterator first, ForwardIterator last,
const T& x, T1*) {
typedef typename __type_traits<T1>::is_POD_type is_POD;
__uninitialized_fill_aux(first, last, x, is_POD());
}
另外 fill 的实现非常简单:其实就是往迭代器的范围赋值而已
template <class ForwardIterator, class T>
void fill(ForwardIterator first, ForwardIterator last, const T& value) {
for ( ; first != last; ++first)
*first = value;
}
1.5.3 uninitialized_fill_n
该函数接受三个参数,一个 first 表示起始位置;一个 n,表示操作的数量;一个val。
从first迭代器处开始,操作 n 个地址空间,把val赋值给这些位置。
其实与上面的uninitialized_fill是一样的,只不过上面表示范围,这个表示一个数量
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));
}
同理使用 value_type 推断出迭代器的元素类型,然后再判断其是否为POD类型;
如果是,则直接 fill_n 即可
如果不是,则需要调用其 构造函数
使用 n-- ,当n=0时停止,这个for来作为循环。
注意:上面的三个函数都具有一个特性:commit or rallback
即要么对范围的操作全部成功(全部赋值成功),要么全部不成功
如果不成功,会 抛出异常,然后调用destroy,将全部的成功的元素析构掉,因为他已经失败了,不允许他一半成功,一半失败
destroy(result, cur);//uninitialized_copy
destroy(first, cur);//uninitialized_fill
destroy(first, cur);//uninitialized_fill_n
destroy 接受一个范围,然后就是我们上面所讨论的destroy的内容了