从轮子造起——SGI-STL空间配置器

引言

人们常说,不要从轮子重新造起,要站在巨人的肩膀上。面对扮演轮子角色的这些STL组件,我们是否有必要深究其设计原理或实现细节呢?答案因人而异。从应用的角度思考,你不需要探索实现细节(然而相当程度地认识底层实现,对实务运用有绝对的帮助)。从技术研究与本质提升的角度来看,深究细节可以让你彻底掌握一切:不是为了重温数据结构和算法,或是想要扮演轮子角色,或是想要进一步扩张别人的轮子,都可因此获得深厚扎实的基础。

天下大事,必作于细。

                                                                                                                                                                                   —— 摘自《STL源码剖析》 侯捷

看了侯捷大师的源码剖析一书,深感STL的博大精深。对空间配置器的设计思想更是觉得震撼。我们进步最快的途径,是要先模仿。所以决定分析透彻STL空间配置器的原理,并实现之。

设计哲学

我选的是SGI版本的STL,以后也都直接叫做STL,不加以区分了。这个版本的设计哲学是:

  1. 向system heap申请空间
  2. 考虑多线程状态(我只关注空间配置原理,不关心这个)
  3. 考虑内存不足时的应变措施
  4. 考虑过多“小型区块”可能造成的内存碎片问题(这里是外碎片,STL引入了内碎片问题,后面说)

外碎片

首先,从外碎片引入STL:在堆上频繁的申请、释放小型区块,会造成外碎片问题。那么什么时外碎片呢?

假设有60B的内存,首先申请20B,再申请16B,再申请20B,最后释放16B的空间。如图:

         

现在16B已经释放,总共剩余20B的空间,但是现在再想要分配17B的空间却分配不出来了,因为不连续。这种明明有,却因为不连续而无法分配出内存的情况就叫做外碎片。

一级空间配置器

外碎片是由于频繁申请与释放小块内存导致的,大块内存的申请就不会有这种问题了,所以对于大块内存的申请,STL调用一级空间配置器来分配。

STL认为大于128字节的内存块是大块的。一级空间配置器其实不难,它只是简单地封装了malloc与free,并且模拟实现了C++ operator new 中的set_new_handler(具体的介绍可以查看《effective C++》一书)。什么是set_new_handdler?就是内部维护的一个函数指针,当malloc申请内存失败时,并不想直接返回,而是想尝试去调用一下其它办法,看是否有什么方法来释放出一部分内存,好让malloc成功,这个方法通过这个handler指针来设置,初始时为NULL。所以一级空间配置器做的就是当malloc失败,且handler函数不为NULL时,循环调用这个函数,然后malloc,直至成功才返回申请到的内存首地址。如果handler函数为NULL,malloc又失败,那么会抛一个Bad_Alloc异常。

不过一般没人用罢了,因为也没有什么有效的释放内存的方法。

二级空间配置器

它维护了一块内存池和一个自由链表:

         

初始时,内存池的内存均通过malloc获得, 是一块连续的大块内存,并有_start_free标识开头,_end_free标识最后一个内存单元的下一个。

自由链表初始时是空的,它由左到右依次为8,16,24,...,128,底下挂的是对应大小的内存块。如果申请的是1~8个字节大小的内存,那么统一分配8个字节大小。比如,申请4个字节的大小,会先向上取整到8的最小倍数,然后取出内存块返回(所分配的内存块都是8的整数倍)。这里就有4个字节被浪费了,这叫做内碎片。

前面说了,大于128个字节的找一级空间配置器分配,小于等于128字节的就先找的自由链表。设申请的内存大小为size,如果size大于128,则找一级空间配置器分配,如果size小于等于128,又分以下几种情况:

找到size大小在自由链表中对应的下标,查看这个位置小面是否挂有内存,如果有,则头删,取出第一块返回;如果当前链表下没有挂内存块,那么调用refill函数。

refill函数做的是填充自由链表,首先计算20倍的size的大小,分配20个所需对象的大小(为什么是20倍?因为如果刚刚申请了一个这么大小的内存块,可能很快又要申请同样大小的内存块,所以为了效率,依次申请20个对象的大小挂到链表上,以备不时之需),然后从内存池索取:

如果内存池剩余的内存足够分配20个对象的大小,那么取出这么大的一块内存,切为20块,第一块返回,剩余19块依次挂到自由链表对应位置下面;

如果内存池剩余的内存不够分配20个对象的大小,但至少有一个,那么能申请多少申请多少个,第一个返回,剩余的挂到自由链表对应位置下面;

如果内存池剩余的内存连一个对象的大小都分配不出来,就调用malloc向操作系统要。但是调用malloc之前,要先将内存池剩余的内存挂到自由链表对应的位置上去。

找操作系统malloc,但是操作系统也不一定有内存,但是也不能轻易放弃,当前链表位置下面没有内存块,但是更高的地方可能有,于是向自由链表大的方向依次去找,如果有,则头删,取出第一块,作为新的内存池,然后内存池就有内存了,接下来的逻辑重复之前就好,但这次一定能从内存池拿到内存。

但是如果自由链表的后面也没有内存了,那可真就是山穷水尽了,但还是不能轻言放弃。别忘了,我们还有一级空间配置器!但是malloc已经申请不出内存了,找一级空间配置器又有什么用呢?这是因为一级空间配置器中有可能设置了handler函数,所以只能抓住这最后一根救命稻草,就算没有设置这个handler函数,大不了抛个异常走人。

源代码

源码托管到了GitHub上面,开源共享:https://github.com/Fireplusplus/Data-Structure/blob/master/my_allocator.h

测试

写了个函数用来测试,代码上边也附了,这里把测试结果贴出来:


  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fireplusplus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值