浅析STL allocator

  一般而言,我们习惯的 C++ 内存配置操作和释放操作是这样的:


1 class FOO{};
2 FOO *pf = new FOO;
3 delete pf;

  我们看其中第二行和第三行,虽然都是只有一句,当是都完成了两个动作。但你 new 一个对象的时候两个动作是:先调用::operator new
分配一个对象大小的内存,然后在这个内存上调用FOO::FOO()构造对象。同样,当你 delete
一个对象的时候两个动作是:先调用FOO::~FOO() 析构掉对象,再调用::operator delete将对象所处的内存释放。为了精密分工,STL
将allocator决定将这两个阶段分开。分别用 4 个函数来实现:

  1.内存的配置:alloc::allocate();

  2.对象的构造:::construct();

  3.对象的析构:::destroy();

  4.内存的释放:alloc::deallocate();

  其中的 construct() 和 destroy()定义在 STL的库文件中,源代码如下:


 1 template <class T>
2 inline void destroy(T* pointer) {
3 pointer->~T(); //只是做了一层包装,将指针所指的对象析构---通过直接调用类的析构函数
4 }
5
6 template <class T1, class T2>
7 inline void construct(T1* p, const T2& value) {
8 new (p) T1(value); //用placement new在 p 所指的对象上创建一个对象,value是初始化对象的值。
9 }
10
11 template <class ForwardIterator> //destory的泛化版,接受两个迭代器为参数
12 inline void destroy(ForwardIterator first, ForwardIterator last) {
13 __destroy(first, last, value_type(first)); //调用内置的 __destory(),value_type()萃取迭代器所指元素的型别
14 }
15
16 template <class ForwardIterator, class T>
17 inline void __destroy(ForwardIterator first, ForwardIterator last, T*) {
18 typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
19 __destroy_aux(first, last, trivial_destructor()); //trival_destructor()相当于用来判断迭代器所指型别是否有 trival destructor
20 }
21
22
23 template <class ForwardIterator>
24 inline void //如果无 trival destructor ,那就要调用destroy()函数对两个迭代器之间的对象元素进行一个个析构
25 __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) {
26 for ( ; first < last; ++first)
27 destroy(&*first);
28 }
29
30 template <class ForwardIterator> //如果有 trival destructor ,则什么也不用做。这更省时间
31 inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {}
32
33 inline void destroy(char*, char*) {} //针对 char * 的特化版
34 inline void destroy(wchar_t*, wchar_t*) {} //针对 wchar_t*的特化版

  看到上面这么多代码,大家肯定觉得 construct() 和 destroy()
函数很复杂。其实不然,我们看到construct()函数只有几行代码。而 destroy()
稍微多点。但是这么做都是为了提高销毁对象时的效率。为什么要判断迭代器所指型别是否有 trival
destructor,然后分别调用不同的执行函数?因为当你要销毁的对象很多的时候,而这样对象的型别的destructor 都是 trival
的。如果都是用__destroy_aux(ForwardIterator first, ForwardIterator last,
__false_type)来进行销毁的话很费时间,因为没必要那样做。而当你对象的destructor 都是 non-trival
的时候,你又必须要用__destroy_aux(ForwardIterator first, ForwardIterator last,
__false_type)来析构。所以,我们要判断出对象型别的destructor 是否为 trival,然后调用不同的__destroy_aux。

  说完 construct() 和 destory() ,我们来说说 alloc::allocate()
和 alloc::deallocate(),其源代码在 <stl_alloc.h>中。stl_alloc.h中代码设计的原则如下:

  1.向 system heap 要求空间

  2.考虑多线程状态

  3.考虑内存不足时的应变措施

  4.考虑过多“小型区块”可能造成的内存碎片问题。

  stl_alloc.h中的代码相当复杂,不过没关系。我们今天只看其中的allocate() 和
