2.STL源码解析-空间配置器alloc
空间配置器就是给容器分配空间的。像我们平时使用new和delete动态分配释放对象内存一样。空间配置器也封装了这些功能。但是STL的空间配置器不仅仅只简单调用分配空间,它在一些地方都做了优化来提升性能。
构造和析构
我们在调用new动态分配对象内存时,通常会先调用malloc分配空间然后调用默认构造函数。通过delete来释放内存时,会先调用默认析构函数来析构对象,然后调用free来释放空间。在new/delete中把这两步封装在了一起。
而在STL中空间分配则是将这两阶段操作区分开来。内存配置和释放操作由成员函数alloc:allocate()和alloc:deallocate()负责。对象的构造和析构由::construct()和::destory()负责。
这样做有什么好处?
有种情况就是默认的构造函数和析构函数其实并没有做业务处理,在STL中这类构造函数叫trivial constructor((直译:平凡的构造函数))和trivial deconstructor(平凡的析构函数),STL是否可以选择对于这类析构函数不做调用,来减少性能开销。特别是一次性构造或析构一段范围内的对象时,这里的开销节省还是可以的。STL的确这么做了。
在使用 allocator
分配内存时,可以上一节提到的traits技法来实现trivial constructor的判断,以此来避免额外的构造函数和析构函数调用的开销。
看看构造函数的源码:
// FUNCTION TEMPLATE uninitialized_default_construct
template<class _FwdIt> inline
void _Uninitialized_default_construct_unchecked(const _FwdIt _First, const _FwdIt _Last, false_type)
{ // default-initialize all elements in [_First, _Last), no special optimization
_Uninitialized_backout<_FwdIt> _Backout{_First};
for (; _Backout._Last != _Last; ++_Backout._Last)
{
::new (static_cast<void *>(_Unfancy(_Backout._Last))) _Iter_value_t<_FwdIt>;
}
_Backout._Release();
}
template<class _FwdIt> inline
void _Uninitialized_default_construct_unchecked(_FwdIt, _FwdIt, true_type)
{ // default-initialize all elements in [_First, _Last), trivially default constructible types
// nothing to do
}
这两个函数的作用是在指定范围内(由 _First
和 _Last
定义)执行构造操作,即调用元素类型的构造函数。
- 上述两个模板函数分为两个构造版本,根据类型的是否是trivial constructor 进行选择使用。函数的目标是在指定范围内执行默认构造操作。
- 对于
false_type
版本,即非trivial constructor 类型的版本,它使用_Uninitialized_backout
类来执行默认构造。在循环中,对每个元素调用::new
运算符,使用_Unfancy
将指针_Backout._Last
转换为未修饰的指针,然后通过_Iter_value_t<_FwdIt>
获得迭代器指向的元素类型,以调用相应类型的默认构造函数。 - 对于
true_type
版本,即trivial constructor 类型的版本,因为这些类型的默认构造函数不执行任何实际操作,所以函数中没有特殊的优化,即“nothing to do”。
析构函数源码:
// FUNCTION TEMPLATE destroy_n
template<class _FwdIt,
class _Diff> inline
_FwdIt _Destroy_n1(_FwdIt _First, _Diff _Count, false_type)
{ // destroy [_First, _First + _Count), no special optimization
for (; 0 < _Count; ++_First, (void)--_Count)
{
_Destroy_in_place(*_First);
}
return (_First);
}
template<class _FwdIt,
class _Diff> inline
_FwdIt _Destroy_n1(const _FwdIt _First, const _Diff _Count, true_type)
{ // destroy [_First, _First + _Count), trivially destructible
return (_STD next(_First, _Count)); // nothing to do
}
这两个函数的作用是在指定范围内执行元素的销毁操作,即调用元素的析构函数。
…………性能优化点1
- 上述两个模板函数分为两个析构版本,根据类型的是否是trivial deconstructor 进行选择使用。函数的目标是在指定范围内执行析构操作。
- 对于
false_type
版本,即非trivial deconstructor 类型的版本,它使用循环对每个元素调用_Destroy_in_place
函数,该函数执行析构操作。在循环中,对每个元素调用_Destroy_in_place
函数,递减_Count
,直至_Count
变为零。 - 对于
true_type
版本,即trivial deconstructor 类型的版本,因为这些类型的析构函数不执行任何实际操作,所以函数中没有特殊的优化,即“nothing to do”。
空间配置与释放:alloc
STL的空间配置才是真正的核心点。为了避免过多的小型内存区块造成的内存碎片问题,STL设计了双层配置器来处理不同空间大小的内存申请场景。
- 一级空间配置器:分配超过128bytes大小的内存使用。直接使用malloc和free来配置管理空间。
- 二级空间配置器:分配小于128bytes大小的内使用。采用复杂的memory pool来管理空间。维护16个自由链表,负责16种小区块内存的配置能力。
是否只开放一级配置器还是同时开放一级和二级配置器是由__USE_MALLOC定义决定的。
一二级空间配置器都进行了标准的封装。通过allocate分配空间,通过deallcate释放空间。
一级配置器
简单实现一个一级空间配置器
#include <cstdlib>
#include <new> // 为了使用 std::bad_alloc
class __malloc_alloc_template{
public:
// 分配内存
static void* allocate(size_t size) {
void* result = std::malloc(size);
if(result == 0) result = oom_malloc(size);
return result;
}
// 释放内存
static void deallocate(void* ptr) {
std::free(ptr);
}
// 重新分配内存
static void* reallocate(void* ptr, size_t old_size, size_t new_size) {
void* result = std::realloc(p, size);
if(result == 0) result = oom_realloc(p, size);
return result;
}
// 处理内存耗尽情况
static void* oom_malloc(size_t size) {
while (true) {
// 不断尝试分配内存
void* ptr = allocate(size);
if (ptr != nullptr) {
return ptr; // 分配成功,返回指针
}
// 内存不足,尝试释放一些内存
std::new_handler globalHandler = std::get_new_handler();
if (globalHandler == nullptr) {
throw std::bad_alloc(); // 如果没有新的处理器,抛出 bad_alloc 异常
}
globalHandler(); // 调用新的处理器尝试释放内存
}
}
// 其他可能的成员函数,例如构造函数、析构函数等
};
一级空间配置器的allocate和realloc都是在调用malloc和realloc分配内存,当分配不成功时,改调用oom_malloc或者oom_realloc。后面两个函数会不断调用内存不足处理例程,去尝试释放多余的内存,再继续分配。如果globalHandler 未被客户端设置,则抛出异常。
二级空间配置器(内存池)
二级配置器多了一些机制,专门针对内存碎片。内存碎片化带来的不仅仅是回收时的困难,配置也是一个负担,额外负担永远无法避免,毕竟系统要划出这么多的资源来管理另外的资源,因此区块越小越多,额外负担率就越高。
第一级配置器是直接使用 malloc(), free(), realloc() 并配合类似 C++ get_new_handler 机制实现的。第二级配置器的工作机制要根据区块的大小是否大于 128bytes 来采取不同的策略。如果需要配置区块大于128bytes,就交给一级配置器。当小于128bytes,就交给内存池分配。
二级空间配置器需要维护16个free list,各自管理8,16,24,32,40,48,56,64,72,80,88,90,104,112,120,128bytes的小额区块。如果有小额区块申请,则从对应大小的free list中取出一块来使用。如下所示:
…………性能优化点2
这里可以了解一下free-list的节点结构:
union obj
{
union obj * free_list_link;
char client_data[1];
};
STL这里也做了性能优化。可以看到这里用到了union联合体,因为每个空闲链表区块的节点在空闲时需要作为链表连接下一个节点。在分配空间时又需要作为指针指向分配的空间。所以这里用到了联合体,一个obj节点在不同的时机代表两种不同的意义,但只用到了一个变量的空间。
简单实现一个二级空间配置器:核心的三个函数allocate、deallocate、refill、chunk_alloc
#include <cstdlib> // 为了使用 malloc 和 free
#include <cstddef> // 为了使用 size_t
// 定义二级空间配置器
template <bool threads, size_t inst>
class __default_alloc_template {
private:
// free-list 节点的结构
union obj {
union obj* free_list_link;
char client_data[1];
};
// 自由链表数组
static obj* free_list[NFREELISTS];
// 为了对齐,将 nbytes 上调至 8 的倍数
static size_t ROUND_UP(size_t bytes) {
return (bytes + ALIGN - 1) & ~(ALIGN - 1);
}
// 根据大小计算索引
static size_t FREELIST_INDEX(size_t bytes) {
return (bytes + ALIGN - 1) / ALIGN - 1;
}
static void* refill(size_t n);
// 分配一大块内存
static char* chunk_alloc(size_t size, int& nobjs);
public:
// 分配内存
static void* allocate(size_t n) {
if (n > MAX_BYTES) {
// 对于大于 MAX_BYTES 的内存块,使用第一级空间配置器分配
// 这里简化为直接使用 malloc
return std::malloc(n);
}
obj* result;
obj* my_free_list = free_list + FREELIST_INDEX(n);
result = *my_free_list;
if (my_free_list == nullptr) {
// 自由链表为空,重新填充
return refill(ROUND_UP(n));
}
//调整free_list
my_free_list = my_free_list->free_list_link;
return result;
}
// 释放内存
static void deallocate(void* p, size_t n){
if (n > MAX_BYTES) {
// 对于大于 MAX_BYTES 的内存块,使用第一级空间配置器释放
// 这里简化为直接使用 free
std::free(p);
return;
}
// 将释放的内存块添加到自由链表中
size_t index = FREELIST_INDEX(n);
obj* my_free_list = static_cast<obj*>(p);
my_free_list->free_list_link = free_list[index];
free_list[index] = my_free_list;
}
};
当free_list上没有可用的区块了,就需要用到refill函数为free_list重新填充空间。
// 重新填充自由链表
static void* refill(size_t n) {
int nobjs = 20; // 一次分配 20 个节点
char* chunk = chunk_alloc(n, nobjs);
if (nobjs == 1) {
return chunk; // 如果只分配了一个节点,直接返回
}
// 将多余的节点加入到自由链表中
size_t index = FREELIST_INDEX(n);
obj* my_free_list = reinterpret_cast<obj*>(chunk + n);
free_list[index] = my_free_list;
for (int i = 2; i <= nobjs; ++i) {//从2开始,因为第一块区域需要返回去使用
my_free_list->free_list_link = reinterpret_cast<obj*>(chunk + i * n);
my_free_list = my_free_list->free_list_link;
}
my_free_list->free_list_link = nullptr;
return chunk;
}
从内存池中取空间出来,就需要用到chunk_alloc函数。
如下,书中提了一个示例: