参考资料:
STL六大组件
-
设计配置器(包括内存池,构造 析构函数等)
包括 allocator 和 constructor ,分别定义在 allocator.h 和 construct.h 中。
allocator 负责空间的配置与回收,定义了一个类mystl::alloc 用于管理内存,定义在 alloc.h 。
constructor 负责对象的构造与析构,对应两个全局函数: construct 和destroy 。
-
设计迭代器的五种类型
iterator ,连接着容器与算法,是一种泛型指针 ,定义在 iterator.h 中。每个容器都附带专属的迭代器,是一种重载了 operator* ,operator-> ,operator++ ,operator-- 等操作的模板类。
-
设计容器
如 vector ,list ,map
-
设计常用算法
包括 基本算法 ,数值算法 ,set算法 ,heap算法 ,其他算法
-
设计仿函数
即函数对象 ,定义在functional.h 中。
另外还有hash_functional , mystl::hash 函数对象用于计算元素的哈希值(在哈希表中的位置),对一些内置型别和容器做了特化处理。
-
设计配接器(适配器)
container adapters :如stack , queue , priority_queue
iterator adapters :如reverse_iterator ,是一种反向迭代器,重载了operator* ,operator-> ,operator++ ,operator-- ,operator+ ,operator- ,operator+= ,operatpr-= ,operator[] 等操作,变前进为后退,后退为前进。
六大组件介绍:https://blog.csdn.net/qq_45893475/article/details/120749735
上面图片地址:https://github.com/Alinshans/MyTinySTL/wiki
一、空间配置器
功能分析:C/C++需要手动的管理内存,而STL首先需要实现一个能自己申请空间并且释放空间不再需要我们管理的功能,即 空间配置器
解释:空间配置器一般隐藏在各个板块的组件中,实际操作中我们根本注意不到他的存在,但它又是实现STL非常重要的一部分。就像在 类 中我们实现的构造、析构函数,定义对象的时候会自动调用。
1、SGI STL
SGI的空间配置器与众不同,且与STL标准规范不同。名为alloc ,而非allocator
虽然SGI也配置了allocator ,但是不建议使用(效率~只在基层进行配置/释放空间,且不接受任何参数)
1.1 new 和 delete
在调用new 和 delete 进行对象的创建和销毁的同时,也会有内存配置操作和释放操作:
- new:编译器先调用
::operator new 分配内存,然后调用 Obj::Obj() 构造对象内容 - delete:编译器先调用
Obj::obj() 析构对象,然后调用::operator delete 释放内存
为了精密分工,STL allocator将这两个阶段操作区分开来:
- 对象构造由
::construct() 负责,对象释放由::destroy() 负责 - 内存配置由
alloc::allocate() 负责,内存释放由alloc::deallocate() 负责
看一幅图:
下面分别来看:
1.2 构造 和 析构
在<stl_construct.h> 中
construct() 函数:
- 接受一个指针P 和一个初始值 value,该函数的用途就是将初值设定到指针所指的空间。
destroy() 函数有两个版本:
- 第一个版本接受一个指针,准备将该指针所指之物 析构掉。
- 第二个版本接受 first 和 last 两个迭代器,将 [first, last) 范围内的所有对象 析构掉。
#include <new.h>
template <class T1, class T2>
inline void construct(T1* p, const T2& value) {
new(p) T1(value);
}
template <class T>
inline void destroy(T* pointer) {
pointer->~T();
}
template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last) {
__destroy(first, last, value_type(first));
}
后面还有 判断元素的数值类型(value type)是否有 trivial destructor等…见P51
1.3 内存的配置与释放
在<stl_alloc.h> 中,SGI对此设计原理:
- 向 system heap要求空间
- 考虑多线程(multi-threads)状态
- 考虑内存不足时的应变措施
- 考虑过多“小型区块” 可能造成的内存碎片(fragment) 问题
考虑到 “小型区块” 可能造成的内存碎片问题,SGI设计了双层级配置器:
- 当配置区块超过 128字节 时,视为足够大,使用第一级配置器——直接使用 malloc() 和 free()
- 当配置区块小于 128bytes 时,为了降低额外负担,直接使用第二级配置器——采用复杂的 memory pool 处理方式
无论第一级还是第二级配置器, alloc 都为其包装了接口,使其能够符合 STL标准。
#ifdef _USE_MALLOC
...
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc;
#else
...
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;
#endif
template <class T, class Alloc>
class simple_alloc {
public:
static T* allocate(size_t n) {
return 0 == n ? 0 : (T*)Alloc::allocate(n * sizeof(T));
}
static T* allocate(void) {
return (T*)Alloc::allocate(sizeof(T));
}
static void deallocate(T* p, size_t n) {
if (0 != n) Alloc::deallocate(p, n * sizeof(T));
}
static void deallocate(T* p) {
Alloc::deallocate(p, sizeof(T));
}
};
一、二级配置器的关系:
一、二级配置器的包装接口和运用方式(SGI STL讲配置器多了一层包装使得Alloc具备标准接口):
1.4 alloc 一级配置器源码阅读
注意:
- 第一级配置器以 malloc(), free(), realloc() 等C函数执行实际的内存配置、释放和重配置操作,并实现类似的 C++ new-handler 的机制
- SGI 第一级配置器的 allocate() 和 reallocate() 都是在调用 malloc() 和 realloc() 不成功后,该调用 oom_malloc() 和 oom_realloc()
- oom_malloc() 和 oom_realloc() 都有内循环,不断调用 “内存不足处理例程”,期望某次调用后,获得足够的内存而圆满完成,哪怕有一丝希望也要全力以赴
如果用户没有指定 “内存不足处理例程”,这时便无力回天,STL会抛出异常,或调用exit(1) 终止程序
template <int inst>
class __malloc_alloc_template {
private:
static void* oom_malloc(size_t);
static void* oom_realloc(void*, size_t);
static void (*__malloc_alloc_oom_handler)();
public:
static void* allocate(size_t n) {
void* result = malloc(n);
if (0 == result) {
result = oom_malloc(n);
}
return result;
}
static void* reallocate(void* p, size_t , size_t new_sz) {
void* result = realloc(p, new_sz);
if (0 == result) {
result = oom_realloc(p, new_sz);
}
return result;
}
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>::__malloc_alloc_oom_handler)() = 0;
template <int inst>
void* __malloc_alloc_template<inst>::oom_malloc(size_t n) {
void (*my_malloc_handler)();
void* result;
for (;;) {
my_alloc_handler = __malloc_alloc_oom_handler;
if (0 == my_alloc_handler) {
__THROW_BAD_ALLOC;
}
(*my_alloc_handler)();
result = malloc(n);
if (result) {
return (result);
}
}
}
template <int inst>
void* __malloc_alloc_template<inst>::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_hanler)();
result = realloc(p, n);
if (result) {
return (result);
}
}
}
type __malloc_alloc_template<0> malloc_alloc;
1.5 alloc二级配置器源码阅读
第二级配置器多了一些机制,专门针对内存碎片。内存碎片化带来的不仅仅时回收时的困难,配置也是一个负担。
毕竟系统要划出这么多资源来管理另外的资源,额外负担永远都是无法避免的,但是区块越小,额外负担率就越高。
前面也说了,当区块大小小于 128bytes 时采用 内存池memory pool的策略:
上面提到了 自由链表,自由链表自由在哪?又如何维护?
一方面,自由链表中有些区块已经分配给了客端使用,所以 free-list 不需要再指向它们;另一方面,为了维护free-list,每个结点还需要额外的指针指向下一个结点。
现在考虑一下,如果每个结点都有两个指针,这样不就造成了额外负担吗?
解决办法,使用 union(联合体/共同体) ,几个变量共用一个内存位置,在不同的时间保存不同的数据类型和不同长度的变量。(一个union 只配置一个足够大的空间以容纳最大长度的数据成员)
因此,借助 union 的帮助,实现 free-list 节点的结构
union obj {
union obj* free_list_link;
char client_data[1];
};
在 union obj 中,定义了两个字段:
- 第一个字段,obj可以看做一个指针,指向链表中的下一个节点
- 第二个字段,obj也可以看做一个指针,不过此时指向实际的内存区
一物二用的好处就是不会为了维护链表所必须的指针而造成内存的另一种浪费。
第二级配置器部分实现
enum {__ALIGN = 8};
enum {__MAX_BYTES = 128};
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};
template <bool threads, int inst>
class __default_alloc_template {
private:
static size_t ROUND_UP(size_t bytes) {
return (((bytes) + __ALIGN - 1) & ~(__ALIGN - 1));
}
private:
union obj {
union obj* free_list_link;
char client_data[1];
};
private:
static obj* volatile free_list[__NFREELISTS];
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);
static char* start_free;
static char* end_free;
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);
};
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, }
1.6 空间配置器函数 allocate源码阅读
第二级配置器拥有配置器的标准接口函数 allocate()。此函数首先判断区块的大小,如果大于128bytes ——>调用第一级配置器;小于128bytes——>检查对应的 free-list(如果没有可用区块,就将区块上调至8倍数的边界,然后调用refill(),为free-list重新填充空间)。
空间申请
调用标准接口函数 allocate() :
static void* allocate(size_t n) {
obj* volatile* my_free_list;
obj* result;
if (n > (size_t)__MAX_BYTES) {
return (malloc_alloc::allocate(n));
}
my_free_list = free_list + FREELIST_INDEX(n);
result = *my_free_list;
if (result == 0 ) {
void* r = refill(ROUND_UP(n));
return r;
}
*my_free_list = result->free_list_link;
return (result);
};
NOTE:每次都是从对应的 free-list 的头部取出可用的内存块。然后对 free_list 进行调整,使上一步调出的内存的下一节点变成头结点。
空间释放
NOTE:通过调整 free-list 链表将区块放入free_list 的头部
重新填充 free_lists
- 当发现 free_list 中没有可用区块时,就会调用 refill() 为 free_list 重新填充空间
- 新的空间将取自内存池(经由 chunk_alloc()完成)
- 缺省取得20个新节点(区块),但万一内存池空间不足,获得的节点数可能小于20
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n) {
int nobjs = 20;
char* chunk = chunk_alloc(n, nobjs);
obj* volatile *my_free_list;
obj* result;
obj* current_obj, *next_obj;
int i;
if (1 == nobjs) return (chunk);
my_free_list = free_list + FREELIST_INDEX(n);
result = (obj*)chunk;
for (i = 1; ; i++) {
current_obj = next_obj;
next_obj = (obj*)((char*)next_obj + n);
if (nobjs - 1 == i) {
current_obj->free_list_link = 0;
break;
}
else {
current_obj->free_list_link = next_obj;
}
}
return (result);
}
内存池 memory pool
我们知道从内存池中取空间给 free-list使用 是 chunk_alloc() 在工作,如何工作?
chunk_alloc() 函数以 end_free-start_free 来判断内存池的 “水量”。
如果第一级配置器的 malloc() 也失败了,就发出 bad_alloc 异常。
来看源码:
template <bool threads, int inst>
char* __default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs) {
char* result;
size_t total_bytes = size * nobjs;
size_t bytes_left = end_free - start_free;
if (bytes_left >= total_bytes) {
result = start_free;
start_free += total_bytes;
return (result);
}
else if (bytes_left >= size) {
nobjs = bytes_left / size;
total_bytes = size * nobjs;
result = start_free;
start_free += total_bytes;
return (result);
}
else {
size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
if (bytes_left > 0) {
obj* volatile *my_free_list = free_list + FREELIST_INDEX(bytes_left);
((obj*)start_free)->free_list_link = *my_free_list;
*my_free_list = (obj*)start_free;
}
start_free = (char*)malloc(bytes_to_get);
if (0 == start_free) {
int i;
obj* volatile *my_free_list, *p;
for (i = size; i <= __MAX_BYTES; i += __ALIGN) {
my_free_list = free_list + FREELIST_INDEX(i);
p = *my_free_list;
if (0 != p) {
*my_free_list= p->free_list_link;
start_free = (char*)p;
end_free = start_free + i;
return (chunk_alloc(size, nobjs));
}
}
end_free = 0;
start_free = (char*)malloc_alloc::allocate(bytes_to_get);
}
heap_size += butes_to_get;
end_free = start_free + bytes_to_get;
return (chunk_alloc(size, nobjs));
}
}
|