deallocate()。在讲这两个函数之前,我们还必须来了解一下SGI 
STL(SGI限定词是STL的一个版本,因为真正的STL有很多不同公司实现的版本,我们所讨论的都是SGI版本) 配置器的工作原理:

  考虑到小型区块可能造成内存破碎问题(即形成内存碎片),SGI STL 设计了双层级配置器。第一层配置器直接使用malloc() 和
free().第二层配置器则视情况采用不同的策略:但配置区块超过 128 bytes时,调用第一级配置器。当配置区块小于 128 bytes时,采用复杂的
memory pool 方式。下面我们分别简单的介绍一下第一级和第二级配置器:

第一级配置器 _ _malloc_alloc_template:

  由于第一级配置器的配置方法比较简单,代码也容易理解,我在这里全部贴出:


 1 //以下是第第一级配置器
2 template <int inst>
3 class __malloc_alloc_template {
4
5 private:
6
7 //以下函数用来处理内存不足的情况
8 static void *oom_malloc(size_t);
9
10 static void *oom_realloc(void *, size_t);
11
12 static void (* __malloc_alloc_oom_handler)();
13
14 public:
15
16 static void * allocate(size_t n)
17 {
18 void *result = malloc(n); //第一级配置器,直接使用malloc()
19 //如果内存不足,则调用内存不足处理函数oom_alloc()来申请内存
20 if (0 == result) result = oom_malloc(n);
21 return result;
22 }
23
24 static void deallocate(void *p, size_t /* n */)
25 {
26 free(p); //第一级配置器直接使用 free()
27 }
28
29 static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)
30 {
31 void * result = realloc(p, new_sz); //第一级配置器直接使用realloc()
32 //当内存不足时,则调用内存不足处理函数oom_realloc()来申请内存
33 if (0 == result) result = oom_realloc(p, new_sz);
34 return result;
35 }
36
37 //设置自定义的out-of-memory handle就像set_new_handle()函数
38 static void (* set_malloc_handler(void (*f)()))()
39 {
40 void (* old)() = __malloc_alloc_oom_handler;
41 __malloc_alloc_oom_handler = f;
42 return(old);
43 }
44 };
45
46 template <int inst>    
47 void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;  //内存处理函数指针为空,等待客户端赋值
48
49 template <int inst>
50 void * __malloc_alloc_template<inst>::oom_malloc(size_t n)
51 {
52 void (* my_malloc_handler)();
53 void *result;
54
55 for (;;) { //死循环
56 my_malloc_handler = __malloc_alloc_oom_handler; //设定自己的oom(out of memory)处理函数
57 if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; } //如果没有设定自己的oom处理函数,毫不客气的抛出异常
58 (*my_malloc_handler)(); //设定了就调用oom处理函数
59 result = malloc(n); //再次尝试申请
60 if (result) return(result);
61 }
62 }
63
64 template <int inst>
65 void * __malloc_alloc_template<inst>::oom_realloc(void *p, size_t n)
66 {
67 void (* my_malloc_handler)();
68 void *result;
69
70 for (;;) {
71 my_malloc_handler = __malloc_alloc_oom_handler;
72 if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; } //如果自己没有定义oom处理函数,则编译器毫不客气的抛出异常
73 (*my_malloc_handler)(); //执行自定义的oom处理函数
74 result = realloc(p, n); //重新分配空间
75 if (result) return(result); //如果分配到了,返回指向内存的指针
76 }
77 }

  上面代码看似繁杂,其实流程是这样的:

  1.我们通过allocate()申请内存,通过deallocate()来释放内存,通过reallocate()重新分配内存。

  2.当allocate()或reallocate()分配内存不足时会调用oom_malloc()或oom_remalloc()来处理。

  3.当oom_malloc() 或 oom_remalloc()还是没能分配到申请的内存时,会转如下两步中的一步:

    a).调用用户自定义的内存分配不足处理函数(这个函数通过set_malloc_handler() 来设定),然后继续申请内存!

    b).如果用户未定义内存分配不足处理函数,程序就会抛出bad_alloc异常或利用exit(1)终止程序。

  看完这个流程,再看看上面的代码就会容易理解多了!

