0. 介绍
在STL六大组件中,用户通常接触不到空间配置器allocator,其总是隐藏在其他组件的背后。但是,从STL实现的角度,第一个需要介绍的就是空间配置器,因为所有存放在容器container中的数据都需要配置空间,从而进行高效的增删改查等操作。
参考链接:
- https://blog.csdn.net/xy913741894/article/details/66974004
- https://blog.csdn.net/passion_wu128/article/details/38966581
1. 空间配置器
一般而言,在C++中进行内存空间分配和释放操作,通常使用如下类似代码:
class Foo {...};
Foo* pf = new Foo; // 配置内存,然后构造对象
delete pf; // 析构对象,然后释放内存
其中new
算式包含两个阶段操作:
- 调用
::operator new
配置内存;
::operator new
分为三种形式:
void* operator new (std::size_t size) throw (std::bad_alloc);
// 分配size内存大小,分配失败则抛出异常void* operator new (std::size_t size, const std::nothrow_t& nothrow_constant) throw();
//分配size内存大小,分配失败则返回NULLvoid* operator new (std::size_t size, void* ptr) throw();
// placement new,本质是对::operator new重载。它不分配内存,调用合适的构造函数在ptr所指的地方构造一个对象,之后返回实参指针ptr。
三种形式使用分别如下:
A* a = new A;
//底层调用第一种A* a = new(std::nothrow) A;
//底层调用第二种new (p)A();
//底层调用第三种 ,在指针p上调用A::A()
构造函数
- 调用
Foo::Foo()
构造对象;
同时new
算式还具有如下的特点:
- 通过
_set_new_handler
设置new
配置内存失败调用的回调函数; new
配置内存失败后,不会像malloc
一样返回NULL,而是抛出异常,程序员判断内存是否配置成功需要使用异常捕获的机制;
try
{
int* p = new int[1000000000000];
}
catch (std::bad_alloc)
{
// 内存配置失败处理
}
- 通过使用nothrow可以设置new配置内存失败后不抛出异常,而是返回NULL;
int* p = new(std::nothrow) int[1000000000000];
if(p == nullptr)
{
// 内存配置失败处理
}
其中delete算式也包含两阶段操作:
- 调用
Foo::~Foo()
将对象析构 - 调用
::operator delete()
释放内存
1.1 SGI标准的空间配置器allocator
SGI中的allocator配置器,只是简单将C++中的::operator new 和::operator delete做了一层封装,效率不佳,因此通常不作为默认空间配置器使用。
#include <new>
#include <cstddef> // for ptrdiff_t, size_t
#include <cstdlib> // for exit()
#include <climits> // for UINT_MAX
#include <iostream> // for cerr
namespace sample
{
template <class T>
inline T* _allocate(ptrdiff_t size, T*)
{
std::set_new_handler(0);
T* tmp = (T*) (::operator new((size_t)(size * sizeof(T))));
if(tmp == 0)
{
std::cerr << "*out of memory" << std::endl;
exit(1);
}
return tmp;
}
template <class T>
inline void _deallocate(T* buffer)
{
::operator delete(buffer);
}
template <class T1, class T2>
inline void _construct(T1* p, const T2& value)
{
new(p) T1(value); // placement new, 调用T1::T1(value)构造函数
}
template <class T>
inline void _destroy(T* ptr)
{
ptr->~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;
pointer allocate(size_type n, const void* hint=0)
{
return _allocate((difference_type)n, (pointer)0);
}
void deallocate(pointer p, size_type n)
{
_deallocate(p);
}
void construct(pointer p, const T& value)
{
_construct(p, value);
}
void destroy(pointer p)
{
_destroy(p);
}
pointer address(reference x)
{
return (pointer)&x;
}
const_pointer const_address(const_reference x)
{
return (const_pointer)&x;
}
size_type max_size() const{
return size_type(UINT_MAX / sizeof(T));
}
};
} // end of namespace sample
int main()
{
const int MAX_NUMS = 10;
sample::allocator<int> alloc;
int* header = alloc.allocate(MAX_NUMS);
for (int i=0; i<MAX_NUMS; ++i)
{
alloc.construct(header+i, i);
}
for(int i=0; i<MAX_NUMS; ++i)
{
std::cout << *(header + i) << "\t";
}
std::cout << std::endl;
alloc.destroy(header);
alloc.deallocate(header, MAX_NUMS);
return 0;
}
1.2 具有次配置力的SGI空间配置器alloc
上述的空间配置器allocator
只是对::operator new
和::operator delete
简单包装,并没有任何效率上的强化。SGI
特殊的空间配置器alloc
,在内存配置、对象构建等方面上做了很多优化,是STL
容器默认的空间配置器。alloc
中将new
和delete
的两阶段行为分离,内存配置操作由alloc::allocate()
负责;内存释放操作由alloc::deallocate()
负责;对象构造操作由::construct()
负责;对象析构操作由::destroy()
负责。
内存空间的配置与释放由stl_alloc.h
文件负责,对象内容的构造与析构由stl_construct.h
文件负责。
对象内容构造与析构
在stl_constuct.h
文件中,construct()
用于对象构造,destroy()
用于对象析构,整体流程如下图:
construct()
接收一个指针p
和一个初值value
,该函数的功能是在指针p
所指的空间上调用对象构造函数,并用value
值初始化,构造对象;destroy()
函数的功能是析构对象,分为两种情况:- 第一种情况接收一个指针,通过直接调用对象的析构函数,析构指针所指向的对象;
- 第二种情况接收
first
和last
两个迭代器,功能是析构[first,last)
范围内的所有对象。对于范围内的每个对象,需要通过value_type()
获取迭代器所指对象的类别,在利用type_traits
判断该类别的析构函数是否trivial
,若是(__true_type)
,则什么都不做结束,若是(__false_type)
,则遍历范围内的每个对象进行析构。
:::warning
trivial
的意思是琐碎的,即不重要的,若经过type_traits
之后返回__true_type
,则表示数据类型不重要,不需要调用析构函数,类似int、double
等基本数据类型,返回__false_type
,则表示数据类型对象必须调用析构函数,析构对象,例如成员变量中有指针之类的,涉及动态内存配置和释放。
:::
空间的配置与释放
空间配置器alloc
使用malloc()
和free()
完成内存的配置和释放,同时,考虑到小型区块所可能造成的内存破碎问题,alloc
有两种形式。
当配置内存块的大小超过128 bytes
时,调用第一级配置器__malloc_alloc_template
,当配置区块小于128 bytes
时,调用第二级配置器__default_alloc_template
。
在SGI
中,第一级或第二级配置器具体使用时都被定义为alloc
,同时,SGI
对alloc
又进行了再次包装,接口为simple_alloc
。
容器、alloc
和simple_alloc
三者之间的关系如下,alloc
可能是第一级或者第二级配置器,作为模板参数传入到容器中,再传入到simple_alloc
中,得到专属空间配置器data_allocator
。
第一级配置器__malloc_alloc_template
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);
// 内存分配失败, 改用oom_malloc()
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);
// 内存分配失败, 改用oom_realloc()
if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
return __result;
}
// 模拟C++的set_new_handler()
static void (* __set_malloc_handler(void (*__f)()))()
{
void (* __old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = __f;
return(__old);
}
};
第一级配置器使用malloc(), free(), realloc()
等C
函数执行实际的内存配置、释放、重配置操作,并实现类似C++ new-handler
的机制。
- 将
alloc
定义为第一级配置器
// inst参数被指定为0
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc;
C++ new handler
机制
new handler
机制是,当内存配置分配无法满足要求时,会调用一个用户执行的函数。也就是,在使用new
配置内存时,一旦::operator new
无法满足配置要求,则在丢出std::bad_alloc
异常之前,会先调用用户指定的处理例程,该例程被称为new-handler
。
/** If you write your own error handler to be called by @c new, it must
* be of this type. */
typedef void (*new_handler)(); // 函数指针
/// Takes a replacement handler as the argument, returns the
/// previous handler.
new_handler set_new_handler(new_handler) throw();
通过调用set_new_handler
即可设置new-handler
, set_new_handler
的形参是一个指向函数的指针,返回值是也是一个指向函数的指针,这个函数是 set_new_handler
被调用前有效的new-handler
。
#include <new> // new
#include <iostream> // cerr
#include <climits> // ULONG_MAX
// function to call if operator new can't allocate enough memory
void outOfMem()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
size_t memory_size = 1000000000000L;
std::cout << "want to allocate " << memory_size * sizeof(int) << " bytes." << std::endl;
try
{
int *pBigDataArray = new int[memory_size];
}
catch(const std::exception& e)
{
std::cerr << e.what() << '\n';
}
}
/* Output:
want to allocate 4000000000000 bytes.
Unable to satisfy request for memory
Aborted
*/
默认情况下第一级配置器的new_handler
为0。
// 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
oom_malloc()
实现
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)(); // 调用new_handler, 企图释放内存
__result = malloc(__n); // 再次尝试配置内存
if (__result) return(__result);
}
}
oom_realloc()
实现
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)();// 调用new_handler, 企图释放内存
__result = realloc(__p, __n);// 再次尝试配置内存
if (__result) return(__result);
}
}
当第一级配置器调用malloc()
和realloc()
不成功后,会调用oom_malloc()
和oom_realloc()
。后两者内部都包含for
循环,不断调用new_hanlder
处理例程,期望在某次调用之后获得足够的内存而圆满完成任务。
但是如果用户没有设置new_handler
处理例程,则会调用__THROW_BAD_ALLOC
丢出bad_alloc
异常信息。
第二级配置器__default_alloc_template
当配置一块内存时,需要额外空间管理内存,如下图所示。为避免太多小额区块分配造成内存的碎片,以及配置时额外空间负担,第二级配置器提供一些额外机制解决该问题。
针对配置内存区块小于128 bytes情况,第二级配置器采用内存池(memory pool)方式管理。具体做法如下:
- 配置一大块内存(8的倍数),并使用自由链表free-list对其进行维护;
- 当相同大小的内存需求时,直接从free-list中拨出,对于任何小额区块的内存需求量都会上调到8的倍数;
- 如果客端释放小额区块,配置器会将其回收到free-list中;
- 维护了16个free-lists,分别管理8、16、… 128bytes的小额区块。
free-list节点结构如下:
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
};
部分实现代码
enum {__ALIGN = 8}; // 小额区块的上调边界
enum {__MAX_BYTES = 128}; // 小额区块的上限
enum {__NFREELISTS = __MAX_BYTES / __ALIGN}; // free-lists的个数
template <bool threads, int inst>
class __default_alloc_template {
private:
// ROUND_UP将小额区块上到到8的倍数
static size_t ROUND_UP(size_t bytes)
{
return (((bytes) + __ALIGN -1) & ~(__ALIGN - 1));
}
union obj{ // free-list节点
union obj* free_list_link;
char client_data[1];
};
// 16个free-list
static obj* volatile free_list[__NFREELISTS];
// 根据分别区块的大小, 决定使用第n号free-list, n从0开始
static size_t FREELIST_INDEX(size_t bytes){
return (((bytes) + __ALIGN-1) / __ALIGN -1);
}
// 返回一个大小为n的对象,并可能将其他分配大小为n的区块加入到free-list中
static void* refill(size_t n);
// 配置一大块空间,空间大小为size * nobjs
static char* chunk_alloc(size_t size, int& nobjs);
// chunk allocation state
static char* start_free; // memory pool起始位置
static char* end_free; // memory pool结束位置
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);
};
// static data member的初始化
template <bool threads, int inst>
char* __default_alloc_template<threads, inst>::start_free = 0;
template <bool threads, int inst>
char* __default_alloc_template<threads, inst>::end_free = 0;
template <bool threads, int inst>
size_t __default_alloc_template<threads, inst>::heap_size = 0;
template <bool threads, int inst>
__default_alloc_template<threads, inst>::obj* volatile
__default_alloc_template<threads, inst>::free_list[__NFREELISTS] =
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
空间配置函数allocate
如下图所示,allocate
函数首先会判断区块的大小,大于128 bytes
则会转而调用第一级配置器,小于128 bytes
就检查对应的free list
。如果 free list
之内有可用的区块,就直接拿来用,如果没有可用区块,就调用refill
准备为free list
重新填充空间。
从free-list
中直接调出内存区块的操作步骤如下:
- 根据配置的内存区块大小,找到合适的
free-list
节点; - 将节点的指向赋值给临时的
result
; - 将节点指向位置调整到
result
指向的下一个节点;
空间释放函数deallocate
deallocate
首先根据区块大小,决定是否调用第一个配置器,如果区块小于128 bytes
就找到对应的free-list
,将其回收。
区块回收纳入free-list
的操作如下图所示:
重新填充free-list函数refill
当free-list
中没有可用区块时,就会调用refill()
,重新配置一大片内存,并将其连接到free-list
上。默认情况下,分配20个新区块、如果内存池空间不足,则区块个数可能小于20。
重新填充free-list的过程如下:
内存池(memory pool)
在refill()
中,通过chunk_alloc()
从内存池中取空间给free-list
使用,这一部分比较细节性的讨论了各种情况,在这里不做介绍。
2. 内存基本处理工具
STL中定义了五个全局函数,作用与未初始化空间上,功能分别如下:
construct()
用于对象构造;destroy()
用于对象析构;uninitialized_copy():
对应copy()
函数;uninitialized_fill():
对应fill()
函数;uninitialized_fill_n():
对应fill_n()
函数;
后面3个全局函数实际定义于stl_uninitialzed
文件。
uninitialized_fill_n()
该函数接收三个参数:
- 迭代器
first
指向欲初始化空间的起始处; n
表示欲初始化空间的大小;x
表示初值;
函数运行逻辑如下图所示:
- 利用
type_traits
萃取迭代器first
的value type
; - 判断该型别是否为
POD
。POD(Plain Old Data)
指的是标量型别或者C struct
型别。POD
型别其ctor/dtor/copy/assignment
函数是不重要的,可以直接初始化,而对于non-POD
则需要调用范围内每个对象的构造函数进行初始化。
uninitialized_fill()
函数的实现过程与之类似,这里不做分析了。
uninitialized_copy()
该函数接收三个参数:
- 迭代器
first
指向输入端的起始位置; - 迭代器
last
指向输入端的结束位置; - 迭代器
result
指向输出端(欲初始化空间)的起始处
函数逻辑与之前的uninitialized_fill_n()
类似,先萃取迭代器的value type
,判断是否为POD
类别,分别调用不同的函数完成区间范围内对象的拷贝。
针对char*
和wchar_t*
两种型别,涉及了特化版本,使用memmove
来执行拷贝行为。
3. 总结
本文介绍了SGI STL中实现的空间配置器,包含标准的allocator
和特殊的alloc
,重点对空间配置器alloc
进行了分析,分为对象构造、析构和空间配置、释放两大部分。在空间配置与释放章节,分别对第一级和第二级配置器的原理进行了细致分析,了解了各种情况下空间是如何配置和释放。最后,介绍了3个用于对象初始化的全局函数。