- 以STL的运用角度而言,空间配置器是最不需要介绍的东西,它总是隐藏在一切组件(更具体地说是指容器,container) 的背后
- 但是STL的操作对象都存放在容器的内部,容器离不开内存空间的分配
- 为什么不说allocator是内存配置器而说它是空间配置器呢?因为空间不一定 是内存,空间也可以是磁盘或其它辅助存储介质。是的,你可以写一个allocator, 直接向硬盘取空间。以下介绍的是SGI STL提供的配置器,配置的对象,这里分配的空间是指分配内存
- set_new_handler()总结
namespace JJ
{template <class T>inline T* _allocate(std::ptrdiff_t size,T*){std::set_new_handler(0);T* tmp = (T*)(::operator new ((std::size_t)(size * sizeof(T))));if (tmp == 0){std::cerr << "out of memory " << std::endl;exit(1);}return tmp;}template <class T>inline void _deallocate(T* buffer){::operator delete (buffer);}template <class T1,class T2>inline void _construct(T1* p,const T2& value){new(p) T1(value);}template <class T>inline void _destroy(T* ptr){ptr->~T();}template <class T>class allocator{public:typedef T value_type;typedef T* pointer;typedef const T* const_pointer;typedef T& reference;typedef const T& const_reference;typedef size_t size_type;typedef ptrdiff_t difference_type;//rebind allocator of type Utemplate <class U>struct rebind{typedef allocator<U> other;};//hint used for locality. ref.[Austern],pl89pointer allocate(size_type n,const void* hint = 0){return _allocate((difference_type)n,(pointer)0);}void deallocate(pointer p,size_type n){_deallocate(p);}void construct(pointer p,const T& value){_construct(p,value);}void destroy(pointer p){_destroy(p);}pointer address(reference x){return (pointer)&x;}const_pointer const_address(const_reference x){return (const_pointer)&x;}size_type max_size()const{return size_type (UINT_MAX / sizeof(T));}};
}
- SGI STL在这个项目上根本就逸脱了 STL标准规格,使用一个专属的、拥有次层配置(sub-allocation)能力的、效率优越的特殊配置器,稍后有详细介绍
- 备 次 配 置 力 (sub-allocation)的 S G I 空间配置器
- SGI STL的配置器与众不同,也与标准规范不同,其名称是a llo c 而非 allo ca to r,而且不接受任何参数。换句话说,如果你要在程序中明白采用SGI配 置器,则不能采用标准写法:
- SGI STL allocator未能符合标准规格,这个事实通常不会给我们带来困扰,因 为通常我们使用缺省的空间配置器,很少需要自行指定配置器名称,而SGI STL的每一个容器都已经指定其缺省的空间配置器为a llo c .例如下面的vector声明:
- S G I标 准 的 空 间 配 量 器 ,std::allocator
- SG I特 殊 的 空 间 配 置 器 ,std::alloc
- 上一节所说的a llo c a to r 只是基层内存配置/释放行为(也就是 ::operator new和 : :operator delete)的一层薄薄的包装,并没有考虑到任何效率上的强 化. S G I另有法宝供其本身内部使用。
- 这其中的new算式内含两阶段操作3: (1 )调 用 ::operator new配置内存; ⑵ 调 用Foo::Foo()构造对象内容。delete算式也内含两阶段操作:(1)调用Foo: :-Foo ()将对象析构;(2 ) 调 用 ::operator delete释放内存。
- 为了精密分工,STL a llo c a to r决定将这两阶段操作区分开来。内存配置操作 由 alloc: al locate ()负责,内存释放操作由alloc : : deallocate ()负责;对象构造操作由::construct:()负责,对象析构操作由:;destroy负责
- 内存空间的配置/释放与对象内容的构造/析构,分别着落在这两个文件身上。其 中 <stl_construct .h>定义有两个基本函数:构造用的 construct() 和析构用的destroy。。在一头栽进复杂的内存动态配置与释放之前,让我们先看清楚这 两个函数如何完成对象的构造和析构。
- 构造和析构基本工具:co n stru ct()和 destroy()
- 这两个作为构造、析构之用的函数被设计为全局函数,符合STL的规范.此外,STL还规定配置器必须拥有名为construct ()和 destroy ()的两个成员函数(见2.1节 ),然而真正在SGI STL中大显身手的那个名为std::alloc的配 置器并未遵守这一规则(稍后可见
- 上 述 construct ()接受一个指针p 和一个初值value,该函数的用途就是 将初值设定到指针所指的空间上。C++的 placement new 运算子5 可用来完成这一任务。
- destroy() 有两个版本,第一版本接受一个指针,准备将该指针所指之物析 构掉。这很简单,直接调用该对象的析构函数即可。第二版本接受first和 last 两个迭代器(所谓迭代器,第3 章有详细介绍),准备将 [first, last)范围内的所有对象析构掉。我们不知道这个范围有多大,万一很大,而每个对象的析构函数都无关痛痒(所谓rrzvia/destructor), 那么一次次调用这些无关痛痒的析构函数, 对效率是一种伤害。
- 因此,这里首先利用value_type()获得迭代器所指对象的 型别,再利用 _type_traits< T> 判断该型别的析构函数是否无关痛痒.若是 (一true_type), 则什么也不做就结束;若 否 (— false_type), 这才以循环 方式巡访整个范围, 并在循环中每经历一个对象就调用第一个版本的destroy ()
空 间 的 配 置 与 释 放 , std::alloc
- 对象构造前的空间配置和对象析构后的空间释放,由 <stl_alloc.h>负责, S G I 对此的设计哲学如下:
- C + + 的 内 存 配 置 基 本 操 作 是 ::operator new ( ) , 内存释放基本操作 是 : operator delete)). 这两个全局函数相当于C 的malloc ( ) 和 f r e e O 函 数。是的,正是如此,S G I 正是以malloc ()和 f r e e O 完成内存的配置与释放。
- 考虑到小型区块所可能造成的内存破碎问题,S G I 设计了双层级配置器,第一级配置器直接使用 malloc() 和 fme(),第二级配置器则视情况采用不同的策略:当配置区块超过128 bytes时,视 之 为 “足够大”,便调用第一级配置器;当配 置区块小于128 bytes时,视 之 为 “过小”,为了降低额外负担(overhead,见 2.2.6 节 ),便采用复杂的memory pool整理方式,而不再求助于第一级配置器, 整个设 计究竟只开放第一级配置器,或是同时开放第二级配置器,取决于_ use_malloc是否被定义(唔,我们可以轻易测试出来,SGI STL并未定义一_USE_MALLOC)
- 无论alloc被定义为第一级或第二级配置器,SGI还为它再包装一个接口如 下,使配置器的接口能够符合STL规格:
- 其内部四个成员函数其实都是单纯的转调用,调用传递给配置器(可能是第一级也可能是第二级)的成员函数.这个接口使配置器的配置单位从bytes转为个别元素的大小(sizeof (T) ) . SGI STL容器全都使用这个simple_alloc接口,例如:
第 一 级 配 置 器 ―malloc_alloc_template 剖析
- 第一级配置器以malloc () , free () , realloc ()等 C 函数执行实际的内存配置、释放、重配置操作,并实现出类似C + + new-hand宜了的机制。是的,它不能 直 接 运 用 C++ new-handier机制,因为它并非使用::operator n e w 来配置内存。
- 所 谓 C++ new handler机制是,你可以要求系统在内存配置需求无法被满足 时 ,调用一个你所指定的函数。换句话说,一 旦 ::operator new无法完成任务, 在 丢 出 std::bad_alloc异常状态之前,会先调用由客端指定的处理例程。该处理例程通常即被称为new-handiero new-handier解决内存不足的做法有特定的模式,请 参 考 《婀 伽 e C++》2 e 条款7
- handler
- 程通常即被称为new-handiero new-handier解决内存不足的做法有特定的模式
- 注意,S G I 以 malloc而 非 ::operator new来配置内存(我所能够想象的 一个原因是历史因素,另一个原因是C + + 并未提供相应于 realloc () 的内存配 置 操 作 ),因此,S G I 不能直接使用C + + 的 set_new_handler (),必须仿真一个类似的 set_malloc_handler ()
- 请 注 意 ,S G I 第 一 级 配 置 器 的 allocate ()和 realloc ( ) 都是在调用 malloc ()和 realloc ()不成功后,改调用 oom_malloc ()和 oom_realloc ()=后两者都有内循环,不 断 调 用 “内存不足处理例程”,期望在某次调用之后,获得足够的内存而圆满完成任务。但 如 果 “内存不足处理例程”并未被客端设定,oom_malloc() 和 oom_realloc() 便老实不客气地调用 — THROW_BAD_ALLOC,丢 出 bad_alloc异常信息,或 利 用 exit(l)硬生生中止程序。
- 记住,设 计 “内存不足处理例程”是客端的责任,设 定 “内存不足处理例程”也是客端的责任.再一次提醒你,“内存不足处理例程”解决问题的做法有着特定的模式,请 参 考 [Meyers98]条款7
第二级配置器 __default_alloc_template 剖析
- 第二级配置器多了一些机制,避免太多小额区块造成内存的碎片。小额区块带来的其实不仅是内存碎片,配置时的额外负担(overhead)也是一个大问题% 额外 负担永远无法避免,毕竟系统要靠这多出来的空间来管理内存,如图2-3所示。但是区块愈小,额外负担所占的比例就愈大,愈显得浪费。
- SGI第二级配置器的做法是,如果区块够大,超过128 bytes时,就移交第一 级配置器处理.当区块小于128 bytes时,则以内存池(memory pool)管理,此法 又称为次层配置(sub-allocation):
- 每次配置一大块内存,并维护对应之自由链表 {free-list). 下次若再有相同大小的内存需求,就直接从free-lists中拨出。如果客户端释还小额区块,就由配置器回收到free-lists中—— 是的,别忘了,配置器除了负 责配置,也负责回收。
- 为了方便管理,SGI第二级配置器会主动将任何小额区块的内存需求量上调至8 的倍数(例如客端要求30 bytes,就自动调整为32 bytes),并维护 16 个free-lists,各自管理大小分别为 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 bytes的小额区块。free-lists的节点结构如下:
- 诸君或许会想,为了维护链表(lists), 每个节点需要额外的指针(指向下一 个节点),这不又造成另一种额外负担吗?你的顾虑是对的,但早已有好的解决办法。
- 注意’上 述 obj所用的是union,由于union之故,从其第一字段观之, obj可被视为一个指针,指向相同形式的另一个。切从其第二字段观之。obj可 被视为一个指针,指向实际区块,如图2-4所示。一物二用的结果是,不会为了维护链表所必须的指针而造成内存的另一种浪费(我们正在努力节省内存的开销呢)。这种技巧在强型(strongly typed)语言如Java中行不通,但是在非强型语言如C++中十分普遍
空 间 配 置 函 数 allocate()
- 此函数首先判断区块大小,大 于 128 bytes就调用第一级配置器,小 于 128 bytes就检查对应的free listo 如 果 free list之内有可用的区块,就直接拿来 用,如果没有可用区块,就将区块大小上调至8 倍数边界,然后调用refilio, 准备为free list重新填充空间。refill ()将于稍后介绍。
- free_list维护8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 bytes的小额区块,需要分配内存的时候,如上图所示,需要分配的大小是96,my_free_list先找到96对应的链条,result指向想要的96区块,my_free_list移动到下一个区块,将result需要的区块排除到链外,表示为其分配了区间
空 间 释 放 函 数 deallocate。
- 身为一个配置器, _ default_alloc_template 拥有配置器标准接口函数deallocated o 该函数首先判断区块大小,大于128 bytes就调用第一级配置器, 小于128 bytes就找出对应的free list,将区块回收。
- 使用q指向需要回收的空间,使用my_free_list找到与之大小相匹配的存储区块的链条,使用q衔接对应的 存储区块的链条位置,移动初始位置
重 新 填 充 free lists
- 回头讨论先前说过的allocate (). 当它发现free list中没有可用区块了时, 就 调 用 refillO,准备为free list重新填充空间.新的空间将取自内存池(经由 chunk_alloC ()完 成 )。缺省取得2 0 个新节点(新区块),但万一内存池空间不 足,获得的节点数(区块数)可能小于20:
内 存 池 ( m e m o r y pool )
- 从内存池中取空间给斤e。 使用,是 chunk_alloc()的工作:
- 上图存在一个错误,size是需要的大小,nobjs是需要的数量
- 上述的chunk_alloc ( ) 函数以end_free - s t a r t _ f r e e 来判断内存池的水量。如果水量充足,就直接调出20个区块返回给free list。如果水量不足以提供20 个区块,但还足够供应一个以上的区块,就拨出这不足20个区块的空间出去。这时候其pass by reference的n o b js参数将被修改为实际能够供应的区块数。如果 内存池连一个区块空间都无法供应,对客端显然无法交待,此时便需利用m alloc 从 heap中配置内存,为内存池注入活水源头以应付需求。新水量的大小为需求量 的两倍,再加上一个随着配置次数增加而愈来愈大的附加量。
- 如果一开始free_list 和 内存池都是空的,当用户需求数据的时候,发现没有内存,申请的空间是20的两倍,也就是40,一半内存交给free_list用于数据的维护,一半数据给 内存池;当再次申请内存时,所对应的free_list不存在数据,就会先向内存池索要内存,满足一部分之后,将剩余的内存交给 free_list;
内存基本处理工具
- STL定义有五个全局函数,作用于未初始化空间上• 这样的功能对于容器的实现很有帮助,我们会在第4章容器实现代码中,看到它们肩负的重任 前两个函数是2.2.3节说过的、用于构造的construct ()和用于析构的destroy (),另三个函数uninitialized_copy(),uninitialized_fill(),uninitialized_fill_n(), 分别对应于高层次函数copy () fill () fill_n() 这些都是STL算法,将在第6 章介绍。如果你要使用本节的三个低层次函数,应该包含 <memory>, 不过SG I把它们实际定义于 <stl_uninitialized>。
uninitialized_copy
- uninitialized_copy ()使我们能够将内存的配置与对象的构造行为分离开来。如果作为输出目的地的[result/ result+(last-first))范围内的每一个迭代器都指向未初始化区域,则 uninitialized_copy ()会使用copy constructor,给身为输入来源之[first, last)范围内的每一个对象产生一份复制品’放进输出 范围中。换句话说,针对输入范围内的每一个迭代器该函数会调用
construct (&* (result+ (i-f irst) ) , *i), 产 生 * i 的复制品,放置于输出范围的相对位置上。式中的construct ()已于2.2.3节讨论过。 - result 指向内存拷贝的目的地址,i和first指向的是同一个内存区间,使用i和first之间的差值,将*i(元素的数值)拷贝到指定的位置
- 果你需要实现一个容器,uninitialized.copy() 这样的函数会为你带来 很大的帮助,因为容器的全区间构造函数(range constructor) 通常以两个步骤完 成:
- C++ 标准规格书要求 uninitialized_copy () 具 有 'commit or rollback 语 意,意思是要么“构造出所有必要元素”,要么(当有任何一个copy constructor失 败时)“不构造任何东西””
2.3.2 u n in itia liz e d _fill
- &*i 其实就是元素的位置,*i代表元素的数值,加上&,就是取地址,后面接入x,construct就是在指定的位置上填写x
- 与 uninitialized_copy() 一样,uninitialized_f ill ()必须具备 acommit or rollback语意,换句话说,它要么产生出所有必要元素,要么不产生任何元素。如果有任何一个 copy constructor 丢出异常(exception) ,uninitialized_f ill (),必须能够将已产生的所有元素析构掉。
u n in itia liz e d _ fill_ n
- uninitialized_fill_n ()能够使我们将内存配置与对象构造行为分离开来。它会为指定范围内的所有元素设定相同的初值。
- fill_n
- 如果是POD类型,使用_true_type,交由高阶函数执行,使用fill_n对每个元素进行赋值
- 如果不是POD类型,使用_flase_type,调用construct函数对每个元素进行赋值
uninitialized_copy
- 这个函数的进行逻辑是,首先萃取出迭代器result的 value type (详见第3 章 ) , 然后判断该型别是否为PO D 型别:
- 如果是POD类型,采用最有效率的办法就是复制的方式,内部调用fill_n对元素进行复制
- 如果不是POD类型,只能使用最保险安全的construct的函数构造的方式
- char是一个字节
- wchar_t 使用sizeof判断不同环境下的占据的单个元素的空间大小
- 使用memmove 直接对指定的内存区间进行数据的移动
u n in itia liz e d _ fill