1.概述
整个STL的操作对象(所有的数值)都存放在容器之内,而容器一定需要配置空间以置放资料。
一般C++内存配置和释放操作是这样的:
class A {};
A* pa = new A;
//...执行其他操作
delete pa;
这其中的new算式内含两阶段操作:(1)调用:: operator new配置内存(2)调用Fo::F0o()构造对象内容。 delete算式也内含两阶段操作:(1)调用Foo::~foo()将对象析构;(2)调用:: operator delete释放内存
为了精密分工, STL allocator决定将这两阶段操作区分开来。内存配置操作由 alloc::allocate()负责,内存释放操作由 alloc:: deallocate()负责对象构造操作由:: construct()负责,对象析构操作由:: destroy()负责
2.构造和析构工具:全局函数construct()和 destroy()
<stl_construct.h>部分代码
#include <new.h> // 使用 placement new,需要先包含此文件。
// 将初值 __value 设定到指针所指的空间上。
template <class _T1, class _T2>
inline void _Construct(_T1* __p, const _T2& __value) {
new ((void*) __p) _T1(__value); // placement new,调用 _T1::_T1(__value);
}
template <class _T1>
inline void _Construct(_T1* __p) {
new ((void*) __p) _T1();
}
// 第一个版本,接受一个指针,准备将该指针所指之物析构掉。
template <class _Tp>
inline void _Destroy(_Tp* __pointer) {
__pointer->~_Tp();
}
// 第二版本,destroy 泛型特化
inline void _Destroy(char*, char*) {}
inline void _Destroy(int*, int*) {}
inline void _Destroy(long*, long*) {}
inline void _Destroy(float*, float*) {}
inline void _Destroy(double*, double*) {}
#ifdef __STL_HAS_WCHAR_T
inline void _Destroy(wchar_t*, wchar_t*) {}
#endif /* __STL_HAS_WCHAR_T */
上述 construct()接受一个指针p和一个初值 value,该函数的用途就是将初值设定到指针所指的空间上。C++的placement new运算子可用来完成这一任务。destroy()有两个版本,第一版本接受一个指针,准备将该指针所指之物析构掉。直接调用该对象的析构函数即可。第二版本接受 first和last两个迭代器,准备将[first,last)范围内的所有对象析构掉。
3.空间的配置与释放
<stl_alloc.h>
在SGI版本的STL中,空间的配置释放都由< stl_alloc.h > 负责。它的设计思想如下:向system heap要求空间,考虑多线程,考虑内存不足的应变措施,考虑内存碎片的问题.
考虑到小型区块所可能造成的内存破碎问题,SGI设计了双层级配置器,第一级配置器直接使用 malloc()和free),第二级配置器则视情况采用不同的策略:当配置区块超过128 bytes时,视之为“足够大”,便调用第一级配置器;当配置区块小于128 bytes时,视之为“过小”,为了降低额外负担,采用复杂的 memory pool整理方式,而不再求助于第一级配置器。
3.1第一级配置器
直接调用malloc和free来配置释放内存
// SGI STL 第一级配置器
// 无 “template 类型参数”,“非类型参数 __inst”,完全没有用
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);
}
};
----------------------------------
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);
}
}
template <int __inst>
void* __malloc_alloc_template<__inst>::_S_oom_realloc(void* __p, size_t __n)
{
void (* __my_malloc_handler)();
void* __result;
// 给一个已经分配了地址的指针重新分配空间,参数 __p 为原有的空间地址,__n 是重新申请的地址长度
for (;;) {
// 当 "内存不足处理例程" 并未被客户设定,便调用 __THROW_BAD_ALLOC,丢出 bad_alloc 异常信息
__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;
请注意,SGI第一级配置器的 allocate()和 realloc()都是在调用malloc()和 realloc()不成功后,改调用oom_ malloc()和oom_realloc()后两者都有内循环,不断调用“内存不足处理例程”,期望在某次调用之后,获得足够的内存而圆满完成任务。但如果“内存不足处理例程”并未被客端设定,oom_ malloc()和oom_ realloc()便老实不客气地调用 THROWBADALLOC,丢出 bad alloc异常信息,或利用exit(1)硬生生中止程序。
3.2第二级配置器
视情况采用不同的策略:当配置区块超过128 bytes时,视之为“足够大”,便调用第一级配置器;当配置区块小于128 bytes时,视之为“过小”,为了降低额外负担,采用复杂的 memory pool整理方式,而不再求助于第一级配置器
allocate函数
/* __n must be > 0 */
// 申请大小为n的数据块,返回该数据块的起始地址
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);
// Acquire the lock here with a constructor call.
// This ensures that it is released in exit or during stack
// unwinding.
# ifndef _NOTHREADS
/*REFERENCED*/
_Lock __lock_instance;
# endif
_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;
};
if用户需要的区块大于128bytes,则直接调用第一级空间配置器
else如果用户需要的区块小于等于128128bytes,则到自由链表free_list中去找{
if自由链表有,则直接去拿来用
else空闲链表没有可用数据块,就将区块大小先调整至 8 倍数边界,然后调用 _S_refill() 重新填充
}
deallocate函数
/* __p may not be 0 */
// 空间释放函数 deallocate()
static void deallocate(void* __p, size_t __n)
{
if (__n > (size_t) _MAX_BYTES)
malloc_alloc::deallocate(__p, __n); // 大于 128 bytes,就调用第一级配置器的释放
else {
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n); // 否则将空间回收到相应空闲链表(由释放块的大小决定)中
_Obj* __q = (_Obj*)__p;
// acquire lock
# ifndef _NOTHREADS
/*REFERENCED*/
_Lock __lock_instance;
# endif /* _NOTHREADS */
__q -> _M_free_list_link = *__my_free_list; // 调整空闲链表,回收数据块
*__my_free_list = __q;
// lock is released here
}
}
static void* reallocate(void* __p, size_t __old_sz, size_t __new_sz);
} ;
逻辑:
if区块大于128, 则直接由第一级空间配置器收回
else区块小于等于128, 则找到对应的自由链表,将区块收回
二级配置器和一级配置器一样都提供Allocate和Deallocate的接口
4.自由链表
自由链表是一个指针数组,有点类似hash桶,它的数组大小为16,每个数组元素代表所挂的区块大小,比如free _ list[0]代表下面挂的是8bytes的区块,free _ list[1]代表下面挂的是16bytes的区块…….依次类推,直到free _ list[15]代表下面挂的是128bytes的区块
内存池,以start _ free和 end _ free记录其大小,用于保存未被挂在自由链表的区块,它和自由链表构成了伙伴系统。自由链表对应的位置没有所需的内存块该怎么办,也就是Refill函数的实现。
//freelist没有可用区块,将要填充,此时新的空间取自内存池
static void* Refill(size_t n)
{
size_t nobjs = 20;
char* chunk = (char*)ChunkAlloc(n, nobjs); //默认获得20的新节点,但是也可能小于20,可能会改变nobjs
if (nobjs == 1) // 如果只获得一个数据块,那么这个数据块就直接分给调用者,空闲链表中不会增加新节点
return chunk;
//有多块,返回一块给调用者,其他挂在自由链表中
Obj* ret = (Obj*)chunk;
Obj* cur = (Obj*)(chunk + n);
Obj* next = cur;
Obj* volatile *myFreeList = FreeList + FreeListIndex(n);
*myFreeList = cur;
for (size_t i = 1; i < nobjs; ++i)
{
next = (Obj*)((char*)cur + n);
cur->freeListLink = next;
cur = next;
}
cur->freeListLink = NULL;
return ret;
}
从内存池中取空间给freelist使用,是_S_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);
// Try to make use of the left-over piece.
// 内存池的剩余空间分给合适的空闲链表
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;
}
_S_start_free = (char*)malloc(__bytes_to_get); // 配置 heap 空间,用来补充内存池
if (0 == _S_start_free) { // heap 空间不足,malloc() 失败
size_t __i;
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __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 <= (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));
// Any leftover piece will eventually make it to the
// right free list.
}
}
_S_end_free = 0; // In case of exception.
_S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get); // 调用第一级配置器
// This should either throw an
// exception or remedy the situation. Thus we assume it
// succeeded.
}
_S_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;
return(_S_chunk_alloc(__size, __nobjs)); // 递归调用自己
}
}
上述的 chunk_alloc()函数以end_free- start free来判断内存池的水量。如果水量充足,就直接调出20个区块返回给 free list如果水量不足以提供20个区块,但还足够供应一个以上的区块,就拨出这不足20个区块的空间出去。这时候其 pass by reference的 nobjs参数将被修改为实际能够供应的区块数。如果内存池连一个区块空间都无法供应,对客端显然无法交待,此时便需利用malloc()从heap中配置内存,为内存池注入活水源头以应付需求。新水量的大小为需求量的两倍,再加上一个随着配置次数增加而愈来愈大的附加量.以上便是整个第二级空间配置器的设计。
5.总结
我们知道,引入相对复杂的空间配置器,主要源自两点:
1、频繁使用malloc、free开辟释放小块内存带来的性能效率的低下
2、内存碎片问题,导致不连续内存不可用的浪费
引入两层配置器帮我们解决了以上的问题,但是也带来一些问题:
1、内存碎片的问题,自由链表所挂区块都是8的整数倍,因此当我们需要非8倍数的区块,往往会导致浪费。
2、我们并没有释放内存池中的区块。释放需要到程序结束之后。这样子会导致自由链表一直占用内存,其它进程使用不了。