C++ STL 容器在一定程度上简化了代码的书写过程。以STL运用的角度而言,我们并不需要了解空间配置器,因为它总是隐藏在一切组件的背后;但是如果我们就STL的实现角度而言,我们则需要深入了解容器空间配置器。本博文主要讲述SGI STL一级空间配置器
目录
allocate()/deallocate()/reallocate()
一般而言,我们所习惯的 C++ 内存配置操作和释放操作是这样的:
class FOO{...}
Foo* pf = new FOO; //配置内存,然后析构对象
delete pf; //将对象析构,然后释放内存
我们知道其中new算是内容含两阶段的操作:<1>调用 ::operator new配置内存;<2>调用Foo::Foo()构造对象内容。delete算式也包含两阶段操作:<1>调用Foo::~Foo()将对象析构;<2>调用::operator delete 释放内存。
STL为了提高效率则把两者分开,内存配置操作由 alloc::allocate() 负责,内存释放操作由 alloc;:deallocate()负责,对象的构造由 ::construct() 负责,对象的析构由::destroy()负责。
STL标准规格将配置器定义于<memory>之中,SGI<memory>内包含一下两个文件:
#include<stl_alloc.h> //负责内存空间的配置于释放
#include<stl_construct.h> //负责对象内容的构造和析构
在深入探究复杂的内存动态配置与释放之前,我们先来看一下construct()和destroy()这两个函数是如何完成对象的构造和析构。、
下面是<stl_construct.h>的部分内容:
#include<new.h> //欲使用 placement new,需包含此文件
//construct
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();
}
//destroy第一个版本,接收一个指针
template <class _Tp>
inline void _Destroy(_Tp* __pointer)
{
__pointer->~_Tp(); //调用~_Tp()
}
上述construct()接收一个指针p和一个初始值value,该函数的用法就是将初始值设定到指针所指向的空间上,C++的placement new可以来完成这一工作。placement new 允许你在一个已经分配好的内存中构造一个新的对象。STL借助C++中的placement new 来提高效率,因为使用new操作符分配内存需要在堆中查找足够大的剩余空间,这个操作速度是很慢的 ,而且可能出现无法分配内存的异常。但是借助placement new 就可以解决这个问题,我们构造对象实在一个预先准备好的内存缓冲区中进行,不需要查找内存,而且不会出现程序在运行中途出现内存不足的异常。对于destroy()它接受一个指针,准备将指针所指之物析构掉,只需要调用该对象的析构函数即可。对于对象的析构,STL 还提供了第二个版本的destroy(),该函数接收first,last这个迭代器。我们从<stlstl_construct.h>中找出相关源码并对其进行分析:
//destroy第二版本 接受两个迭代器
//如果元素的数值型别(value type)有non_trivial destructor...
template <class _ForwardIterator>
void
__destroy_aux(_ForwardIterator __first, _ForwardIterator __last, __false_type)
{
for ( ; __first != __last; ++__first)
destroy(&*__first);
}
//如果元素的数值型别(value type)有trivial destructor...
template <class _ForwardIterator>
inline
void __destroy_aux(_ForwardIterator, _ForwardIterator, __true_type) {}
//判断元素的数值型别(value type)是否有trivial destructor...
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));
}
第二版本destroy()接受first和last两个迭代器 ,准备将[first,last]范围内的所有对象析构掉,我们不知道范围有多大,万一很大,而每个对象的析构函数都无关痛痒(所谓trivial destructor)那么一次次调用这些无关痛痒的析构函数,对效率是一种伤害。因此,这里首先利用value_type()来获取迭代器所指对象的型别,在利用_type_traits<T>判断该型别的析构函数是否无关痛痒。若是(_true_type),则什么也不做就结束;若是(_false_type),这才循环方式访寻整个范围,并在循环中每经历一个对象就调用第一个版本的destroy()。
另外destroy()第二版本还针对迭代器类型为char*和wchar_t*定义了特例化版本
inline void _Destroy(char*, char*) {}
inline void _Destroy(wchar_t*, wchar_t*) {}
下面图片源自于《STL源码分析》,有利于深入理解construct()和destroy()。
allocate()/deallocate()/reallocate()
在了解了对象的构造和析构行为后,我们现在来看一下内存的配置与释放。
对象构造前的空间配置和对象析构后内存释放,由<stl_alloc.h>负责,SGI对此设计的哲学如下:
- 向system heap 要求空间。
- 考虑多线程(multi-threads)状态。
- 考虑内存不足时的应变措施。
- 考虑“小型区块”可能造成的内存碎片问题。
C++的内存配置基本操作是::operator new(),内存释放基本操作是::operator delete(),这两个全局函数相当于C中的malloc()和free()函数。是的,正是如此,SGI正是以malloc()和free()完成内存的配置与释放。
考虑到小型区块可能造成的内存碎片问题(关于内存碎片,可以参考https://mp.youkuaiyun.com/postedit/89482835),SGI设计了双层级配置器,第一级配置器直接使用malloc()和free(),第二级则通过实现了一个内存池(memory pool)来实现内存的开辟与释放。那么我们到底只开放一级空间配置器还是同时开放二级空间配置器,则取决于宏
__USE_MALLOC是否被定义
# ifdef __USE_MALLOC
...
//__malloc_alloc_template 一级空间配置器
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc; //令alloc为第一级配置器
# else
...
//__default_alloc_template 二级空间配置器
//令alloc为第二级配置器
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;
#endif
无论alloc被定义为第一级或第二级配置器,SGI还为它再包装一个接口,使之符合STL规格:
template<class _Tp, class _Alloc>
class simple_alloc {
public:
static _Tp* allocate(size_t __n)
{ return 0 == __n ? 0 : (_Tp*) _Alloc::allocate(__n * sizeof (_Tp)); }
static _Tp* allocate(void)
{ return (_Tp*) _Alloc::allocate(sizeof (_Tp)); }
static void deallocate(_Tp* __p, size_t __n)
{ if (0 != __n) _Alloc::deallocate(__p, __n * sizeof (_Tp)); }
static void deallocate(_Tp* __p)
{ _Alloc::deallocate(__p, sizeof (_Tp)); }
};
其内部四个成员函数都是单纯的转调用,调用传递给配置器(可能是第一级配置器也可能是第二级配置器)的成员函数。这个接口使配置器的配置单位从bytes转为个别元素的大小(sizeof(T))。SGI STL全部容器都用simple_alloc接口。
在了解了以上知识后,我们来学习一下本片博客的重点,一级空间配置器_malloc_alloc_template
先说allocate()
template <int __inst>
class __malloc_alloc_template
{
private:
//oom : out of memory
//以下函数都是用来处理内存不足的情况
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); //第一空间配置器直接使用malloc(),向system heap 要求空间
if (0 == __result)
__result = _S_oom_malloc(__n); //malloc()函数调用失败后,改用_S_oom_malloc()
return __result;
}
上面提出当我们调用malloc失败后,会改调用_S_oom_malloc(),下面我们给出他的源码并进行分析:
#define __THROW_BAD_ALLOC fprintf(stderr, "out of memory\n"); exit(1)
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
template <int __inst>
void (* __malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;
//内存不足处理例程,初值为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;
//由于初始值设置为0,如果用户没有自定义相应的内存不足处理例程,那么还是会抛出异常
if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }
(*__my_malloc_handler)(); //用户有自定义(释放内存),则进入相依的处理程序
__result = malloc(__n);
if (__result) return(__result);
//不断的尝试释放和配置是因为用户不知道还需要释放多少内存来满足分配需求,只能逐步的释放内存
}
}
对于这个函数,他并非代码所写for(;;) 是一个死循环,它有两个退出条件:<1>用户没有定义相应的内存不足处理例程,即没有通过释放内存来解决现有内存分配不足的问题,结果抛出异常,直接退出。<2>在用户定义了释放内存程序例程后,成功分配指定大小的内存,返回指向该内存区域的首地址。
对于reallocate(),它与allocate()类型,下面直接给出源码。
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;
}
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);
}
}
对于一级空间配置器释放内存dellocate()则直接是调用free()函数,下面给出源码:
static void deallocate(void* __p, size_t /* __n */)
{
free(__p);
}
我们注意上面一级空间配置器的源码,可以清楚的看出,一级空间配置器是以malloc()、realloc()、free()等C函数执行实际的内存配置、释放、重配置操作并非使用::operator new来配置内存,因此,SGI不能直接使用C++的set_new_handler(),必须仿真一个类似的set_malloc_handler()。
以下仿真C++的 set_malloc_handler(),换句话说,你可以通过它指定你自己的out_of_memory handler。
//函数声明其实很简单,该函数接收一个返回值为空,参数为空的函数指针作为形参,最后返回一个返回值和参数
//均为空的函数指针
static void (* __set_malloc_handler(void (*__f)()))()
{
void (* __old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = __f;
return(__old);
}
这个函数重新指定了内存分配异常处理函数,并返回了原有的内存分配异常处理函数,即设置新处理例程的同时也保留了原有的处理例程。allocate()和reallocate()函数中的用户定义内存不足异常处理例程就是通过它来指定的。
最后,做出总结,SGI第一级配置器的allocate()和reallocate()都是在调用malloc()和realloc()函数失败后,改调用_S_oom_malloc()和_S_oom_realloc()。后两者都有内循环,不断调用“内存不足处理例程”,期望在某次调用之后,可以获取足够的内存。如果客户端没有定义“内存不足处理例程,_S_oom_malloc()和_S_oom_realloc()便会调用__THROW_BAD_ALLOC,丢出bad_alloc异常信息,然后直接利用exit(1)中止程序。因此,可以知道,“内存不足处理例程”SGI STL并不为我们提供,需要我们自己设计并实现。
对于第二级配置器可以参考我的其他博文:https://blog.youkuaiyun.com/FDk_LCL/article/details/89457611