源码下载地址:
空间分配函数
首先需要了解内存分配的一个过程:程序调用=》allocator=》new=》malloc=》操作系统分配。
然后要理解三个有关于new的概念
- new operator:分配内存并初始化对象,不可被重载
- operator new:只分配内存,不初始化对象,可被重载,相当于C的malloc
- placement new:对分配好的内存进行初始化
Foo * p = new (ptr) Foo(value); // ptr指向已经分配内存的起始位置
SGI配置器
SGI STL的配置器与标准规范不同,其名称为alloc而非allocator,不接受任何参数。所以在VC或CB需要这么指定配置器:
vector<int,std::allocator> iv;
而在GCC中,需这样指定:
vector<int,std::alloc> iv;
但其实SGI STL也支持allocator,只是效率不佳,仅仅对::operator new做了一层薄薄的封装。
一般来说,以下代码分为两阶段:(1)调用::operator new配置内存;(2)调用Foo::Foo()构造对象内容。
Foo* pf=new Foo;
STL alloc将这两阶段分开,内存配置交给alloc::allocate()负责,对象构造交给::construct()负责。
配置器定义于< memory>中,SGI 内含两个文件
<stl_alloc.h>:负责内存空间的配置和释放
<stl_construct.h>:负责对象内容的构造和析构
还有个重要的文件 <stl_uninitialized.h>:用来fill或copy大块内存数据
stl_construct.h
定义了全局函数construct() 和 destroy()
construct() 核心代码
// 将初值 __value 设定到指针所指的空间上。
template <class _T1, class _T2>
inline void _Construct(_T1* __p, const _T2& __value) {
new ((void*) __p) _T1(__value); // placement new,调用 _T1::_T1(__value);
}
destroy()核心代码
// 第一个版本,接受一个指针,准备将该指针所指之物析构掉。
template <class _Tp>
inline void _Destroy(_Tp* __pointer) {
__pointer->~_Tp();
}
// 第二个版本,接收两个迭代器。
// 调用 __VALUE_TYPE() 获得迭代器所指对象的类别
template <class _ForwardIterator>
inline void _Destroy(_ForwardIterator __first, _ForwardIterator __last) {
__destroy(__first, __last, __VALUE_TYPE(__first));
}
其实destroy还有一个需要判断的地方:
判断value_type时,如果这些元素有自己的析构函数,则这个析构函数叫做non-trivial destructor(构造函数、析构函数、拷贝构造函数、复制构造函数都可能是non-trivial)。
POD类型是C++的内建类型或传统C结构体类型,必有trivial ctor/dtor/copy/assignment四种函数。
若value_type没有non-trivail destructor,则说明这个对象的析构是无意义的,因而不需要一次又一次地调用那些无关痛痒的析构函数,直接调用malloc()、memcpy()等内存操作提高性能。
判断value_type是否有non-trival destrory的方法:
// 利用__type_traits<T>判断一个对象的所述类,进而得知该类型的析构函数是否需要做什么。
// 如果为true,则什么也不用做;否则,逐个调用第一个版本的析构函数
template <class _ForwardIterator, class _Tp>
inline void
__destroy(_ForwardIterator __first, _ForwardIterator __last, _Tp*)
{
typedef typename __type_traits<_Tp>::has_trivial_destructor
_Trivial_destructor;
__destroy_aux(__first, __last, _Trivial_destructor());
}
空间的配置和释放:alloc配置器
stl标准的空间配置为allocator,SGI也实现了这个版本的配置器,但由于其效率不高,SGI STL默认的配置器为alloc。
考虑到小型区块可能造成的内存破碎问题,SGI设计了双层级配置器:当配置区块超过128bytes时,便调用第一级配置器;如果小于128bytes,为降低额外开销overhead(稍后再讲),便采用复杂的memory pool整理方式。
为使得alloc符合STL规格,SGI将Alloc做了一层封装:
第一级配置器 __malloc_alloc_template
template <int __inst>
class __malloc_alloc_template {
private:
// 以下函数将用来处理内存不足的情况
static void* _S_oom_malloc(size_t);
static void* _S_oom_realloc(void*, size_t);
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
static void (* __malloc_alloc_oom_handler)();
#endif
public:
// 第一级配置器直接调用 malloc()
static void* allocate(size_t __n)
{
void* __result = malloc(__n);
// 以下无法满足需求时,改用 _S_oom_malloc()
if (0 == __result) __result = _S_oom_malloc(__n);
return __result;
}
// 第一级配置器直接调用 free()
static void deallocate(void* __p, size_t /* __n */)
{
free(__p);
}
// 第一级配置器直接调用 realloc()
static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
{
void* __result = realloc(__p, __new_sz);
// 以下无法满足需求时,改用 _S_oom_realloc()
if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
return __result;
}
// 以下仿真 C++ 的 set_new_handler(),可以通过它指定自己的 out-of-memory handler
// 为什么不使用 C++ new-handler 机制,因为第一级配置器并没有 ::operator new 来配置内存
// 可能是历史原因
static void (* __set_malloc_handler(void (*__f)()))()
{
void (* __old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = __f;
return(__old);
}
};
当内存分配失败后,将通过out-of-memory handler尝试分配新内存,以_S_oom_malloc为例:
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
// 初值为0,由客户自行设定
template <int __inst>
void (* __malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;
#endif
template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_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);
}
}
C++ new handler机制:你可以要求系统在内存配置需求无法满足时,在抛出std::bad_alloc异常状态前,调用一个你所指定的函数,这个函数就称为new-handler。
第二级配置器 __default_alloc_template
malloc向系统申请内存时,需配置额外负担以管理内存,内存块越小,额外负担所占比例就越大。而在C++中,大部分内存都是小块的(如变量),如果小块内存也直接malloc,将造成大量的空间浪费。
编辑
在上图中,蓝色部分为实际数据,绿色部分为字节补齐部分,上下两端的红色部记录内存块的大小(这也是为什么,在free一块内存时,只需要指导这块内存的首地址即可)。红色的部分称为cookie,两个cookie各占4字节。至于为什么上下两端都有,暂时不知道。
因此,STL尽量减少malloc的次数,设置了如下的自由链表free-lists:
free-list的节点结构为:
union _Obj {
union _Obj* _M_free_list_link; // 利用联合体特点
char _M_client_data[1]; /* The client sees this. */
};
free-list有三个值得注意的点:
- union的妙用,可以节省自由链表中指针的额外开销:在当前节点未分配前,节点中的值为下一个节点的地址;当节点分配后,free_list头指针指向它的下一个节点,而它内部的值被替换为用户的data值。=》从这也看出,自由链表从头节点分配效率更高。
- 区块按照8字节对齐:8、16、24、...、128bytes
- 配置器不仅负责区块的分配器,也负责区块的回收,也就是把一个区块挂到对应的free-list
如果自由链表中没有可分配的节点了,将从内存池获取内存并分配节点:
// 配置一大块空间,可容纳nobjs个大小为 size的区块
// 如果内存池空间也不够了,nobjs可能会降低
static char* _S_chunk_alloc(size_t __size, int& __nobjs);
// Chunk allocation state.
static char* _S_start_free; // 内存池起始位置。只在 _S_chunk_alloc() 中变化
static char* _S_end_free; // 内存池结束位置。只在 _S_chunk_alloc() 中变化
static size_t _S_heap_size;
同样,二级配置器也需要符合STL标准接口,需要封装函数 allocate():
static void* allocate(size_t __n)
{
void* __ret = 0;
// 如果需求区块大于 128 bytes,就转调用第一级配置
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n);
}
else {
// 根据申请空间的大小寻找相应的空闲链表(16个空闲链表中的一个)
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
_Obj* __RESTRICT __result = *__my_free_list;
// 空闲链表没有可用数据块,就将区块大小先调整至 8 倍数边界,然后调用 _S_refill() 重新填充
if (__result == 0)
__ret = _S_refill(_S_round_up(__n));
else {
// 如果空闲链表中有空闲数据块,则取出一个,并把空闲链表的指针指向下一个数据块
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
}
}
return __ret;
};
回收区块:deallocate(),其中关于线程安全的代码未粘贴
// 空间释放函数 deallocate()
static void deallocate(void* __p, size_t __n)
{
// 大于 128 bytes,就调用第一级配置器的释放
if (__n > (size_t) _MAX_BYTES)
malloc_alloc::deallocate(__p, __n);
else {
// 否则将空间回收到相应空闲链表(由释放块的大小决定)中
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
_Obj* __q = (_Obj*)__p;
// 调整空闲链表,回收数据块
__q -> _M_free_list_link = *__my_free_list;
*__my_free_list = __q;
}
}
free lists的填充
在allocate时,如果没有可用区块时,就调用refill(),为free list重新填充空间。新的空间取自内存池(经由chunk_alloc()完成),取得20个新节点,如果内存池空间也不足,获得的节点数可能小于20。以下为refill()源码:
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20;
// 调用 _S_chunk_alloc(),缺省取 20 个区块作为 free list 的新节点
char* __chunk = _S_chunk_alloc(__n, __nobjs);
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
// 如果只获得一个数据块,那么这个数据块就直接分给调用者,空闲链表中不会增加新节点
if (1 == __nobjs) return(__chunk);
// 否则根据申请数据块的大小找到相应空闲链表
__my_free_list = _S_free_list + _S_freelist_index(__n);
/* 在chunk空间建立free list */
__result = (_Obj*)__chunk;
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
// 从1开始,第0个数据块给调用者,地址访问即chunk~chunk + n - 1
for (__i = 1; ; __i++) { // 头插法
__current_obj = __next_obj;
__next_obj = (_Obj*)((char*)__next_obj + __n);
if (__nobjs - 1 == __i) {
__current_obj -> _M_free_list_link = 0;
break;
} else {
__current_obj -> _M_free_list_link = __next_obj;
}
}
return(__result);
}
内存池取内存的过程chunk_alloc() :
// 从内存池中取空间
template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size,
int& __nobjs)
{
char* __result;
size_t __total_bytes = __size * __nobjs; // 需要申请空间的大小
size_t __bytes_left = _S_end_free - _S_start_free; // 计算内存池剩余空间
if (__bytes_left >= __total_bytes) { // 内存池剩余空间完全满足申请
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
} else if (__bytes_left >= __size) { // 内存池剩余空间不能满足申请,提供一个以上的区块
__nobjs = (int)(__bytes_left/__size);
__total_bytes = __size * __nobjs;
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
} else { // 内存池剩余空间连一个区块的大小都无法提供
size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
// 内存池的剩余空间分给合适的空闲链表
if (__bytes_left > 0) {
_Obj* __STL_VOLATILE* __my_free_list =
_S_free_list + _S_freelist_index(__bytes_left);
((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
*__my_free_list = (_Obj*)_S_start_free;
}
// 配置 heap 空间,用来补充内存池
_S_start_free = (char*)malloc(__bytes_to_get);
if (0 == _S_start_free) { // heap 空间不足,malloc() 失败
size_t __i;
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __p;
for (__i = __size;
__i <= (size_t) _MAX_BYTES;
__i += (size_t) _ALIGN) {
__my_free_list = _S_free_list + _S_freelist_index(__i);
__p = *__my_free_list;
if (0 != __p) {
*__my_free_list = __p -> _M_free_list_link;
_S_start_free = (char*)__p;
_S_end_free = _S_start_free + __i;
return(_S_chunk_alloc(__size, __nobjs));
}
}
_S_end_free = 0;
// 调用第一级配置器
_S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
}
_S_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;
return(_S_chunk_alloc(__size, __nobjs)); // 递归调用自己
}
}
-
内存池空间完全满足容量,直接分配
-
内存池空间不足,但能分配一个以上的区块,则先给分配出来给调用方用着先
-
如果一个区块都分配不了了,就准备扩充内存池了,在此之前,需将内存池中较小的零头区块分配给合适的free list。(96bytes的没有,可能有16bytes的)
-
调用malloc补充内存池
- 假如malloc这里配置了40+n(附加量)个96bytes的区块,一个交给调用方,19个交给free list,另外20+n(附加量)个交给内存池。
-
如果heap空间不足,malloc失败,那就从大块的free list找有没有空闲的节点,有的话就把这个区块释放了,拿给小区块的free list用用。
-
如果free list中的大区快也都用完了,只能借助第一级配置器(借用里面的out-of-memory机制,看有没有哪个程序可以释放一部分资源出来)
内存池的实际操作:
tip:
对于容器,如vector,默认的配置器为alloc(也可以自己指定为allocator),而alloc配置器默认使用二级配置器。
空间配置讲完了,那如何在分配的空间上进行数据操作呢?前面讲过了construct()和destroy(),另外还有三个填充类函数,其实际定义于<stl_uninitialized>:
一、uninitialized_copy:区间拷贝
// 内存的配置与对象的构造行为分离开来。
template <class _InputIter, class _ForwardIter>
inline _ForwardIter
uninitialized_copy(_InputIter __first, _InputIter __last,
_ForwardIter __result)
{
return __uninitialized_copy(__first, __last, __result,
__VALUE_TYPE(__result));
}
简单来说就是首先得到__result的 value_type(如是是否POD类型,判断是否含有non-trivial 函数),进而判断是调用construct() 还是 直接调用memmove()或copy() ;然后,就是把 [__first,__last)这个区间上的数据,挨个复制到目标容器上。
可以看出__result 这个迭代器有两个功能:指示目标容器的起点;指示迭代器所指元素的类型。
以下是这个函数的进一步转调:
// 这个函数用来过渡的,这种技法在类型推导中比较常见
template <class _InputIter, class _ForwardIter, class _Tp>
inline _ForwardIter
__uninitialized_copy(_InputIter __first, _InputIter __last,
_ForwardIter __result, _Tp*)
{
typedef typename __type_traits<_Tp>::is_POD_type _Is_POD;
return __uninitialized_copy_aux(__first, __last, __result, _Is_POD());
}
// 如果是trival construct,直接内存拷贝
template <class _InputIter, class _ForwardIter>
inline _ForwardIter
__uninitialized_copy_aux(_InputIter __first, _InputIter __last,
_ForwardIter __result,
__true_type)
{
return copy(__first, __last, __result);
}
// 如果是non-trival construct,挨个复制
template <class _InputIter, class _ForwardIter>
_ForwardIter
__uninitialized_copy_aux(_InputIter __first, _InputIter __last,
_ForwardIter __result,
__false_type)
{
_ForwardIter __cur = __result;
__STL_TRY {
for ( ; __first != __last; ++__first, ++__cur)
_Construct(&*__cur, *__first);
return __cur;
}
__STL_UNWIND(_Destroy(__result, __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);
}
二、uninitialized_fill:区间单值填充
template <class _ForwardIter, class _Tp>
inline void uninitialized_fill(_ForwardIter __first,
_ForwardIter __last,
const _Tp& __x)
{
__uninitialized_fill(__first, __last, __x, __VALUE_TYPE(__first));
}
可以看出,就是将__x的值复制到[__first,__last),复制的技法和uninitialized_copy类似。注意这里的__first和__last是目标迭代器。
三、uninitialized_fill_n
template <class _ForwardIter, class _Size, class _Tp>
inline _ForwardIter
uninitialized_fill_n(_ForwardIter __first, _Size __n, const _Tp& __x)
{
return __uninitialized_fill_n(__first, __n, __x, __VALUE_TYPE(__first));
}
和上一个函数很相近,只不过是把区间终点换成区间长度了。
总结:以上三个函数,其实就是结合转调实现泛化和特化的一个过程。
个人感觉:char* 类型的特化,有点像打补丁,应该归属 __true_type那个版本中去,然后再特化,不调用copy()而调用memmove(),这样更好理解一些。