为什么不说allocator是内存配置器而说它是空间配置器呢?因为,空间不一定是内存,空间也可以是磁盘或其它辅助储存媒体。是的,你可以写一个 allocator,直接向硬盘取空间1。以下介绍的是 SGI STL 提供的配置器,配置的对象,呃,是的,是内存!
2.2具备次配置力(sub-allocation)的SGI 空间配置器
SGI STL 的 配 置 器 与 众 不 同 , 也 与 标 准 规 范 不 同 , 其 名 称 是alloc而 非allocator,而且不接受任何自变量。 换句话说如果你要在程序中明白采用SGI
配置器,不能采用标准写法:
vector<int,std::allocator<int> iv;// in VC or CB
必须这么写:
vector<int,std::alloc> iv; // in GCC
2.2.1 SGI 标准的空间配置器,std::allocator
虽然 SGI 也定义有一个符合部份标准、名为allocator的配置器,但SGI自己从用过它,也不建议我们使用。主要原因是效率不彰,只把 C++的::operatornew和::operator delete做一层薄薄的包装而已。
2.2.2SGI 特殊的空间配置器,std::alloc
一般而言,我们所习惯的 C++ 内存配置动作和释放动作是这样:
class Foo { ... };
Foo* pf = new Foo;//配置内存,然后建构对象
delete pf; //将对象解构,然后释放内存
这其中的 new算式内含两阶段动作3: (1)呼叫::operator new配置内存, (2)
呼叫Foo::Foo()建构对象内容。 delete算式也内含两阶段动作: (1)呼 叫 Foo::~Foo()将对象解构, (2)呼叫::operator delete释放内存。
为了精密分工, STL allocator决定将这两阶段动作区分开来。内存配置动作由alloc:allocate()负责,内存释放动作由alloc::deallocate()负责;对象建构动作由::construct()负责,对象解构动作由::destroy()负责。
2.2.3构造和析构基本工具:construct()和destroy()
这两个做为建构、解构之用的函式被设计为全局函数,符合 STL 的规范4。此外STL 还规定配置器必须拥有名为 construct()和destroy()的两个成员函式(见2.1 节), 然而真正在 SGI STL 中大显身手的那个名为 std::alloc 的配置器并不遵守此一规则
上述construct()接受一个指标p和一个初值value,此函式的用途就是将初值设定到指标所指的空间上。 C++ 的placement new运算子5可用来完成此一任务。
destroy()有两个版本 ,第一版本接受一个指标,准备将该指标所指之物解构掉。 这很简单,直接呼叫该对象的解构式即可。第二版本接受first和last两个迭代器(所谓迭代器,第三章有详细介绍), 准备将[first,last)范围内的所有物件解构掉。我们不知道这个范围有多大,万一很大,而每个物件的解构式都无关痛痒(所谓 trivialdestructor), 那么一次次呼叫这些无关痛痒的解构式,对效率是一种蕲伤。因此,这里首先利用value_type()获得迭代器所指物件的型别,再 利 用__type_traits<T>判 别 该 型 别 的 解 构 式 是 否 无 关 痛 痒 。 若 是( __true_type), 什么也不做就结束;若否( __false_type), 这才以循环方式巡访整个范围,并在循环中每经历一个对象就呼叫第一个版本的 destroy()。
但是C++ᴀ身并不直接支持对「指标所指之物」的型别判断,也不支持对「对象解构式是否为trivial」的判断,因此, 上述的value_type()和__type_traits<>该如何实作呢?
2.2.4空间的配置与释放,std::alloc
对象构造前的空间配置,和对象析构后的空间释放,由<stl_alloc.h>负责, SGI
对此的设计哲学如下:
c向 system heap要求空间。
c考虑多线程( multi-threads)状态。
c考虑内存不足时的应变措施。
c考虑过多「小型区块」可能造成的内存破碎( fragment)问题。
为了将问题复杂度控制在一定范围,都排除多线程。
C++的内存 配 置 基 本 动 作 是::operator new(),内存释放基本动作是::operator delete()。这两个全域函式相当于 C 的 malloc()和 free() 函式。是的,正是如此, SGI 正是以malloc() 和free() 完成内存的配置与释放。
考虑小型区块所可能造成的内存破碎问题, SGI 设计了双层级配置器,第一级配置器直接使用 malloc()和free(),第二级配置器则视情况采用不同的策略:当配置区块超过128bytes,视之为「足够大」, 便呼叫第一级配置器;当配置区块小于 128bytes,视之为「过小」,为了降低额外负担(overhead,见 2.2.6 节),便采用复杂的memory pool整理方式,而不再求助于第一级配置器。【整个设计究竟只开放第一级配置器,或是同时开放第二级配置器,取决于__USE_MALLOC6是否被定义】(我们可以轻易测试出来)
2.2.5第一级配置器 __malloc_alloc_template 剖析
第一级配置器以malloc(), free(), realloc()等 C函式执行实际的内存配置、释放、重配置动作,并实作出类似 C++ new-handler7的机制。是的,它不能直接运用 C++ new-handler机制,因为它并非使用::operatornew来配置记忆体。
C++ new handler 机制是,【你可以要求系统在内存配置需求无法被满足时,唤起一个你所指定的函式。换句话说一旦::operator new无法达成任务,在丢 出std::bad_alloc异常状态之前,会先呼叫由客端指定的处理例程。此处理例程通常即被称为 new-handler。new-handler 解决内存不足的作法有特定的模式.】
SGI 第一级配置器的allocate() 和realloc()都是在呼叫malloc()和realloc()不成功后,改呼叫oom_malloc()和oom_realloc()。后两者都有内循环,不断呼叫「内存不足处理例程」, 期望在某次呼叫之后,获得足够的内存而圆满达成任务。但如果「内存不足处理例程」并不被客端设定,oom_malloc()和oom_realloc() 便老实不客气地呼叫__THROW_BAD_ALLOC,丢出bad_alloc异常讯息,或利用exit(1)硬生生中止程序。
2.2.6第二级配置器 __default_alloc_template 剖析
第二级配置器多了一些机制,避免太多小额区块造成内存的破碎。小额区块带来的其实不仅是内存破碎而已,配置时的额外负担( overhead)也是一大问题8。额外负担永远无法避免,毕竟系统要靠这多出来的空间来管理内存,如图2-3。但是区块愈小,额外负担所占的比例就愈大、愈显得浪费。
SGI第二级配置器的作法是,如果区块够大,超过 128 bytes,就移交第一级配置器处理。当区块小于 128 bytes,则以内存池( memory pool)管理,此法又称为次层配置( sub-allocation): 每次配置一大块内存,并维护对应之自由链表( freelist)。 下次若再有相同大小的内存需求,就直接从free-lists中拨出。如果客端释还小额区块,就由配置器回收到free-lists中—是的,别忘了,配置器除了负责配置,也负责回收。为了方便管理, SGI第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数(例如客端要求 30 bytes,就自动调整为 32bytes), 并维护 16 个 free-lists,各自管理大小分别为 8, 16, 24, 32, 40, 48, 56, 64, 72,80, 88, 96, 104, 112, 120, 128 bytes的小额区块。 free-lists 的节点结构如下:
unionobj {
union obj * free_list_link;
char client_data[1]; /* The client sees this. */
};
上述obj所用的是union,由于union之故,从其第一字段观之, obj可被视为一个指标,指向相同形式的另一个obj。从其第二字段观之, obj可被视为一个指标,指向实际区块,如下图。 一物二用的结果是,不会为了维护串行所必须的指针而造成内存的另一种浪费。
2.2.7空间配置函数 allocate()
身 为 一 个 配 置 器 , __default_alloc_template 拥 有 配 置 器 的 标 准 介 面 函 式allocate()。此函式首先判断区块大小,大于 128 bytes 就呼叫第一级配置器,小于 128 bytes 就检查对应的 free list。如果free list之内有可用的区块,就直接拿来用,如果没有可用区块,就将区块大小上调至 8 倍数边界,然后呼叫refill(),
准备为 free list 重新填充空间。
2.2.8空间释还函式 deallocate()
身 为 一 个 配 置 器 , __default_alloc_template 拥 有 配 置 器 标 准 介 面 函 式deallocate()。此函式首先判断区块大小,大于 128 bytes 就呼叫第一级配置器,
小于 128 bytes 就找出对应的 free list,将区块回收。
2.2.9重新充填 free lists
回头讨论先前说过的 allocate()。当它发现free list中没有可用区块了,就呼叫refill() 准 备 为free list重 新 填 充 空 间 。 新 的 空 间 将 取 自 记 忆 池 ( 经 由chunk_alloc()完成)。 预设取得20个新节点(新区块), 但万一记忆池空间
不足,获得的节点数(区块数)可能小于 20:
2.2.10记忆池(memory pool)
从记忆池中取空间给free list使用,是 chunk_alloc()的工作: chunk_alloc()函式以end_free - start_free 来判断记忆池的水量。如果水量充足,就直接拨出 20 个区块传回给 free list。如果水量不足以提供 20 个区块,但还足够供应一个以上的区块,就拨出这不足20个区块的空间出去。这时候其pass by reference 的 nobjs 参数将被修改为实际能够供应的区块数。如果记忆池连一个区块空间都无法供应,对客端显然无法交待,此时便需利用malloc()从 heap 中配置内存,为记忆池注入活水源头以应付需求。新水量的大小为需求量的两倍,再加上一个随着配置次数增加而愈来愈大的附加量。 万一山穷水尽,整个system heap 空间都不够了(以至无法为记忆池注入活水源头), malloc()行动失败, chunk_alloc()就四 处寻找有无「尚有无用区块,且区块够大」之其他free lists。找到的话就挖一块交出,找不到的话就呼叫第一级配置器。第一级配置器其实也是使用malloc()来配置内存,但它有 out-of-memory处理机制(类似 new-handler 机制), 或许有机会释放其它的内存拿来此处使用。如果可以,就成功,否则发出bad_alloc异常。
2.3内存基本处理工具
STL定义有五个全域函式,作用于未初始化空间上。这样的功能对于容器的实作很有帮助。前两个函式是 2.2.3节说过,用于建构 的construct()和用于解构的destroy(),另三个函式是uninitialized_copy(),uninitialized_fill(),uninitialized_fill_n()10,分别对应于高阶函式copy()、 fill()、 fill_n()— 这些都是 STL 算法。
2.3.1 uninitialized_copy
template <class InputIterator, class ForwardIterator>
ForwardIterator
uninitialized_copy(InputIterator first, InputIterator last,
ForwardIterator result);
uninitialized_copy() 使我们能够将内存的配置与对象的建构行为分离开来。如果做为输出目的地的[result, result+(last-first))范围内的每一个迭 代 器 都 指 向 未初 始 化 区 域 , 则uninitialized_copy()会 使 用copyconstructor,为身为输入来源之[first,last)范围内的每一个对象产生一份复制品,放进输出范围中。换句话说,针对输入范围内的每一个迭代器 i,此函式会呼叫construct(&*(result+(i-first)),*i) ,产生*i的复制品,放置于输出范围的相对位置上。
如果你有需要实作一个容器, uninitialized_copy()这样的函式会为你带来很大的帮助,因为容器的全区间构造函数 (range constructor)通常以两个步骤完成:
c配置内存区块,足以包含范围内的所有元素。
c使用uninitialized_copy(),在该内存区块上建构元素。
2.3.2 uninitialized_fill
template <class ForwardIterator, class T>
void uninitialized_fill(ForwardIterator first, ForwardIterator last,
const T& x);
uninitialized_fill() 也能够使我们将内存配置与对象的建构行为分离开来。如果[first,last)范围内的每个迭代器都指向未初始化的内存,那么uninitialized_fill()会在该范围内产生x( 上式第三参数)的复制品。换句话 说uninitialized_fill()会 针 对 操 作 范 围 内 的 每 个 迭 代 器 i , 呼 叫construct(&*i, x),在i所指之处产生x的复制品。
和 uninitialized_copy()一样, uninitialized_fill() 必须具备 "commit or
rollback"语意,换句话说它要不就产生出所有必要元素,要不就不产生任何元素。
如果有任何一个copy constructor丢出异常( exception), uninitialized_fill()
必须能够将已产生之所有元素解构掉。
2.3.3 uninitialized_fill_n
template <class ForwardIterator, class Size, class T>
ForwardIterator
uninitialized_fill_n(ForwardIterator first, Size n, const T& x);
uninitialized_fill_n()能够使我们将内存配置与对象建构行为分离开来。
它会为指定范围内的n个元素设定相同的初值。
如果[first, first+n)范围内的每一个迭代器都指向初始化的内存,那么
uninitialized_fill_n()会呼叫copy constructor,在该范围内产生x( 上式
第三参数)的复制品。也就是说面对 [first,first+n)范围内的每个迭代器 i,
uninitialized_fill_n()会呼叫construct(&*i, x),在对应位置处产生x 的
复制品。
总结:
考虑小型区块所可能造成的内存破碎问题, SGI 设计了双层级配置器,第一级配置器直接使用 malloc()和free(),第二级配置器则视情况采用不同的策略:当配置区块超过128bytes,视之为「足够大」, 便呼叫第一级配置器;当配置区块小于 128bytes,视之为「过小」,为了降低额外负担(overhead,见 2.2.6 节),便采用复杂的memory pool管理方式,而不再求助于第一级配置器。
第一级空间配置器:
SGI 第一级配置器的allocate() 和realloc()都是在呼叫malloc()和realloc()不成功后,改呼叫oom_malloc()和oom_realloc()。后两者都有内循环,不断呼叫「内存不足处理例程」, 期望在某次呼叫之后,获得足够的内存而圆满达成任务。但如果「内存不足处理例程」并不被客端设定,oom_malloc()和oom_realloc() 便老实不客气地呼叫__THROW_BAD_ALLOC,丢出bad_alloc异常讯息,或利用exit(1)硬生生中止程序。
第二级空间配置器:
_default_alloc_template 拥 有 配 置 器 的 标 准 介 面 函 式allocate()。此函式首先判断区块大小,大于 128 bytes 就呼叫第一级配置器,小于 128 bytes 就检查对应的 free list。如果free list之内有可用的区块,就直接拿来用,如果没有可用区块,就将区块大小上调至 8 倍数边界,然后呼叫refill(),准备为 free list 重新填充空间。
新 的 空 间 将 取 自 记 忆 池 ( 经 由chunk_alloc()完成)。 预设取得20个新节点(新区块), 但万一记忆池空间不足,获得的节点数(区块数)可能小于 20:
如果水量充足,就直接拨出 20 个区块传回给 free list。如果水量不足以提供 20 个区块,但还足够供应一个以上的区块,就拨出这不足20个区块的空间出去。如果记忆池连一个区块空间都无法供应,对客端显然无法交待,此时便需利用malloc()从 heap 中配置内存,为记忆池注入活水源头以应付需求。新水量的大小为需求量的两倍,再加上一个随着配置次数增加而愈来愈大的附加量。 万一山穷水尽,整个system heap 空间都不够了(以至无法为记忆池注入活水源头), malloc()行动失败, chunk_alloc()就四 处寻找有无「尚有无用区块,且区块够大」之其他free lists。找到的话就挖一块交出,找不到的话就呼叫第一级配置器。第一级配置器其实也是使用malloc()来配置内存,但它有 out-of-memory处理机制(类似 new-handler 机制), 或许有机会释放其它的内存拿来此处使用。如果可以,就成功,否则发出bad_alloc异常。
身 为 一 个 配 置 器 , __default_alloc_template 拥 有 配 置 器 标 准 介 面 函 式deallocate()。此函式首先判断区块大小,大于 128 bytes 就呼叫第一级配置器,
小于 128 bytes 就找出对应的 free list,将区块回收。