stl源码剖析_STL源码剖析 阅读笔记(二)allocator

一、空间分配器 allocator

使用上看,空间分配在任何语言的任何组件都不需要我们去过多关心,因为语言、组件的底层肯定都比较完整的做了这件事情。

实现上看,学习 allocator 的原理在源码学习中是首当其冲。因为没有空间分配,则无从谈起对象创建。这里说是空间分配,而不是内存分配,是因为也可以在内存之外的地方(如硬盘)分配空间。

分配器主要作用就是分配空间,根据规范,其需要实现一些接口,完成一些关于空间分配的功能。标准接口规范见附录(一)。

本文会提到以下几个方面:

  • SGI STL 分配器介绍
  • construct 和 destroy
    • destroy 接收指针和迭代器的方法
  • alloc 分配器
    • 一层分配器
    • 二层分配器 和 free-list
      • 分配
      • 释放
      • 补充
  • 全局函数
    • uninitialized_copy
    • uninitialized_fill
    • uninitialized_fill_n

二、SGI STL 的 alloc

SGI STL 的分配器与众不同,也与标准规范不同,其名称是 alloc 而非 allocator,而且不接受任何参数。具体来说,想在程序中明确使用 SGI 分配器,不能写std::allocator<int>,而要写成std::alloc

即使它不符合标准规范,也不会对我们使用造成任何影响,因为通常我们都使用缺省的分配器,而不会自己指定。而 STL 的每一个容器都指定缺省分配器为 alloc。

当然了,SGI 也定义了符合部分标准、名为 allocator 的分配器,但出于其效率原因,STL 从未使用它,也不推荐程序员使用。它只是把 ::operator new 和 ::operator delete 做了一层薄薄的封装,没有做优化。详细代码见附录(二)。

下面详细聊聊 SGI STL 实现的 alloc 。

一般而言,C++的内存分配和释放操作如下:

class Foo {...};
Foo * pf = new Foo;    // 分配内存,构造对象
delete pf;             // 析构对象,释放内存
  • new 内含两步操作:(1)调用 ::operator new 分配内存(2)调用 Foo::Foo() 构造对象内容。
  • delete 内含两步操作:(1)调用 Foo::~Foo() 析构对象(2)调用 ::operator delete 释放内存。

为了精细分工,分配器将这两个步骤分开做。内存分配由 alloc::allocate() 负责,内存释放由 alloc::deallocate() 负责;对象构造由 ::construct() 负责,对象析构由 ::destroy() 负责。

// STL规定分配器 allocator 定义于 memory 中
#include <memory>

// memory 中含有两个文件
#include <stl_alloc.h>          // 负责内存空间的分配和释放,定义了 一级、二级分配器。
#include <stl_construct.h>      // 负责对象内容的构造和析构,有 construct 和 destroy 方法。

// memory 中还有一个文件
#include <stl_uninitialized.h>  // 定义了一些全局函数,用来填充fill 或者 复制copy 大块内存的数据
// 其中有如下方法
// un_initialized_copy()
// un_initialized_fill()
// un_initialized_fill_n()
// 这些方法不属于分配器的范畴,但与对象初值设置有关,对大规模元素初值设置很有帮助。
// 在效率上,最差会调用 construct,最佳会调用C的 memmove 进行内存移动。

(一)construct 和 destroy

对于对象的 construct 和 destroy 可以概括如下图所示。其源码见附录(三)

c7965434b7922b13d3f6863c263b4952.png
  • construct 接收 指针p 和 初值value,会将value设置到p所指的空间上。
  • destroy 可以接收 指针、迭代器。
    • 基本类型指针:不做处理
    • 对象类型指针:调用析构函数
    • 迭代器:会判断析构函数是否为 trivial destructor(无用的、没必要的、无意义的析构函数)
      • 是:则不做处理
      • 否:调用 迭代器中每个元素的析构函数。

这里有个问题是,如何判断是否为 trivial呢?

答案是:使用 __type_traits<T>::has_trivial_destructor() ,该函数会返回 __true_type 或 __false_type,前者代表是trivial,后者代表是有意义的。

该类的具体实现需要去研究下 traits,这里先不展开。

(二)STL alloc

<stl_alloc.h> 负责了对象构造前的空间分配和对象析构前的空间释放,有下面几个设计原则:

  • 向 system heap 申请空间
  • 考虑多线程状态(为了将问题简化,这里不讨论多线程状态)
  • 考虑内存不足的应变措施
  • 考虑过多小块内存造成的碎片问题

C++ 的内存分配和释放主要使用 ::operator new() 和 ::operator delete(),这两个相当于C的 malloc() 和 free(),SGI 正是以 malloc 和 free 完成的内存分配和释放。

