<STL系列; 配置器
Posted 小贝也沉默
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了<STL系列; 配置器相关的知识,希望对你有一定的参考价值。
一、 概述
只有看大牛的代码,才知道自己是多弱。(个人理解)
最近一直在研习STL源码。主要学习手册是选用侯捷老师的《STL源码剖析》。确实是一本学习STL源码的绝佳学习指导, 但不可少的还有一份完整的STL源码。在看书的同时, 肯定要结合标准的源码效果才好。当然,更好的方式肯定是在此基础上,再进行模仿,自己写一段代码。
STL是开源库, 全称 Standard Template Library(标准模板库)。这是一个大部分C++程序员最熟悉的陌生人。官网http://www.sgi.com/tech/stl/index.html。从官网上可以下载到最新版本的STL源码。也可以到http://download.csdn.net/detail/sunbibei/9167701进行下载,都是免费的。下载下来之后,可以看到所有文件都是头文件或没有后缀的文件。千万不要认为下载错了。所有的实现都在这些文件里面。
我的做法是用Eclipse新建了一个C++工程,然后将所有头文件复制到该工程下,方便我查看代码,以及一些测试代码的编写。
刚准备入手学习STL时,相信每个不了解STL的人都会有同一个问题,就是不知道从何入手,如此多的头文件。这个时候侯捷老师的《STL源码剖析》就派上了用场。在一大堆关于STL的历史和发展过程以及其他东西的介绍之后,从第二节开始就正式进入了状态。下述文字,转载自《STL源码剖析》。
STL六大组件:
- 容器(containers): 各种数据结构,如vector, list, deque, set, map用来存放数据.从实现角度来看,STL容器时一种class template.就体积而言,这部分很像冰山在海面下的比例.
- 算法(algorithms): 各种常用算法,如sort,search, copy, erase…,从实现来看,STL算法是一种function template.
- 迭代器(iterators): 扮演容器与算法之间的胶合剂.是所谓的”泛型指针”.共有五种类型,以及其他衍生变化.从实现来看,迭代器是一种将operator*, operator->,operator++,operator–等指针相关操作予以重载的class template.所有STL容器都附带有自己的专属迭代器.原生指针(native pointer)也是一种迭代器.
- 仿函数(functors): 行为类似于函数,可以作为算法的某种策略.从实现的角度来看,仿函数是一种重载了operator()的class或class template.一般函数指针可视为狭义的仿函数.
- 适配器(adapters): 一种用来修饰容器或仿函数或迭代器接口的东西.例如,STL提供queue和stack,虽然看似容器,其实只能算是一种容器适配器,因为他们的底部完全借助deque,所有操作都由底层的deque供应.改变functor接口者,称为function adapter.改变container接口者,称为container adapter;改变iterator接口者,称为iterator adapter.
- 配置器(allocators): 负责空间配置与管理.从实现的角度来看,配置器是一个实现了动态空间配置,空间管理,空间释放的class template.
二、 级联的配置器
配置器,是为对象配置内存空间的一个类或一系列方法。对于使用者来说,是不可见的。它在暗处起着至关重要的作用,也是最底层的一个实现。STL的源码研习,当然是逃不开这一块内容的。
头文件: memory
一般而言,在日常的使用当中,对于某个类的对象申明时,都是将内存配置和对象的构建混做一个步骤来执行。例如下述代码:
/* class A
...
*/
A* obj = new A();
delete obj;
上述代码中,在第一句话中,我们申明了一个类A的对象obj,然后再释放这个对象。执行步骤大致如下:
- 配置内存
- 调用类A的构造函数
- 调用类A的析构函数
- 释放内存
两句话, 分别将构造函数,配置内存合并以及析构函数,释放内存给合并了.而STL的典型思路,为了精细分工,STL allocator决定将上诉过程拆分.内存配置操作由 alloc::allocate()
负责,内存释放操作由alloc::deallocate()
负责.对象构造操作由 ::construct()
负责, 对象析构操作由 ::destroy()
负责.其中, ::construct()
和::destroy()
是两个全局函数.
而memory文件中, 首当其冲的就是几个头文件的#include
,而配置器的定义就位于其中stl_aloc.h文件中.
STL的配置器是一个级联形式的配置器.第一级是一块高速缓存,对于频繁,小块的内存配置进行响应.当第一级配置器不能处理配置请求时, 会转交第二级进行处理, 第二级配置器响应对大块内存区域的配置.
2.1 第一级配置器
下述代码是摘自STL源码,当然,去掉了所有内联函数的实现,仅仅保留了其申明.完整版本见后文附录.
enum _ALIGN = 8;
enum _MAX_BYTES = 128;
enum _NFREELISTS = 16;
template <bool threads, int inst>
class __default_alloc_template
private:
static size_t _S_round_up(size_t);
union _Obj
union _Obj* _M_free_list_link;
char _M_client_data[1];
;
private:
static _Obj* _S_free_list[];
static size_t _S_freelist_index(size_t);
static void* _S_refill(size_t);
static char* _S_chunk_alloc(size_t, int&);
static char* _S_start_free;
static char* _S_end_free;
static size_t _S_heap_size;
class _Lock;
friend class _Lock;
class _Lock
public:
_Lock() __NODE_ALLOCATOR_LOCK;
~_Lock() __NODE_ALLOCATOR_UNLOCK;
;
public:
static void* allocate(size_t);
static void deallocate(void*, size_t);
static void* reallocate(void*, size_t, size_t);
;
template <bool __threads, int __inst>
char* __default_alloc_template<__threads, __inst>::_S_start_free = 0;
template <bool __threads, int __inst>
char* __default_alloc_template<__threads, __inst>::_S_end_free = 0;
template <bool __threads, int __inst>
size_t __default_alloc_template<__threads, __inst>::_S_heap_size = 0;
template <bool __threads, int __inst>
typename __default_alloc_template<__threads, __inst>::_Obj*
__default_alloc_template<__threads, __inst> ::_S_free_list[]
= 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ;
由上述类申明可以看出,其public
属性的函数仅有三个,对于使用者而言,他也只是关心怎么配置空间和释放空间而已.并且,所有成员函数都是static
属性.而类申明之前的三个enum
,只是充当一个常量定义而已.
第一级配置器, 管理个一个free list和一块内存池来完成对使用者频繁小块内存的申请以及管理, 用以防止可能出现的内存碎片,提高整体的运行效率和安全性.当然,代码中对小块内存是有严格定义的: enum _MAX_BYTES = 128;
第一级配置器可分配的内存最大是128 byte.从代码中,我们也可以看出, 最小分配内存是8 byte(当然, 使用者申请小于8 byte的内存块,也是由第一级配置器完成任务.具体细节后文详细解说).
free list: 顾名思义, 就是管理一个线性链表.链表中每个节点所代表的就是当前空闲的内存块.示意图如下:
该类的私有成员_S_free_list
即是链表的链表头.下面管理16个节点.每个节点分别是8, 16, 24, …, 128内存块的首地址.
内存池: 顾名思义, 就是由第一级配置器管理的一大块内存, 供free list没有空闲内存块时快速的分配内存空间.
下面是__default_alloc_template ::allocate(size_t)
的实现,也是第一级配置器最复杂代码量最多的一块:
/* __n 必须大于0, 实现中并未出现对__n小于0的容错代码 */
static void* allocate(size_t __n)
void* __ret = 0;
// 当申请内存大于_MAX_BYTES(128)时,调用第二级配置器
if (__n > (size_t) _MAX_BYTES)
__ret = malloc_alloc::allocate(__n);
// 如果申请空间在_MAX_BYTES(128)以内,则使用第一级配置器
else
// 首先找到负责管理大小为__n的内存区域的free list
// _S_freelist_index(size_t __n) 实现对__n对应free list
// 索引的计算.如, 输入6时, 返回0; 输入124时, 返回15.
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
// 在申明_Lock对象时会调用构造函数,请求线程锁.
// 确保在退出或者栈解绑时被释放(在析构函数中释放线程锁).
// 下述代码是对多线程的安全控制.我们不过分关心这个东西的实现.
# ifndef _NOTHREADS
/*REFERENCED*/
// _Lock是__default_alloc_template 的内部类
_Lock __lock_instance;
# endif
// 获得对应节点的首地址, __my_free_list的计算见else中第一行代码
// 假设输入是6, 则__result是第1(索引是0)个节点的首地址
_Obj* __RESTRICT __result = *__my_free_list;
if (__result == 0)
// 如果8byte内存块没有了, 则重新再内存池中获取内存,填充8byte
// 内存块.具体实现见后文.
// _S_round_up(size_t __n)这个函数实现上调__n到大于__n的
// 最小8的倍数,例如输入7, 则上调7到8;输入20,则上调至24,
// 依次类推.
// 其实现很简洁.仅一句话搞定.
// (((__n) + (size_t) _ALIGN - 1) & ~((__n) _ALIGN - 1));
// 假设预期输出为x, &之前括弧内将__n增大到[x, x + 8)范围内.
// &之后括弧内是0x11111000.即将[x, x + 8)范围内的这个数
// 2进制表示的最后三位置零.显然, 无论是[x, x + 8)范围内任意
// 数, 都将得到x.
// 如果还是理解不了, 可以这样想.8的倍数肯定是能够被8整除的.
// 也就是说,连续除3次2, 都是整除.用二进制表示的话, 除2就是
// 将二进制右移一位.连续移动3次,则只需要最后3为都是0,就能
// 满足整除了.
__ret = _S_refill(_S_round_up(__n));
else
// 运气好,还有8byte内存块,则将第一块分配的用户.
// 修正管理对应内存块的free list地址,指向下一个内存块.
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
return __ret;
;
接着上面__default_alloc_template ::allocate(size_t)
的余热, 我们再将其中遇到的一个函数__default_alloc_template ::_S_refill(size_t)
给搞定:
/* 返回大小为__n的对象, 这个函数实现的假设是__n已经被上调过. */
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
// 默认从内存池中获取的__n大小内存块的个数. 配置一个,剩下的交由
// free list管理
// 也就是说, 无论你需要几个__n内存块, 都多申请一些.多出来的给
// free list为下一次配置预留
int __nobjs = 20;
// _S_chunk_alloc函数的功能是从内存池中申请__nobjs * __n大小
// 的内存
// __chunk为首地址, __nobjs是以引用形式传参, 带出被函数修改后
// 的值.表示获取__n内存块个数
// 不用判定__chunk是否为空.因为不能正确配置的话,这个地方会有异常
// 抛出.这个函数的实现后文再细说.
char* __chunk = _S_chunk_alloc(__n, __nobjs);
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
// 如果从内存池申请到的空间只有一个__n大小.则直接交给客户,不用向
// free list中添加新的节点了,至少这一次配置内存完成任务了.
// 以后的事,以后再说吧.下一次如果会失败, 那到下一次再说
if (1 == __nobjs) return(__chunk);
// 下述代码都是在获取多个(大于1个)__n内存块的前提下
// 找到管理该大小内存的free list
__my_free_list = _S_free_list + _S_freelist_index(__n);
/* 强制类型转换 */
__result = (_Obj*)__chunk;
// 跳过第一个__n大小的内存块.因为第一个要交给客户使用
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
// 依次将后面__nobjs-1个内存块加入到free list中.
for (__i = 1; ; __i++)
__current_obj = __next_obj;
__next_obj = (_Obj*)((char*)__next_obj + __n);
if (__nobjs - 1 == __i)
// 将最后一个节点指向空(NULL)
__current_obj -> _M_free_list_link = 0;
break;
else
__current_obj -> _M_free_list_link = __next_obj;
return(__result);
由上述实现可以看出来, 实际上的free list或许并不像我前面示意图那样.初始条件下,并不是所有节点下面都是由内存块的.只是被配置过大小的节点下方有内存块被串起来.因为你配置过一次这个大小的内存块, free list会向内存池申请一堆内存块放在后面.这样也切合实际使用, 之前申请过该大小的内存块,在以后的使用中,很可能被再次申请.这样也会带来效率的上升.
现在, 我们该来看一下, free list是怎么向内存池要内存的了.下面是__default_alloc_template ::_S_chunk_alloc(size_t, int&)
的实现:
/* 为了避免使用malloc heap造成的内存碎片,我们分配用较大的内存块来分配
内存 我们假设这个大小是被适当的校正过的
应该注意到,其中第二格参数,是引用的形式存在
第一个参数是需要配置的大小.
第二格参数是获得的_Obj的个数,默认是20个 */
/* 假设我申请11个字节大小的内存, 整体处理流程(不限于该函数)如下:
1) 上调11字节到16字节.
2) 如果当前free list没有16字节内存块了,则会调用该函数
3) 默认会让内存池给16字节free list配置20个16字节内存块.
4) 如果内存池内存够使, 那么直接返回20个16字节内存块,返回给
用户1个,留下19个在free list中管理.
如果当前内存池内存也不够使了,那么想系统申请40(注意,是
2 * 20)个16字节块 + 附加量.
申请成功,则给free list 20个,剩下的保留在内存池中.供
下一次使用.
申请不成功的话,就进行一系列操作(见代码) */
template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size, int& __nobjs)
// 应该留意到,该函数主体由三个部分构成
// 1. 内存足够大.直接从内存池配置内存到free list,然后返回
// 2. 内存够当前使用.则修正分配的个数__nobjs.然后配置内存到
// free list,然后返回
// 3. 内存不够使了.这个时候处理就要复杂一点.具体看代码实现.
char* __result;
size_t __total_bytes = __size * __nobjs;
// 这个地方终于见到了__default_alloc_template其中私有的三个
// 静态成员中的两个出现了.
// 从代码中可以看出来, 原来他们是用来标记内存池的起始地址的.
// 内存池剩余的内存大小
size_t __bytes_left = _S_end_free - _S_start_free;
// 内存池内存足够使用(大于准备配置的大小__size * __nobjs)
// 调整内存池首地址,并返回结果
if (__bytes_left >= __total_bytes)
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
else if (__bytes_left >= __size)
// 当内存池仅不够分配__size * __nobjs,但足够分配[1,__nohjs)
// 个__size时,则, 有几个就给几个吧.先把这一次配置给糊弄过去,
// 不到万不得已, 以完成任务为首要
__nobjs = (int)(__bytes_left/__size);
__total_bytes = __size * __nobjs;
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
else
// __bytes_left < __size, 完了, 内存池空间连一个__size
// 都不够了.不能糊弄了, 现在只有老老实实的向系统申请内存了.
// 计算需要申请内存大小.由两部分构成
// 1. 2倍的__size * __nobjs
// 2. 随着申请的次数而递增的附加量_S_heap_size.配置结束后,
// 会对_S_heap_size进行修正
size_t __bytes_to_get =
2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
// 首先,内存池剩下的内存也不能浪费,也得用了先.我们使用内存池
// 中还剩余的空间.将_S_start_free的下一个节点指向该未使用地址
if (__bytes_left > 0)
_Obj* __STL_VOLATILE* __my_free_list =
_S_free_list + _S_freelist_index(__bytes_left);
((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
*__my_free_list = (_Obj*)_S_start_free;
// 调用malloc向系统申请内存
_S_start_free = (char*)malloc(__bytes_to_get);
if (0 == _S_start_free)
// 向系统申请内存失败了.
size_t __i;
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __p;
// 我们不会尝试在比__size更小的内存块去寻找未使用内存,
// 这样在多进程机器中会带来灾难性的危险.
// 用我们手上有的去做尝试.这样不会带来伤害.
// 怎么尝试?
for (__i = __size;
__i <= (size_t) _MAX_BYTES;
__i += (size_t) _ALIGN)
// 从__size大小区域开始,逐一向上寻求free list中
// 未使用内存
__my_free_list = _S_free_list + _S_freelist_index(__i);
__p = *__my_free_list;
if (0 != __p)
// 如果存在更大块的未使用内存,回收一块到内存池中.
*__my_free_list = __p -> _M_free_list_link;
_S_start_free = (char*)__p;
_S_end_free = _S_start_free + __i;
// 递归的调用自身,保证回收回来的这块内存得到正确
// 的使用.
return(_S_chunk_alloc(__size, __nobjs));
// 如果还是不行.则调用第二级配置器再次尝试分配
// __bytes_to_get内存
// 其实也是调用malloc向系统申请内存.但是第二级配置器中有
// _S_oom_malloc(__n)函数
// 会对out-of-memory进行一定得处理.没有别的办法了,也只
// 能这样看看能不能有点作用了.
// 即使失败了,这个地方也会抛出一个异常.如果客户不对其进行
// 判断,那么就怪不了STL了
_S_end_free = 0; // In case of exception.
_S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
// 如果申请内存成功了.那么修正内存池的各个标志位.
// 然后再次递归调用自身.这个时候应该在前两个条件(见刚进入函数
// 时的注释)中被返回.
// 这样能够保证成功配置到内存,并且能够正确的被使用
_S_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;
return(_S_chunk_alloc(__size, __nobjs));
当这儿, 第一级配置器的__default_alloc_template ::allocate(size_t)
函数就全部解析完了.整体流程图如下:
其中第二级配置器子流程见第三节. 向内存池申请内存子流程如下:
上图中x代表附加量.
下面是__default_alloc_template ::deallocate(void*, size_t)
的实现:
/* __p不能为空.代码中也可以看到,没有对__p为空的容错代码 */
static void deallocate(void* __p, size_t __n)
// 坚持一个原则: 谁配置谁释放.
if (__n > (size_t) _MAX_BYTES)
// 当__n大于_MAX_BYTES(128)时, 则这块内存是第二级配置器配置
// 的,则直接交由第二级配置器去释放
malloc_alloc::deallocate(__p, __n);
else
// __n小于_MAX_BYTES(128)时, 是自己的活, 就得自己干了.
// 找到管理该大小内存块的free list
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
_Obj* __q = (_Obj*)__p;
// 下述代码是多线程安全控制, 我们不关心这个.
// acquire lock
# ifndef _NOTHREADS
/*REFERENCED*/
_Lock __lock_instance;
# endif /* _NOTHREADS */
// 将该块内存的free list再次加入到原始list中来.加在list的
// 第一个节点.
__q -> _M_free_list_link = *__my_free_list;
*__my_free_list = __q;
// lock is released here
下面是__default_alloc_template ::reallocate(void*, size_t, size_t)
的实现:
template <bool threads, int inst>
void*
__default_alloc_template<threads, inst>::reallocate(void* __p,
size_t __old_sz,
size_t __new_sz)
void* __result;
size_t __copy_sz;
// 当新size和老size都是由第一级配置器配置的,则直接交给第一级配置器
// 处理,即直接调用realloc即可.
if (__old_sz > (size_t) _MAX_BYTES && __new_sz > (size_t) _MAX_BYTES)
return(realloc(__p, __new_sz));
// 老size实际配置的内存大小是上调过的.如果需重新配置的内存未超过
// 实际配置内存,则直接返回
if (_S_round_up(__old_sz) == _S_round_up(__new_sz)) return(__p);
// 否则,直接先配置新size的内存块.
__result = allocate(__new_sz);
// 判定需内存拷贝的size
__copy_sz = __new_sz > __old_sz? __old_sz : __new_sz;
memcpy(__result, __p, __copy_sz);
// 释放原始内存块.
deallocate(__p, __old_sz);
return(__result);
2.2 第二级配置器
第二级配置器实际上没有什么可以特别说明的地方.因为第二级配置器几乎就是对系统调用的一个封装而已.
类申明如下:
template <int __inst>
class __malloc_alloc_template
private:
static void* _S_oom_malloc(size_t);
static void* _S_oom_realloc(void*, size_t);
static void (* __malloc_alloc_oom_handler)();
public:
static void* allocate(size_t);
static void deallocate(void*, size_t);
static void* reallocate(void*, size_t, size_t);
static void (* __set_malloc_handler(void (*__f)()))();
;
上述申明中, 有一个函数指针, 以及一个参数为函数指针的函数.这个将在后面的代码中详细解释.
由于上述函数中allocate(size_t)
, deallocate(void*, size_t)
和reallocate(void*, size_t, size_t)
的实现较为简单, 我直接复制在下方:
// 简单的封装,如果malloc(.)分配内存成功,则直接返回.
// 若malloc(.)分配内存失败,则调用_S_oom_malloc(size_t)
static void* allocate(size_t __n)
void* __result = malloc(__n);
if (0 == __result) __result = _S_oom_malloc(__n);
return __result;
// 释放内存.直接调用free();
static void deallocate(void* __p, size_t /* __n */)
free(__p);
// 简单的封装,如果realloc(.)分配内存成功,则直接返回.
// 若realloc(.)分配内存失败,则调用_S_oom_realloc(size_t)
// 注意到一点,其中第二个参数并未使用.
static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
void* __result = realloc(__p, __new_sz);
if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
return __result;
从上面的实现中可以看到, 每一次配置失败时, 都会调用_S_oom_xxx(void*, size_t)
这个函数.而在这之前, 我们应该先看一下剩下的那个public
函数的实现:
// 一个函数指针,该函数的参数仍为函数指针.
static void (* __set_malloc_handler(void (*__f)()))()
void (* __old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = __f;
return(__old);
从代码中可以看到, 其实它的实现比较简单, 就是对私有的函数指针进行复制而已.而被赋值这个函数指针是用来再内存配置失败的情况下做出相应处理的函数.
下面我们就来看一下_S_oom_xxx(void*, size_t)
这个函数到底是什么鬼:
// malloc分配内存失败后调用函数
template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)
void (* __my_malloc_handler)();
void* __result;
// 首先判断__malloc_alloc_oom_handler是否被赋值
// 未赋值则抛出异常.
// 被赋值的话,就执行该函数,处理内存溢出(out-of-memory)后再进行
// 内存分配.若仍是分配失败,再次进行清理.直至成功分配到内存
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);
// realloc分配内存失败后调用函数
template <int __inst>
void* __malloc_alloc_template<__inst>::_S_oom_realloc(void* __p, size_t __n)
void (* __my_malloc_handler)();
void* __result;
// 处理方式类似于_S_oom_malloc(.)
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);
自此, 两级的配置器都已经解析完毕了.
以上是关于<STL系列; 配置器的主要内容,如果未能解决你的问题,请参考以下文章