STL源码剖析 — 空间配置器(allocator)
前言
以STL的实现角度而言,第一个需要介绍的就是空间配置器,因为整个STL的操作对象都存放在容器之中。
你完全可以实现一个直接向硬件存取空间的allocator。
下面介绍的是SGI STL提供的配置器,配置的对象,是内存。(以下内容来自《STL源码剖析》)
引子
因为这篇写得太长,断断续续都有几天,所以先在这里整理一下思路。
- 首先,介绍 allocator 的标准接口,除了拥有一些基本的typedef之外,最重要的就是内存相关的 allocate 和 deallocate;构造相关的 construct 和 destroy。(两者分离)然后就是实现一个简单的配置器,没有内存管理,只是简单的malloc。
- allocate 和 deallocate 负责获取可以用的内存。
- construct调用placement new构造函数,destroy调用相应类型的析构函数 ~T()。
- 然后介绍了SGI的第一级和第二级配置器。定义__USE_MALLOC可以设置使用第一级配置器还是两个都用。
- 内存池保留没有被分配到free list的空间,free list维护一张可供调用的空间链表。
- construct 会使用placement new构造,destroy借助traits机制判断是否为 trivial再决定下一步动作。
- allocate调用refill函数,会缺省申请20个区块,一个返回,19个留在free list。refill又有三种情况。
- deallocate先判断是否大于128byte,是则调用第一级配置器,否就返回给freelist。
空间配置器的标准接口
根据STL的规范,allocator的必要接口
- 各种typedef
allocator::value_type
allocator::pointer
allocator::const_pointer
allocator::reference
allocator::const_reference
allocator::size_type
allocator::difference_type
allocator::rebind // class rebind<U>拥有唯一成员other;是一个typedef,代表allocator<U> - 默认构造函数和析构函数,因为没有数据成员,所以不需要初始化,但是必须被定义
allocator::allocator()
allocator::allocator(const allocator&)
template <class U> allocator::allocator(const allocator<U>&)
allocator::~allocator() - 初始化,地址相关函数
// 配置空间,足以存储n个T对象,第二个参数是提示,能增进区域性
pointer allocator::allocate(size_type n, const void*=) size_type allocator::max_size() const pointer allocator::address(reference x) const
const_pointer allocator::address(const_reference x) const - 构建函数
void allocator::construct(pointer p, const T& x)
void allocator::destory(pointer p)
自己设计一个简单的空间配置器
#ifndef __VIGGO__
#define __VIGGO__
#include <new> // for placement new
#include <cstddef> // for ptrdiff_t, size_t
#include <cstdlib> // for exit()
#include <climits> // for UINT_MAX
#include <iostream> // for cerr namespace VG { template <class T>
inline T* _allocate(ptrdiff_t n, T*) {
set_new_handler();
T* tmp = (T*)(::operator new((size_t)(n * sizeof(T))));
if (tmp == ) {
cerr << "alloc memory error!" << endl;
exit();
}
return tmp;
} template <class T>
inline void _deallocate(T* p) {
::operator delete(p);
} template <class T1, class T2>
inline void _construct(T1* p, const T2& value) {
new(p) T1(value);
} template <class T>
inline void _destroy(T* p) {
p->~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 {
typedef allocator<U> other;
}; pointer address(reference x) {return (pointer)&x;}
const_pointer address(const_reference x) const {
return (const_pointer)&x;
} pointer allocate(size_type n, const void *hint=) {
return _allocate((difference_type)n, (pointer)); // mark
} void deallocate(pointer p, size_type n) {
_deallocate(p);
} size_type max_size() const {return size_type(UINT_MAX / sizeof(T));} void construct(pointer p, const T& x) {
_construct(p, x);
} void destroy(pointer p) {
_destroy(p);
}
};
}
#endif
放在 vector<int, VG::allocator<int> > 中测试,可以实现简单的内存分配,但是实际上的 allocator 要比这个复杂。
SGI特殊的空间配置器
标准的allocator只是基层内存配置/释放行为(::operator new 和 ::operator delete)的一层薄薄的包装,并没有任何效率上的强化。
现在我们看看C++内存配置和释放是怎样做的:
new运算分两阶段(1)调用 ::operator new 配置内存;(2) 调用对象构造函数构造对象内容。
delete运算也分两阶段(1) 调用对象的析构函数;(2)调用 ::operator delete 释放内存。
为了精密分工,STL allocator决定将两阶段操作区分开来,内存配置由 alloc::allocate() 负责。内存释放操作由 alloc::deallocate()负责;对象构造由 ::construct() 负责,对象析构由 ::destroy() 负责。
构造和析构基本工具:construct() 和 destroy()
construct() 接受一个指针p和一个初值value,该函数的用途就是将初值设定到指针所指的空间上。C++的placement new运算子可用来完成这一任务。
destory()有两个版本,一是接受一个指针,直接调用该对象的析构函数即可。另外一个接受first和last,将半开范围内的所有对象析构。首先我们不知道范围有多大,万一很大,而每个对象的析构函数都无关痛痒(所谓 trivial destructor),那么一次次调用这些无关痛痒的析构函数是一种浪费。所以我们首先判断迭代器所指对象是否为 trivial(无意义), 是则什么都不用做;否则一个个调用析构。
上图为construct的实现函数
上图为destroy的实现函数
这里用到我们神奇的 __type_traits<T>,之前介绍的 traits 是 萃取返回值类型 和 作为重载依据的,现在为每一个内置类型特化声明一些tag。
现在我们需要用到 真 和 假 两个标志:
示例:
空间的配置和释放:std::alloc
SGI的设计哲学: 1. 向 system heap 要求空间; 2. 考虑多线程状态(先略过);3. 考虑内存不足时的应变措施;4. 考虑过多“小型区块”可能造成的内存碎片问题。
SGI设计了双层级配置器,第一级配置器直接使用 malloc() 和 free(),第二级配置器则视情况采用不同的策略;当配置区块超过128bytes时,交给第一级配置器。
整个设计究竟只开放第一级配置器,或是同时开放第二级配置,取决于__USE_MALLOC时候被定义:
# ifdef __USE_MALLOC
...
typedef __malloc_alloc_template<> malloc_alloc;
typedef malloc_alloc alloc; // 令alloc为第一级配置器
#else
...
// 令alloc为第二级配置器
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, >alloc;
#endif
其中__malloc_alloc_template就是第一级配置器,__default_alloc_template为第二级配置器。alloc并不接受任何template型别参数。
无论alloc被定义为第一级或第二级配置器,SGI还为它在包装一个接口如下,使配置器的接口能够符合STL规格:
template <class T, class Alloc>
class simple_alloc {
public:
static T *allocate(size_t n)
{return ==n? : (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 ( != n) Alloc::deallocate(p, n*sizeof(T));}
static void deallocate(T *p)
{Alloc::deallocate(p, sizeof(T));}
一二级配置器的关系,接口包装,及实际运用方式,
第一级配置器 __malloc_alloc_template
#if 0
# include <new>
# define __THROW_BAD_ALLOC throw bad_alloc
#elif !defined(__THROW_BAD_ALLOC)
# include <iostream>
# define __THROW_BAD_ALLOC cerr << "out of memery" << endl; exit();
#endif // malloc-based allocator.通常比稍后介绍的 default alloc 速度慢
// 一般而言是thread-safe,并且对于空间的运用比较高效
// 以下是第一级配置器
// 注意,无“template型别参数”。置于“非型别参数”inst,则完全没排上用场
template <int inst>
class __malloc_alloc_template {
private:
//以下都是函数指针,所代表的函数将用来处理内存不足的情况
static void *oom_malloc(size_t);
static void *oom_realloc(void*, size_t);
static void (* __malloc_alloc_oom_handler)();
public:
static void * allocate(size_t n) {
void *result = malloc(n); // 第一级配置器直接使用malloc
// 无法满足需求时,改用oom_malloc
if ( == result) result = oom_malloc(n);
return result;
} static void deallocate(void *p, size_t /* n */) {
free(p); // 第一级配置器直接用free()
} static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz) {
void *result = realloc(p, new_sz);
if ( == result) result = oom_realloc(p, new_sz);
return result;
} // 以下仿真C++的 set_handler()。换句话,你可以通过它
// 指定自己的 out-of-memory handler,企图释放内存
// 因为没有调用 new,所以不能用 set_new_handler
static void (* set_malloc_handler(void (*f)())) () {
void (*old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = f;
return old;
}
}; // 初值为0,待定
template <int inst>
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = ; 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 ( = 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 ( == my_malloc_handler) {__THROW_BAD_ALLOC;}
(*my_malloc_handler)();
result = realloc(p, n);
if (result) return result;
}
}
第二级配置器 __default_alloc_template
空间配置函数 - allocate()
static void * allocate(size_t n);
1. 如果 n 大于128bytes的时候,交给第一级配置器。
2. 找到 n 对应free list下的节点;如果节点不可用(=0)则调用 refill() 填充,否则调整节点指向下一个为止,直接返回可用节点。
重新填充free lists - refill()
void * refill(size_t n); //缺省取得20个节点
把大小为 n 的区块交给客户,然后剩下的19个交给对应的 free_list 管理。
内存池 - chunk_alloc()
char * chunk_alloc(size_t size, int & nobjs); // nobjs是引用,会随实际情况调整大小
申请内存分三种情况:
- 内存池剩余空间完全满足需求。
- 内存池剩余空间不能完全满足需求量,当足够供应一个(含)以上的区块。
- 内存池剩余空间连一个区块的大小都无法提供。
首先必须做的就是查看剩余的空间:
size_t bytes_left = end_free - start_free;
size_t total_bytes = size * nobjs;
面对第一种情况,内存空间足够的,只需要调整代表空闲内存的 start_free 指针,返回区域块就可以。
面对第二种情况,尽量分配,有多少尽量分配。这是nobjs会被逐渐减少,从默认的20到能分配出内存, nobjs = bytes_left / size。
面对第三种情况,情况有点复杂。
- 既然 [start_free, end_free) 之间的空间不够分配 size * nobjs 大小的空间,就先把这段空间分配给合适的 free list 节点(下一步有用)。
- 从 heap 上分配 两倍的所需内存+heap大小的1/16(对齐成8的倍数) 大小的内存。
- 如果heap分配都失败的话,就在 free list 中比 size 大的节点中找内存使用。
- 实在不行只能调用第一级配置器看看有咩有奇迹,oom机制。
- 最后调整 heap_size 和 end_free,递归调用 chunk_alloc 知道至少能分出一个区块。
空间释放函数 - deallocate()
大于128就交给第一级配置器,否则调整free list,释放内存。
完整代码
enum {__ALIGN = };
enum {__MAX_BYTES = };
enum {_NFREELISTS = __MAX_BYTES/__ALIGN}; // 以下是第二级配置器
// 注意,无“template型别参数”,且第二参数完全没排上用场
// 第一参数用于多线程环境下
template <bool threads, int inst>
class __default_alloc_template {
private:
// 将bytes上调至8的倍数
static size_t ROUND_UP(size_t bytes) {
return ((bytes) + __ALIGN-) & ~(__ALIGN-);
} union obj { // free-lists的节点构造
union obj *free_list_link;
char client_data[];
}; static obj *volatile free_list[_NFREELISTS];
static size_t FREELIST_INDEX(size_t bytes) {
return ((bytes) + (__ALIGN-)) / (__ALIGN-);
} // 返回一个大小为n的对象,并可能加入大小为n的其他区块到free list
static void *refill(size_t n);
// 配置一大块空间,可容纳 nobj 个大小为“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; // 内存池结束为止,同上
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);
}; template <bool threads, int inst>
char * __default_alloc_template<threads, inst>::start_free = ; template <bool threads, int inst>
char * __default_alloc_template<threads, inst>::end_free = ; template <bool threads, int inst>
size_t * __default_alloc_template<threads, inst>::heap_size = ; template <bool threads, int inst>
__default_alloc_template<threads, inst>::obj *volatile
__default_alloc_template<threads, inst>::free_list[_NFREELISTS] =
{, , , , , , , , , , , , , , , }; // n must > 0
template<bool threads, int inst>
void * __default_alloc_template<threads, inst>::allocate(size_t n) {
obj * volatile * my_free_list; // 一个数组,数组元素是obj*
obj * result; if (n > (size_t) __MAX_BYTES) {
return malloc_alloc::allocate(n);
} // 寻找16个free lists中适当的一个
my_free_list = free_list + FREELIST_INDEX(n);
result = *my_free_list;
if (result == ) {
// 没找到可用的free list,准备重新填充free list
void *r = refill(ROUND_UP(n));
return r;
} // 调整free list
*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) {
obj *q = (obj*)p;
obj * volatile * my_free_list; 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;
} template <bool threads, int inst>
void * __default_alloc_template<threads, inst>::refill(size_t n) {
int nobjs = ;
// 调用chunk_alloc(),尝试取得nobjs个区块作为free list的新节点
// 注意参数nobjs是pass by reference
char * chunk = chunk_alloc(n, nobjs);
obj * volatile * my_free_link;
obj * result;
obj * current_obj, * next_obj;
int i; // 如果只获得一个区块,这个区块就分配给调用者用,free list无新节点
if ( == nobjs) return chunk;
// 否则准备调整free link,纳入新节点
my_free_link = free_list + FREELIST_INDEX(n); // 以下是chunk空间内建立free list
result = (obj *)chunk;
// 以下引导free list指向新配置的空间(取自内存池)
*my_free_link = next_obj = (obj*) (chunk + n);
// 以下将free list的各节点串接起来
for (i=; ; ++i) { // 从1开始,因为第0个将返回给客户端
current_obj = next_obj;
next_obj = (obj *)((char *)next_obj + n);
if (nobjs - == i) {
current_obj -> free_list_link = ;
break;
} else {
current_obj -> free_list_link = next_obj;
}
}
return result;
} // 假设size已经上调至8的倍数
// 注意参数nobjs是pass by reference
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 {
// 内存池剩余空间连一个区块的大小都无法提供
size_t bytes_to_get = * total_bytes + ROUND_UP(heap_size >> );
// 以下试着让内存池中的残余零头还有利用价值
if (bytes_left > ) {
// 内存池内还有一些零头,先配给适当的free list
// 首先寻找适当的free list
obj * volatile * my_free_list = free_list + FREELIST_INDEX(bytes_left);
// 调整free list,将内存池中的残余空间编入
((obj *)start_free) -> free_list_link = *my_free_list;
*my_free_list = (obj *)start_free;
} // 配置heap空间,用来补充内存池
start_free = (char *)malloc(bytes_to_get);
if ( == start_free) {
// heap空间不足,malloc失败
int i;
obj * volatile * my_free_list, *p;
// 试着检视我们手上拥有的东西,这不会造成伤害。我们不打算尝试配置
// 较小的区块,因为那在多进程机器上容器导致灾难
// 以下搜寻适当的free list
// 所谓适当是指“尚未用区块,且区块够大”的free list
for (i=size; i <= __MAX_BYTES; i+=__ALIGN) {
my_free_list = free_list + FREELIST_INDEX(i);
p = *my_free_list;
if ( != p) { // free list内尚有未用块
// 调整free list以释放未用区块
*my_free_list = p -> free_list_link;
start_free = (char *)p;
end_free = start_free + i;
// 递归调用自己,为了修正nobjs
return chunk_alloc(size, nobjs);
// 注意,任何残余零头终将被编入适当的free list中备用
}
}
end_free = ; // 如果出现意外,调用第一级配置器,看看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);
}
}
STL源码剖析 — 空间配置器(allocator)的更多相关文章
- STL源码剖析——空间配置器Allocator#2 一/二级空间配置器
上节学习了内存配置后的对象构造行为和内存释放前的对象析构行为,在这一节来学习内存的配置与释放. C++的内存配置基本操作是::operator new(),而释放基本操作是::operator del ...
- STL源码剖析——空间配置器Allocator#1 构造与析构
以STL的运用角度而言,空间配置器是最不需要介绍的东西,因为它扮演的是幕后的角色,隐藏在一切容器的背后默默工作.但以STL的实现角度而言,最应该首先介绍的就是空间配置器,因为这是这是容器展开一切运作的 ...
- STL源码剖析——空间配置器Allocator#3 自由链表与内存池
上节在学习第二级配置器时了解了第二级配置器通过内存池与自由链表来处理小区块内存的申请.但只是对其概念进行点到为止的认识,并未深入探究.这节就来学习一下自由链表的填充和内存池的内存分配机制. refil ...
- STL源码剖析(空间配置器)
前言 在STL中,容器的定义中都带一个模板参数,如vector template <class T, class Alloc = alloc> class vector {...} 其中第 ...
- STL源码剖析:配置器
作用:对内存的管理 接口:申请和释放 内容: 几个全局函数 一级配置器 二级配置器 准备知识 POD是什么: Plain Old Data简称POD,表示传统的C语言类型:与POD类型对应的是非POD ...
- STL学习笔记:空间配置器allocator
allocator必要接口: allocator::value_type allocator::pointer allocator::const_pointer allocator::referenc ...
- 《STL源码剖析》相关面试题总结
原文链接:http://www.cnblogs.com/raichen/p/5817158.html 一.STL简介 STL提供六大组件,彼此可以组合套用: 容器容器就是各种数据结构,我就不多说,看看 ...
- 面试题总结(三)、《STL源码剖析》相关面试题总结
声明:本文主要探讨与STL实现相关的面试题,主要参考侯捷的<STL源码剖析>,每一个知识点讨论力求简洁,便于记忆,但讨论深度有限,如要深入研究可点击参考链接,希望对正在找工作的同学有点帮助 ...
- STL源码剖析之空间配置器
本文大致对STL中的空间配置器进行一个简单的讲解,由于只是一篇博客类型的文章,无法将源码表现到面面俱到,所以真正感兴趣的码农们可以从源码中或者<STL源码剖析>仔细了解一下. 1,为什么S ...
随机推荐
- Axure RP简单作品
点击按钮,同时出现1-7 点击按钮,依次出现1-7,
- CSS 语法
CSS 语法 CSS 规则由两个主要的部分构成:选择器,以及一条或多条声明: 选择器通常是您需要改变样式的 HTML 元素. 每条声明由一个属性和一个值组成. 属性(property)是您希望设置的样 ...
- C#数组随机生成四个随机数
int[] face = new int[4]; Random ra = new Random(); for (int i = 0; i < face.Length; i++) { int co ...
- Beta总结
45°炸 031502601 蔡鸿杰 031502604 陈甘霖 031502632 伍晨薇 一.写在Beta项目前 Beta 凡 事 预 则 立 二.GitHub传送门 Beta冲刺重要版本 三.用 ...
- 高级软件工程2017第6次作业--团队项目:Alpha阶段综合报告
高级软件工程2017第6次作业--团队项目:Alpha阶段综合报告 Deadline:2017-10-30(周一)21:00pm (注:以下内容参考集大作业4,集大作业5,集大作业6,集大作业7 一. ...
- Session的过期时间如何计算?
在生成session的时候,会设置一个session过期时间.session的过期时间并不是从生成session对象开始计算,超过过期时间,session就失效了. 而是每当一个浏览器请求,sessi ...
- python之路--day13-模块
1,什么是模块 模块就是系统功能的集合体,在python中,一个py文件就是一个模块, 例如:module.py 其中module叫做模块名 2,使用模块 2.1 import导入模块 首次带入模块发 ...
- System V IPC 之消息队列
消息队列和共享内存.信号量一样,同属 System V IPC 通信机制.消息队列是一系列连续排列的消息,保存在内核中,通过消息队列的引用标识符来访问.使用消息队列的好处是对每个消息指定了特定消息类型 ...
- dede使用心得
Question one: 最近做了一些视频教程传到优酷网站上,但我想引入这些视频教程到我的网站,在发表时我发现织梦CMS自带的编辑器又不直接支持优酷等视频网站的引用.所以为了方便教程的发布,特意在网 ...
- 用C#(.NET Core) 实现简单工厂和工厂方法模式
本文源自深入浅出设计模式. 只不过我是使用C#/.NET Core实现的例子. 前言 当你看见new这个关键字的时候, 就应该想到它是具体的实现. 这就是一个具体的类, 为了更灵活, 我们应该使用的是 ...