一、OOP vs GP

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

可以将容器看作 datas ,将 Algorithms 作为 methods ,两者需要通过迭代器来进行联系。Algorithms 通过 Iterators 确定操作范围,并通过 Iterators 取用 Containers 元素。
2-分配器

那这里引出了为什么 list 内部自带 sort 方法?而不能使用 algorithm 提供的 sort 算法来进行排序?

这里我们搬出 algorithm 里面 sort 方法的部分源代码。
2-分配器

在上面代码中被标红的代码可以看出,迭代器在这里进行了加减除操作,能够符合这种运算的迭代器只只能够是随机访问迭代器,也就是上一排所定义的 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() 进行内存分配的。

2-分配器
而 malloc() 分配的内存是什么样子的呢?看下图。
2-分配器
你所需要分配的内存大小为 size。这里你只需要知道 malloc 给你的远比你申请的大小要大。

说完了 operator new() 和 malloc(),接下来就是对 allocators 的介绍了。下面列举了 VC6 对 allocator 得分使用。
2-分配器
可以看出,它们默认使用的分配器都是 allocator
2-分配器
2-分配器
2-分配器

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

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

下面让我们来看看另一个版本的 allocator,即 G2.9 对 allocator 的使用。
2-分配器
G2.9 所附的标准库,其 alloc 实现如下(<stl_alloc.h>)
2-分配器

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

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

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

那是怎么实现的呢?

它设计了 16 条链表,每条链表负责某一种特定大小的区块,用链表连接起来。第 0 条链表负责 8 bytes 的区块大小,第 1 条链表负责 16 bytes 的区块大小,第 2 条链表负责 24 bytes 的区块大小,依次递增 8 bytes,直到第 15 条链表为 128 bytes 的区块大小。所有的容器需要内存的时候,都向这个分配器申请内存。容器的元素大小会被调整至 8 的倍数,比如你申请的是 50 个字节,它会调整到 56 个字节。然后它就会到第 6 条链表(从 0 开始)中查询这条链表有没有悬挂相应内存块,如果有,则从链表直接取出分配给容器;如果没有,才会想操作系统去申请内存,也就是使用 malloc 申请一大块,然后将其切割成很多 56 个字节的内存块挂到第 6 条链表上,然后再从链表中取出分配给容器。
2-分配器
这样的 alloc 的好处是什么呢?就是省去了 cookie 的 8 字节的大小。想象一下,如果我们需要分配 100 万个元素的话,就省去了 100万 * 8 bytes 的开销,这可是不小的。

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

G4.9 STL 对 allocator 的使用
2-分配器
2-分配器
到了这里,我们还有一个疑惑,那就是 G2.9 的那个版本的 alloc 还存在吗?还在使用吗?

答案是,还在~~

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

2-分配器
可以这样使用:vector<string, __gun_cxx::__pool_alloc<string>> vec;

相关文章:

  • 2022-12-23
  • 2022-12-23
  • 2021-08-21
  • 2021-11-30
  • 2022-02-03
  • 2022-12-23
猜你喜欢
  • 2021-05-28
  • 2022-12-23
  • 2021-07-28
  • 2021-11-28
  • 2021-06-13
  • 2021-11-22
  • 2022-01-31
相关资源
相似解决方案