SGI 设计了双层分配器,如下图所示:

  • 第一级直接使用 malloc 和 free
  • 第二级,当需求大于128 bytes 时,调用一级分配器;小于等于128 bytes 则调用二级分配器。

71899b64a470f818ba52a3107086faef.png

具体采用哪种分配器,需要看 __USE_MALLOC 是否被定义。定义了则用一级分配器,否则调用二级分配器。

SGI 为 alloc 提供了一个 simple_alloc 的接口封装,使得外层使用时无需考虑内部具体用的一级还是二级。SGI STL 的容器都使用这个 simple_alloc 接口,而非直接使用 alloc。代码见附录(四)。

c266badca469f9d203401b766b32b490.png

一级分配器的原理比较简单,正常情况就是调用 malloc 和 free 做分配和释放。当内存不够时需要使用 oom_malloc,在该函数中,会循环调用一个 handler 来处理内存不足的情况。这个 handler 是需要自己指定的,如果没有指定,则抛出 std::bad_alloc 异常。这个 handler 一般称为 new-handler,在 《Effective C++》2e item7 中有特定的解决模式。

(三)STL 二级分配器

下面着重说说二级分配器

二级分配器可以避免产生过多的小区块,可以解决内存碎片和过多的额外开销(系统需要多出来的空间管理内存,可以说是给系统“交税”)。

二级分配器以内存池(memory pool)管理小于128 bytes 的内存,称为次层分配(sub-allocation):先分配一大块内存,组成一个自由链表(free-list),每次要取一定量内存时,从 free-list 中取;在用完后,分配器就归还给 free-list。

分配器会维护 空间为 8、16、24、……、128 这16个 free-list,在分配小内存时,会向上取整(Round Up),寻找最近的 free-list。

free-list 节点结构是一个联合体,该节点在free-list中时,内容是一个指向 下一个节点的指针,在客户端使用时,是具体的数据。这样一物二用,不会造成维护链表指针的内存浪费。这个技巧在强类型语言(Strong Typed)中如 Java 行不通,但在弱类型语言(Weak Typed)中如 C++十分常见。

union obj{
    union obj * free_list_link;  
    char client_data[1];         // client use
}

854f117a8d29d715e054706e633ee5b7.png
free-list 的实现技巧

次层分配中从 free list 分出内存的步骤 allocate 如下图所示:

e0e9850240c93bf637f3d6ae62f55943.png

次层分配中释放内存,往 free list 中归还的步骤 deallocate 如下图所示:

aa1428915638b71c97e92680a2cd766d.png

当 free-list 的空间用尽后,会触发 refill 操作,重新给 free-list 补充 20个节点。refill 会调用 chunk_alloc,该函数中会做具体从内存池中取内存的操作。其过程如下所示。

bee99984612e7cb4c62d22e65806749a.png

ca394099fd3c8dc0ab95ec6800e0e88c.png

简而言之就是,先找自己(32找32),再找亲友(64找32),实在不行就求助大家(96找32)。

三、内存基本处理工具

STL 定义了五个全局函数,除了前文提到的 construct 和 destroy,还有3个用来处理大块内存的复制和移动的 unitialized_copy、uninitialized_fill、uninitialized_fill_n 分别对应高层次的函数 copy、fill、fill_n。

unitialized_copy 函数让内存配置与对象构造行为分开。如果目标地址指向的空间都是未初始化区域,则会直接把源区域的对象产生复制品直接放到目标地址。STL 规范中要求该函数具有原子性,要么全部构造出来,要么全部不构造。

uninitialized_fill、uninitialized_fill_n 也和 unitialized_copy 类似。

这三个函数都会判断 对象是否为 POD(Plain Old Data,标量 or 传统 C 结构体),POD 会具有 trivial 函数,如果是 POD 则用最有效率的方法,如果非 POD 则用最安全的方法。过程大致如下所示。

414b754cec9f1ebfe379c685bb6da155.png

附录

(一)标准接口规范

根据 STL 规范,allocator 必须要实现以下接口。

44b1b92886d247befdf6e4146ff27a5a.png

7ce3e770e343a8107171de0096c7df0e.png

(二)SGI allocator 源码

下面是 SGI 实现的 allocator 全貌

1297eaddcb57129121adca50e32917af.png

a3200ef8ae651c1d17f42ff61719d33a.png

e4d6bf1cc2df554e70675b24b1f55929.png

(三)construct 和 destroy 源码

188b479b4989f933034bca40e113f9a1.png

7019d5aa232f19369cef04a60e8701ad.png

(四)simple_alloc 和 vector

1bb90c64abf4fdb2f28a1f1d76d615b6.png

f8c8b7b4a7fdeea1498f154d9da9e879.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值