2-空间配置器(allocator)

说在前面

以 STL 运用的角度,空间配置器是最不需要介绍的东西,它总是隐藏在一切组件(更具体地说是指容器)的背后。但是若以 STL 的实现角度而言,第一个要介绍的就是它。因为整个 STL 操作对象都存在在容器内,而容器一定需要配置空间。

为什么不说 allocator 是内存配置器而说它是空间配置器呢?因为空间不一定是内存,空间也可以是磁盘或者其它辅助存储介质。是的,你可以写一个 allocator,直接向硬盘取空间。以下要介绍的 SGI STL 提供的配置器,配置的对象,呃,是的,是内存。


一、OOP vs GP

  • OOP(Object-Oriented programming):面向对象编程,就是试图将 datas 和 methods 关联在一起。
  • GP(Generic Programming):泛型编程,试图将 datas 和 methods 分离。

可以将容器看作 datas ,将 Algorithms 作为 methods ,两者需要通过迭代器来进行联系。Algorithms 通过 Iterators 确定操作范围,并通过 Iterators 取用 Containers 元素。
在这里插入图片描述

这里,我想提出一个问题,为什么 list 内部自带 sort 方法?而不能使用 algorithm 提供的 sort 算法来进行排序?

这里我们搬出 algorithm 里面 sort 方法的部分源代码。
在这里插入图片描述

在上面代码中被标红的代码可以看出,迭代器在这里进行了加减除操作,能够符合这种运算的迭代器只能够是随机访问迭代器,也就是上一排所定义的 RandomAccessIterator。而这里的迭代器可以把它当作一个泛化指针,对于一段连续存储的空间,比如 vector,deque,指针+n就表示跳到第几个元素,而 list 它是一个双向链表,它在内存中不一定是连续的,所以只能使用 ++ 或者 --,而不能使用 +n 或者 -n。所以不能使用 algorithm 提供的 sort 方法,而是 list 内部自己提供的 sort 方法。


二、分配器 allocators

说到分配器,得先谈一谈 operator new() 和 malloc()。

或许你没有听过 operator new(),只听过 new()。其实 new() 底层是调用 operator new() 来实现的。而operator new() 其实还是调用 malloc()。下面展示的是 vc 下的 operator new() 的源码,可以很清楚的看到它其实也是调用 malloc() 进行内存分配的。

在这里插入图片描述
而 malloc() 分配的内存是什么样子的呢?看下图。
在这里插入图片描述
你所需要分配的内存大小为 size。但是从上图中,我们明显可以看到 malloc 出来的内存远比你申请的内存要大。是因为 malloc 会产生额外的开销,这里你姑且记住这个特点。

说完了 operator new() 和 malloc(),接下来就是对 allocators 的介绍了。下面列举了 VC6 对 allocator 的使用。
在这里插入图片描述
可以看出,它们默认使用的分配器都是 allocator,下面列出了三种版本的 allocator 的实现。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可以看出,他们内部其实都是使用 operator new() 和 operator delete(),也就是使用 malloc() 和 free() 来分配和释放内存,因此会带来大量的额外的开销。而我们真正关心的是这些额外开销所占的比例,而不是这些开销的大小。如果你申请的区块小,开销的比例就大,我们是不能让忍受的。如果你的区块大,开销所占的比例就小,就可以接受。但是在实际分配的时候,这个区块到底是大是小,通常小,那就开销比较大。这是不太理想的。

我们回过头去看 G2.9 中的 alloctor 源码图片有一段话如下:
在这里插入图片描述
意思就是说:不要使用这个版本的 allocator。SGI STL 提供了另一种版本的 allocator,它虽然定义了这样一种符合标准的 allocator,但是从来没有使用过,而是使用的另一个版本的。

下面让我们来看看另一个版本的 allocator,即 G2.9 对 allocator 的使用。我们可以从下图知道,G2.9 使用的不再是 allocator,而是 alloc (<stl_alloc.h>)。
在这里插入图片描述

在上面,我们提到过,allocator 最终的都是调用 malloc() 来分配大小。所以 alloc 最主要的作用就是要尽量减少 malloc() 的次数,因为 malloc() 的次数越多所带来的额外开销就越大,会形成更多的内存碎片。

为什么 malloc() 会带来额外的开销呢?而这些额外开销到底用来做什么?让我们再次拿出 malloc() 的内存图。
在这里插入图片描述
除了图中蓝色部分,其它都是额外开销。其中最重要的就是上下两个红色部分。我们习惯叫作 cookie(上下都有,各占 4 个字节,共 8 字节)。cookie 记录整块的大小,这是必要的。因为我们在 malloc 的时候,会得到一个指针,free 的时候,只传入了这个指针,而不需要告诉它的大小,这个大小就是记录在 cookie 里面的,所以 free 才知道它到底要回收多大的内存。

