整理至侯捷的《STL源码剖析》
一般的C++内存配置操作为
class Foo { ... };
Foo *pF=new Foo;
delete pF;
new运算符包含两步操作:
- 调用 ::operator new 配置内存
- 调用 Foo 的构造函数构造对象
delete运算符也包含两步操作:
- 调用 Foo 的析构函数析构对象
- 调用 ::operator delete 释放内存
STL空间配置器就是完成这两阶段的操作:
- 内存配置为alloc::allocate()
- 内存释放为alloc::deallocate()
定义在stl_alloc.h中
- 对象构造::construct()
- 对象析构::destroy()
定义在stl_construct.h中
构造和析构
源代码为:
#ifndef __SGI_STL_INTERNAL_CONSTRUCT_H
#define __SGI_STL_INTERNAL_CONSTRUCT_H
#include <new.h>
__STL_BEGIN_NAMESPACE
// construct and destroy. These functions are not part of the C++ standard,
// and are provided for backward compatibility with the HP STL. We also
// provide internal names _Construct and _Destroy that can be used within
// the library, so that standard-conforming pieces don't have to rely on
// non-standard extensions.
// Internal names
template <class _T1, class _T2>
inline void _Construct(_T1* __p, const _T2& __value) {
new ((void*) __p) _T1(__value); //placement new,在__p指向的内存空间上构造对象,调用有参构造函数
}
template <class _T1>
inline void _Construct(_T1* __p) {
new ((void*) __p) _T1(); //placement new,在__p指向的内存空间上构造对象,调用无参构造函数
}
template <class _Tp>
inline void _Destroy(_Tp* __pointer) {
__pointer->~_Tp(); //接受一个指针操作的析构函数版本,调用指针所指对象的析构函数
}
template <class _ForwardIterator>
void
__destroy_aux(_ForwardIterator __first, _ForwardIterator __last, __false_type)
{
for ( ; __first != __last; ++__first)
destroy(&*__first);
}
template <class _ForwardIterator>
inline void __destroy_aux(_ForwardIterator, _ForwardIterator, __true_type) {}
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());
}
//接受两个迭代器参数的析构函数版本,判断迭代器所指类型中析构函数是否是不重要的,若是则
//什么也不做,反之则遍历执行依次执行迭代器范围内的析构函数。
template <class _ForwardIterator>
inline void _Destroy(_ForwardIterator __first, _ForwardIterator __last) {
__destroy(__first, __last, __VALUE_TYPE(__first));
}
//针对接受内置基本类型指针参数的特化版本,什么也不做
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 */
// --------------------------------------------------
// Old names from the HP STL.
//包装内部函数,对外提供接口
template <class _T1, class _T2>
inline void construct(_T1* __p, const _T2& __value) {
_Construct(__p, __value);
}
template <class _T1>
inline void construct(_T1* __p) {
_Construct(__p);
}
template <class _Tp>
inline void destroy(_Tp* __pointer) {
_Destroy(__pointer);
}
template <class _ForwardIterator>
inline void destroy(_ForwardIterator __first, _ForwardIterator __last) {
_Destroy(__first, __last);
}
__STL_END_NAMESPACE
#endif /* __SGI_STL_INTERNAL_CONSTRUCT_H */
空间的配置与释放
SGI STL的设计要求是:
- 向系统堆中申请空间
- 考虑多线程
- 考虑内存不足时的处理
- 考虑过多申请小型区块造成的内存碎片的问题。
下面分析不考虑多线程处理。
考虑内存碎片的问题,SGI STL设计了双层级配置器。第一级配置器直接使用malloc()和free()函数,并且考虑内存不足时的处理;第二级配置器策略为:当配置区块超过128字节时,调用第一级配置器,当小于128字节时,采用内存池的方式。我们可以配置使用哪种方式,默认采用的是第二级配置器。
第一级配置器
第一级配置器采用模板类__malloc_alloc_template实现,静态成员函数allocate实现分配内存,reallocate函数实现重分配内存,deallocate函数实现重新分配内存。
- deallocate函数比较简单,直接free掉指针指向的内存空间就行。
- allocate函数首先调用malloc分配指定字节的内存,如果malloc调用失败,没有可用空间内存,则指向out of memory处理函数_S_oom_malloc。在这个函数中是一个for循环,不断的尝试释放、配置的过程,首先如果我们没有定义内存不足处理函数__malloc_alloc_oom_handler(函数指针,默认情况下值为0),那么就会抛出异常__THROW_BAD_ALLOC,一般需要我们自己定义内存不足处理函数,通过函数static void (* __set_malloc_handler(void (*__f)()))()(该函数是参数和返回值都是函数指针的写法,typedef void(*pFun)(),pFun __set_malloc_handler(pFun p)),我们可以将我们自己实现的handler覆盖__malloc_alloc_oom_handler,当有足够的空间可用的时候然后就返回。
- reallocate函数类似allocate
源码:
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:
static void* allocate(size_t __n)
{
void* __result = malloc(__n);
if (0 == __result) __result = _S_oom_malloc(__n);
return __result;
}
static void deallocate(void* __p, size_t /* __n */)
{
free(__p);
}
static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
{
void* __result = realloc(__p, __new_sz);
if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
return __result;
}
/*
设置自己实现的内存不足处理函数,通过函数指针传参的方法,返回之前的内存不足处理函数指针,该函数的参数和返回值都是函数指针,typedef void (*pFun)()
*/
static void (* __set_malloc_handler(void (*__f)()))()
{
void (* __old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = __f;
return(__old);
}
};
// malloc_alloc out-of-memory handling
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
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); //分配成功则返回
}
}
template <int __inst>
void* __malloc_alloc_template<__inst>::_S_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);
}
}
第二级配置器
我们知道每次申请内存(malloc),都需要额外负担(overhead),操作系统需要来记录我们的申请的内存,这样如果我们每次申请的内存区块都很小,额外负担所占的比例就越大。因此SGI STL中引入了第二级配置器(默认配置器)。具体做法就是如果申请内存大于128字节,就认为区块足够大,转交给第一级配置器,如果小于128字节,就申请一块大的内存,并且将它维护在一个链表中,下次,若有相同大小内存需求,就直接重链表中取,释放了后,然后又回收到链表中。
具体实现定义在模板类__default_alloc_template中。
空间配置涉及到三个函数:
- allocate函数,首先判断请求的字节数,如果大于128字节,则直接调用第一级配置器,返回请求的内存。如果小于128字节,则在链表中寻找可用的区块,找到则返回,没找到则调用_S_refill重新填充链表,最后进行调整链表的指向。
//链表节点,采用union,一物两用,节省内存开销
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
};
/* __n must be > 0 */
static void* allocate(size_t __n)
{
void* __ret = 0;
//大于128字节调用第一级配置器
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n);
}
else {
_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;
if (__result == 0) //没找到,重新填充链表
__ret = _S_refill(_S_round_up(__n));
else { //找到了,调整链表指向
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
}
}
return __ret;
};
- _S_refill函数,首先调用_S_chunk_alloc函数在内存池中申请区块,默认准备获得20个区块,如果只获得了1个区块,这将这个区块分配给申请者,否则准备调整链表,插入新节点。
/* Returns an object of size __n, and optionally adds to size __n free list.*/
/* We assume that __n is properly aligned. */
/* We hold the allocation lock. */
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20; //申请20个区块,实际得到的可能小于20
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);
/* Build free list in chunk */
__result = (_Obj*)__chunk; //第一个区块返回给申请者
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n); //剩下的插入链表中
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);
}
- _S_chunk_alloc函数,从内存池中申请空间给链表用。首先判断内存池中剩余空间是否满足需求量。如果满足,则返回请求的空间;如果不能满足但有一个以上的区块,则返回最大能供应的区块个数;如果一个区块都不能满足,则首先将剩余的空间编入适当的链表中,然后则从系统堆中申请空间来补充内存池,如果堆申请失败,则进入一个for循环,搜寻适当的链表区块,成功则返回,否则调用第一级配置器。
/* We allocate memory in large chunks in order to avoid fragmenting */
/* the malloc heap too much. */
/* We assume that size is properly aligned. */
/* We hold the allocation lock. */
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);
if (0 == _S_start_free) { //堆空间不足
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) { //free list中有未用区块
//调整链表,释放出未用区块
*__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)); //递归调用自己,修正__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)); //递归调用自己,修正__nobjs(申请的区块数)
}
}