分配器allocator的实现
1. 在vc98中allocaters的实现
1.new调用malloc函数
2.allocate调用new函数
2.在BC5中allocator的使用
实现原理
3.G++2.9stl
1. 对allocator的使用(使用alloc)
注意 他有allocator但没有放入标准库里,而是使用了alloc
2.alloc的实现原理
设计了十六条链表,每一条链表负责某一个固定大小的区块,
为什么不建议使用分配器
分配器的申请的释放底层实现实现就是malloc和free
在释放时使用deallocate还需要制定当初申请时的空间大小,对于程序员是极不友好的。
4.G++4.9版本
1.使用了新定义的alloctor(放弃了设计较好的alloc)
2.新扩展的pool_allocator就是原来的alloc
空间配置器
从stl运用的角度来看,空间配置器是最不需要介绍的东西,他总是隐藏在一切组件(容器)的背后
从stl实现的角度来看,第一个就需要了解空间配置器,整个stl的操作对象都存放在容器之内。
为什么说是空间配置器而不说成内存配置器?
因为空间不一定是内存,空间可以是磁盘也可以是其他辅助存储介质,空间包含的范围大。
配置器的接口
allocator: :value_type
allocator: :pointer
allocator::const_point er
allocator::reference
allocator::const_reference
allocator::size_type
allocator::difference_type
allocator: :rebind
allocator函数
拥有次层配置的SGI空间配置器(重点)
在个版本中SGI STL逸脱了STL标准,使用一个专属的,有次层配置的特殊配置器,它提供了一个标准的配置器接口(simple_alloc),对它进行了一层隐藏。
与标准配置器不同
SGI STL的配詈器与众不同, 也与标准规范不同, 其名称是alloc而非allocator,而且不接受任何参数。换句话说,如果你要在程序中明白采用SGI配置器, 则不能采用
标准写法:
vector<int, std::allocator > iv; // in VC or CB
SGI写法:
vector<in七, std: :alloc> iv; // in GCC
SGI并不会带来麻烦,我们通常使用缺省的很少需要自行指定配置器名称,而SGISTL的 每一个容器都已经指定其缺省的空间配置器为alloc。
template <class T, class Alloc = alloc> //缺省使用alloc为配置器
class vector {… };
SGI配置器 std::alloc
一般而言, 我们所习惯的C++内存配置操作和释放操作是这样的:
class Foo {… } ;
Foo* pf= new Foo; //配置内存,然后构造对象
delete pf; //将对象析构,然后释放内存
包含
1.配置内存(分配器内存)/ 释放内存
2.构造对象内容 / 对象析构
STLallocator决定将这两阶段操作区分开来。
内存配置操作由alloc:allocate() 负责,内存释放操作由alloc: :deallocate() 负责;
对象构造操作由: : construct () 负责,对象析构操作由: : destroy () 负责。
#include <stl_alloc.h> 负责内存空间的配置与释放
#include <stl_construct.h> 负责对象内容的构造与析构
construct和destroy函数(对象构造和析构)
定义
解释
construct()接受一个指针p和一个初值value, 该函数的用途就是将初值设定到指针所指的空间上。
destroy()有两个版本, 第一版本接受一个指针,准备将该指针所指之物析构掉。第二版本接受first和last 两个迭代器准备将[first, last )范围内的所有对象析构掉。(如果对象很多,将会花费大量的时间,)
对destroy进行优化(判断value_type)
因此, 这里首先利用value_type()获得迭代器所指对象的 型别, 再利用—type_traits 判断该型别的析构函数是否无关痛痒。 若是
(_true_type), 则什么也不做就结束;若否(—false_type), 这才以循环方式巡访整个范围, 并在循环中每经历一个对象就调用第一个版本的des七roy()。
空间的分配和释放(由<stl_alloc.h>负责)
SGI对此的设计哲学:
• 向systemheap要求空间。
• 考虑多线程(multi-threads)状态。
• 考虑内存不足时的应变措施。
C++的内存配置基本操作是 ::operator new(),内存释放基本操作是: :operator delete()。
这两个全局函数相当于C的malloc()和free()函数,SGI正是以malloc()和free()完成内存的配置与释放。
解决内存破碎问题
考虑到小型区块所可能造成的内存破碎问题,SGI设计了双层级配置器,第 级配置器直接使用malloc()和free(),第二级配置器则视情况采用不同的策略:
- 当配置区块超过 128 bytes时, 视之为 "足够大” ,便涸用第一级配置器;
- 当配 置区块小于 128 bytes时, 视之为 “过小” ,为了降低额外负担 便采用复杂的内存池整理方式。
_malloc_alloc_template就是第 一 级配置器
_default_alloc_ template就是第二级配置器。
注意:alloc并不接受任何template型别参数。
为alloc包装接口simple_alloc
SGI还为它再包装一个接口如下, 使配置器的接口能够符合STL规格:
一二级配置的关系
第一级配置器解析
#if 0
# include <new>
# define __THROW_BAD_ALLOC throw bad_alloc
#elif !defined(__THROW_BAD_ALLOC)
# include <iostream.h>
# define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1)
#endif
// malloc-based allocator. 通常比稍后介绍的 default alloc 速度慢,
//一般而言是 thread-safe,并且对于空间的运用比较高效(efficient)。
//以下是第一级配置器。
//注意,无「template 型别参数」。至于「非型别参数」inst,完全没派上用场。
template <int inst>
class __malloc_alloc_template {
private:
//以下都是函式指标,所代表的函式将用来处理内存不足的情况。
// oom : out of memory.
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);//第一级配置器直接使用 malloc()
// 以下,无法满足需求时,改用 oom_malloc()
if (0 == result) result = oom_malloc(n);
return result;
}
static void deallocate(void *p, size_t /* n */)
{
free(p); //第一级配置器直接使用 free()
}
static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)
{
void * result =realloc(p, new_sz);//第一级配置器直接使用 rea
// 以下,无法满足需求时,改用 oom_realloc()
if (0 == result) result = oom_realloc(p, new_sz);
return result;
}
//以下模拟 C++的 set_new_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);
}
};
// malloc_alloc out-of-memory handling
//初值为 0。有待客端设定。
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_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>::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);
}
}
//注意,以下直接将参数 inst指定为 0。
typedef __malloc_alloc_template<0> malloc_alloc;
SGI第一级配置器的allocate()和realloc()都是在调用malloc()和realloc()不成功后,改调用oom_malloc()和oom_realloc()。后两者都有内循环,不断调用“内存不足处理例程”,期望在某次调用之后,获得足够的内存而圆满完成任务。但如果“内存不足处理例程”并未被客端设定,调用THROW_BADALLOC,丢出bad_alloc异常信息,或利用exit(1)硬生生中止程序。
用于采用的是malloc free realloc函数,执行实际的内存配置,释放,重配等操作,实现类似于C++new-handler7的机制。
他不能直接运用C++new-handler7的机制,因为他并非使用::operate:new来配置内存。
C++new-handler7的机制是什么
你可以要求系统在内存配置需求无法被满足时,调用一个你所指定的函数。换句话说,一旦::operate new无法完成任务, 在丢出std::bad_alloc异常状态之前,会先调用由客端指定的处理例程。这个例程就是new-handler
第二级配置器解析(次层配置)
如果区块超过128k,移交第一级,否则就以内存池管理
每次配置一大块内存,维护其自由链表,下次若再有相同大小的内存需求,就直接从free-lists中拨出。如果客端释还小额区块,就由配置器回收到free-lists中(配置器既负责创建又负责回收)
为了方便管理,将内存需求设置为8的倍数一共有16个自由链表,从8-128.
用户申请空间的工作流程
首先将这个空间大小上调到8的倍数,找到对应的free-list
如果链表有空闲节点,直接取下一个节点分配给用户。
对于你那个链表为空,重新申请链表的节点(默认为20个此大小的节点)
不足20个,但是可以支付一个或者更多该节点的大小,返回可完成的节点个数。(能给多少给多少)
没有办法满足,重新申请内存池,
所申请的内存池大小为2*total_bytes+ROUND_UP(heap_size>>4),total_bytes是所申请的内存大小,SGI将申请2倍还要多的内存。为了避免内存碎片问题,需要将原来内存池中剩余的内存分配给free_list链表。
- 如果内存池申请失败,也就是heap_size(堆空间)不足以支付要求时,SGI的次级配置器将使用最后的绝招查看free_list数组,查看是否有足够大的没有被使用的内存
- 如果这些办法都没有办法满足要求时,只能调用第一级配置器了,我们需要注意,第一级配置器虽然是用malloc来分配内存,但是有new-handler机制(out-of-memory),如果无法成功,只能抛出bad_alloc异常而结束分配。
free-lists节点定义如下:
union obj
{
union obj* free_list_link;
char client_data[1];
};
enum {__ALIGN = 8}; //小型区块的上调边界
enum {__MAX_BYTES = 128}; //小型区块的上界
enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; //free-list个数
// 无template型参数,且第二个参数没有用上
// 第一个用于多线程,暂不讨论
template <bool threads, int inst>
class __default_alloc_template {
private:
/*将bytes上调至8的倍数
用二进制理解,byte整除align时尾部为0,结果仍为byte;否则尾部肯定有1存在,加上
align - 1之后定会导致第i位(2^i = align)的进位,再进行&操作即可得到8的倍数
*/
static size_t ROUND_UP(size_t bytes) {
return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
}
private:
union obj { //free-list的节点
union obj * free_list_link;
char client_data[1]; /* The client sees this. */
};
private:
//16个free-lists
static obj * __VOLATILE free_list[__NFREELISTS];
//根据区块大小,找到合适的free-list,返回其下标(从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);
//配置一大块空间,可容纳nobjs个大小为“size”的区块
//如果配置nobjs个区块有所不便,nobjs可能会降低
static char *chunk_alloc(size_t size, int &nobjs);
//Chunk allocation state
static char *start_free;
static char *end_free;
static size_t heap_size;
public:
// 下面会介绍
static void * allocate(size_t n);
static void * deallocatr(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函数
static void * allocate(size_t n)
{
obj * __VOLATILE * my_free_list;
obj * __RESTRICT result;
if (n > (size_t) __MAX_BYTES) {
return(malloc_alloc::allocate(n));
}
//如果所开辟的区块的大小小于128bytes
my_free_list = free_list + FREELIST_INDEX(n);
result = *my_free_list;
if (result == 0) {
// 没有找到可用的free list,重新配置
void *r = refill(ROUND_UP(n)); // refill会在后续介绍
return r;
}
//如果此时free_list上有空间,则拨出一块空间给对象使用
*my_free_list = result -> free_list_link;
return (result);
};
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4wxaT7Lx-1617158146989)(C:/Users/Godfiry/AppData/Roaming/Typora/typora-user-images/image-20201124104946551.png)]
deallocate函数
static void deallocate(void *p, size_t n)
{
obj *q = (obj *)p;
obj * __VOLATILE * my_free_list;
if (n > (size_t) __MAX_BYTES) {
malloc_alloc::deallocate(p, n);
return;
}
//如果是小内存块的释放,则还是先找到所释放内存块在free_list[]中的位置
my_free_list = free_list + FREELIST_INDEX(n);
q -> free_list_link = *my_free_list;
// 调整free_list上的首地址,即返还给free_list中的这个节点
*my_free_list = q;
}
refill函数重新填充
当它发现free list中没有可用区块了时,就调用refill(),准备为free list重新填充空间。 新的空间将取自内存池(经一
chunk_alloc()完成)。缺省取得20个新节点(新区块),但万一不够,可能小于20个。
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
// 默认填充20个(n字节上调至8的整数倍)的内存块
int nobjs = 20;
// 这个函数的作用是尝试取得nobjs个(n字节上调至8的整数倍)的内存块作为free_list的新节点
// 这里需要注意取得的不一定是20个区块,如果内存池的空间不够,它所获得的区块数目可能小于20个
char * chunk = chunk_alloc(n, nobjs);
obj * __VOLATILE * my_free_list;
obj * result;
obj * current_obj, * next_obj;
int i;
// 如果申请到的区块数目为1,则直接返还给对象使用
if (1 == nobjs) return(chunk);
// 如果不为1则找到区块在free_list[]中所对应位置
my_free_list = free_list + FREELIST_INDEX(n);
//从头拨出1个申请好的区块在下面返还给对象,把剩余的区块全部链在free_list[FREELIST_INDEX]下面
result = (obj *)chunk;
*my_free_list = next_obj = (obj *)(chunk + n);
for (i = 1; ; i++) {
current_obj = next_obj;
//chunck_alloc返回的空间类型为char*
next_obj = (obj *)((char *)next_obj + n);
//如果已经链上的节点的个数等于从内存池申请的节点的个数-1,终止循环
if (nobjs - 1 == i) {
current_obj -> free_list_link = 0;
break;
} else {
current_obj -> free_list_link = next_obj;
}
}
return(result);
}
内存池
refill函数中使用到的chunk_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) {
// 剩余大小不满足申请需求,但是能够供应一个区块以上的大小,也就是refill里说明的获得的区块数目可能小于20个
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);
// 尝试内存池中剩余的小空间分配给合适的free-lists
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;
}
// 用malloc给内存池分配空间
start_free = (char *)malloc(bytes_to_get);
if (0 == start_free) {
// 分配失败
int i;
obj * __VOLATILE * my_free_list, *p;
// 在free_lists中查找没有使用过的内存块,并且它足够大
for (i = size; i <= __MAX_BYTES; i += __ALIGN) {
my_free_list = free_list + FREELIST_INDEX(i);
p = *my_free_list;
// 把free-lists中的内存编入内存池
if (0 != p) {
*my_free_list = p -> free_list_link;
start_free = (char *)p;
end_free = start_free + i;
return(chunk_alloc(size, nobjs)); // 递归调用,剩余的的零头会被重新编入合适的free-lists
}
}
// 如果free_list中也没有内存块了
end_free = 0;
// 试着调用一级空间配置器,可能会抛出异常或者申请到内存
start_free = (char *)malloc_alloc::allocate(bytes_to_get);
}
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
return (chunk_alloc(size, nobjs));
}
}
总结
STL的分配器用于封装STL容器在内存管理上的底层细节。在C++中,其内存配置和释放如下:
new运算分两个阶段: ⑴调用::operator new配置内存; (2)调用对象构造函数构造对象内容
delete运算分两个阶段: ⑴调用对象希构函数;(2)掉员工::operator delete释放内存
为了精密分工,STL allocator将两个阶段操作区分开来:
内存配置有alloc::allocate()负责,内存释放由alloc:: deallocate()负责;
对象构造由::construct()负责,对象析构由::destroy()负责。
同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL 采用了两级配置器
当分配的空间大小超过128B时,会使用第一级空间配置器;当分配的空间大小小于128B时,将使用第二级空间配置器。
第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。