第二级配置器 _ _default_alloc_template:

  第二级配置器的代码很多,这里我们只贴出其中的 allocate() 和
dellocate()函数的实现和工作流程(参考侯捷先生的《STL源码剖析》),而在看函数实现代码之前,我大致的描述一下第二层配置器配置内存的机制。

  我们之前说过,当申请的内存大于 128 bytes时就调用第一层配置器。当申请的内存小于
128bytes时才会调用第二层配置器。第二层配置器如何维护128bytes一下内存的配置呢? SGI 第二层配置器定义了一个
free-lists,这个free-list是一个数组,如下图:

  

  这数组的元素都是指针,用来指向16个链表的表头。这16个链表上面挂的都是可以用的内存块。只是不同链表中元素的内存块大小不一样,16个链表上分别挂着大小为

   8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128 bytes的小额区块,图如下:

   

  就是这样,现在我们来看allocate()代码:


 static void * allocate(size_t n)
{
obj * __VOLATILE * my_free_list;
obj * __RESTRICT result;

//要申请的空间大于128bytes就调用第一级配置
if (n > (size_t) __MAX_BYTES) {
return(malloc_alloc::allocate(n));
}
//寻找 16 个free lists中恰当的一个
my_free_list = free_list + FREELIST_INDEX(n);
result = *my_free_list;
if (result == 0) {
//没找到可用的free list,准备新填充free list
void *r = refill(ROUND_UP(n));
return r;
}
*my_free_list = result -> free_list_link;
return (result);
};

  其中有两个函数我来提一下,一个是ROUND_UP(),这个是将要申请的内存字节数上调为8的倍数。因为我们free-lists中挂的内存块大小都是8的倍数嘛,这样才知道应该去找哪一个链表。另一个就是refill()。这个是在没找到可用的free
list的时候调用,准备填充free lists.意思是:参考上图,假设我现在要申请大小为 56bytes 的内存空间,那么就会到free lists 的第 7
个元素所指的链表上去找。如果此时
#7元素所指的链表为空怎么办?这个时候就要调用refill()函数向内存池申请N(一般为20个)个大小为56bytes的内存区块,然后挂到 #7
所指的链表上。这样,申请者就可以得到内存块了。当然,这里为了避免复杂,误导读者我就不讨论refill()函数了。allocate()过程图如下:

  

  学过链表的操作的人不难理解上图,我就不再讲解。下面看deallocate(),代码如下:


 1 static void deallocate(void *p, size_t n)
2 {
3 obj *q = (obj *)p;
4 obj * __VOLATILE * my_free_list;
5
6 //如果要释放的字节数大于128,则调第一级配置器
7 if (n > (size_t) __MAX_BYTES) {
8 malloc_alloc::deallocate(p, n);
9 return;
10 }
11 //寻找对应的位置
12 my_free_list = free_list + FREELIST_INDEX(n);
13 //以下两步将待释放的块加到链表上
14 q -> free_list_link = *my_free_list;
15 *my_free_list = q;
16 }

  deallocate()函数释放内存的步骤如下图:

  其实这就是一个链表的插入操作,也很简单。不再赘述!上面忘了给链表结点的结构体定义了,如下:


union obj{
union obj * free_list_link;
char client_date[1];
};

  至此,SGI STL的对象的构造与析构、内存的分配与释放就介绍完毕了。

浅析STL allocator,码迷,mamicode.com

时间: 2024-10-11 10:23:51

浅析STL allocator的相关文章

C++ 浅析 STL 中的 list 容器

list - 擅长插入删除的链表 链表对于数组来说就是相反的存在. 数组本身是没有动态增长能力的(程序中也必须又一次开辟内存来实现), 而链表强悍的就是动态增长和删除的能力. 但对于数组强悍的随机訪问能力来说的话,链表却非常弱. list - 是一个双向链表的实现. 为了提供双向遍历的能力,list要比一般的数据单元多出两个指向前后的指针. 这也是没办法的,毕竟如今的PC内存结构就是一个大数组,链表要在不同的环境中实现自己的功能就须要花很多其它空间. list提供了push_back,push_