但是对于容器来说,容器内部各个元素的大小是相同的,没有必要单独用一块空间来记录每个元素的大小。而 G2.9 的另一个版本的 alloc 就是从这个地方着手,它要尽量减少 malloc 的次数。

那是怎么实现的呢?

对象构造前的空间配置和对象析构的空间释放,由 <stl_alloc.h> 负责,SGI 对此的设计哲学如下:

  • 向 system heap 要求空间
  • 考虑多线程(multi-threads)状态
  • 考虑内存不足时的应对措施
  • 考虑过多 ”小型区块“ 可能造成的内存碎片(fragment)问题。

为了将问题控制在一定的复杂度内,以下讨论的就是排除了多线程状态的处理。

C++ 的内存配置操作是:::operator new(),内存释放基本操作是 ::operator delete() 。这两个全局函数相当于 C 的 malloc() 和 free() 函数。正是如此,SGI 正式以 malloc() 和 free() 完成内存的配置与释放。

考虑到小型区块所可能造成的内存碎片的问题,SGI 设计了双层级配置器,第一级配置器使用 malloc() 和 free() 函数,第二级配置器则视情况采用不同的策略:当配置区块超过 128 bytes 时,视之为 ”足够大“,便调用第一级配置器;当配置区块小于 128 bytes 时,则视之为 ”过小“,为了降低额外负担,便采用复杂的 memory pool(内存池)整理方式,而不再求助于第一级配置器。默认使用第二级配置器。

下面是第二级配置器具体的实现方式。

它设计了 16 条链表,每条链表负责某一种特定大小的区块,用链表连接起来。第 0 条链表(#0)负责 8 bytes 的区块大小,第 1 条链表负责 16 bytes 的区块大小,第 2 条链表负责 24 bytes 的区块大小,依次递增 8 bytes,直到第 15 条链表为 128 bytes 的区块大小。所有的 STL 容器需要内存的时候,都向这个分配器申请内存。并且容器申请的大小会被调整至 8 的倍数,比如你申请的是 50 个字节,它会调整到 56 个字节。然后它就会到第 6 条链表(从 0 开始)中查询这条链表有没有悬挂相应内存块,如果有,则从链表取出一块空闲内存块直接取出分配给容器;如果没有,才会向操作系统去申请内存,也就是使用 malloc 申请一大块,然后将其切割成很多 以56 个字节为单位的内存块挂到第 6 条链表上,然后再从链表中取出分配给容器。
在这里插入图片描述
下面是第二级配置器的部分定义内容

enum {__ALIGN = 8};			// 小型区块的上调边界
enum {__MAX_BYTES};			// 小型区块的上限
enum{__NFREELISTS = __MAX_BYTES/__ALIGN};	// free-lists 个数

//free-lists 的节点结构:
union obj{
	union obj *free_list_link;	// 指向相同形式的另一个 obj
	char client_data[1];		// 指向实际区块
};

下面来看看一个具体的申请区块和回收区块的例子。区块自 free list 中取出,阅读次序请按照图中编号。

  • 申请一个 96 bytes 的区块
    在这里插入图片描述
  • 回收一个 96 bytes 的区块
    在这里插入图片描述

这样的 alloc 的好处是什么呢?就是省去了 cookie 的 8 字节的大小。想象一下,如果我们需要分配 100 万个元素的话,就省去了 100万 * 8 bytes 的开销,这可是不小的。

下面我们来具体看看一二级配置器的关系,接口包装,及实际的运用方式。

在这里插入图片描述

但是到了 G4.9 所附的标准库,它的 allocator 却发生了改变,并没有继续使用上述的 alloc,而是使用下图所示的 allocator,至于为什么要放弃使用上述的 alloc,原因尚不清楚。

下面是 G4.9 STL 对 allocator 的使用,从源代码可以看出,allocator 多了一个继承的父类 __allocator_base,结构变得更为复杂。但是个人还是比较偏向 G2.9 版的 alloc。
在这里插入图片描述
在这里插入图片描述
到了这里,我们还有一个疑惑,那就是 G2.9 的那个版本的 alloc 还存在吗?还在使用吗?

答案是,还在~~

G4.9 所附的标准库,有很多 extention allocators。其中 __pool_alloc(线程池) 就是使用的 G2.9 的 alloc。

在这里插入图片描述
可以这样使用:vector<string, __gun_cxx::__pool_alloc<string>> vec;

allocator 姑姐先暂时说到这里~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值