stl allocator源码学习

概述 介绍几个allocator的源码实现:简单的对operator new和operator delete进行封装的实现,vs2015中的实现,STLport中的实现,仿造STLport实现内存池. 1. 参考 http://www.cplusplus.com/reference/memory/allocator/ <STL源码剖析> <C++ Primer 第五版> <Generic Programming and the STL>(<泛型编程和STL>

浅析STL 谓词 + 仿函数 + 函数指针(c)

一:起因 (0)提到C++ STL,首先被人想到的是它的三大组件:Containers(容器), Iterators(迭代器), Algorithms(算法).容器为用户提供了常用的数据结构(如,vector,list,deque,stack,map,multimap,set,multiset,外加string),算法大多是独立于容器的常用的基本算法(一般在algorithm头文件中,其中sort比较常用),迭代器是由容器提供的一种接口,算法通过迭代器来操控容器.详情请看 博客 (1)接下来要介

浅析STL算法中的堆排序

堆结构简述 了解过数据结构的人,应该对堆结构不陌生,堆的底层是使用数组来实现的,但却保持了二叉树的特性.堆分为两种,最大堆和最小堆,以最大堆为例,最大堆保持了根结点大于两个左右两个孩子,同时所有子树一次类推.由于堆底层是数组结构,这里从跟结点开始,按照层序依次走到最后一个结点,结点下标分贝为0~N-1.结构如下图: 上图中,紫色表示的是该元素在数组中的下标,可以看到,每个结点的值总是大于它的左右孩子,这里并没有规定左右孩子的大小关系,也没有规定不是同一棵树之间结点的大小关系.这就是最大堆.同时这

STL源码剖析 — 空间配置器(allocator)

前言 以STL的实现角度而言,第一个需要介绍的就是空间配置器,因为整个STL的操作对象都存放在容器之中. 你完全可以实现一个直接向硬件存取空间的allocator. 下面介绍的是SGI STL提供的配置器,配置的对象,是内存.(以下内容来自<STL源码剖析>) 空间配置器的标准接口 根据STL的规范,allocator的必要接口 各种typedef 1 allocator::value_type 2 allocator::pointer 3 allocator::const_pointer 4

STL 之 空间配置器(allocator)

一.SGI 标准的空间配置器,std::allocator SGI也定义了一个符合部分标准,名为allocator的配置器,但是它自己不使用,也不建议我们使用,主要原因是效率不佳. 它只是把C++的操作符::operator new和::operator delete做了一层简单的封装而已. 二.SGI 特殊的空间配置器,std::alloc 由于SGI 标准的空间配置器只是把C++的操作符::operator new和::operator delete做了一层简单的封装,没有考虑到任何效率上的

STL学习笔记--2、空间配置器 allocator

2.1标准接口 allocator::value_type allocator::pointer allocator::const_pointer allocator::reference allocator::const_reference allocator::size_type allocator::difference_type allocator::rebind allocator::allocator()//默认构造函数 allocator::allocator(const allo

C++ STL源码剖析

1. // Filename: stl_config.h 2. 3. // Comment By: 凝霜 4. // E-mail: [email protected] 5. // Blog: http://blog.csdn.net/mdl13412 6. 7. /* 8. * Copyright (c) 1996-1997 9. * Silicon Graphics Computer Systems, Inc. 10. * 11. * Permission to use, copy, mod

STL源码剖析(1):空间配置器

STL所有的操作对象(所有的数值)都存放在容器之内,容器需要分配空间以存放数据.为什么不说allocator是内存配置器而是空间配置器,因为空间不仅是内存,空间也可以是磁盘或其它辅助储存媒体.这里我们主要讨论内存配置. SGI STL每个容器缺省的空间配置器为alloc,如vector: template<class T, class Alloc = alloc> class vector{--} 一般而言,C++的内存配置和释放操作如下: class Object{--}; Object*