在了解了分配器的进化过程之后,我们就进入了这个进化过程的终极一步:全局分配器。
全局分配器就是想通过“大块切小块”的管理方式,来减少malloc的次数从而减少cookie
全局分配器为了具有更好地适配性,通过管理16条链表来管理不同大小地内存分配。
这就是我们接下来讨论的重点。
不同编译器下的allocator实现
首先,我们来看看常见的几个编译器中,内存分配的实现。
VC6
VC6中的malloc在申请空间时的布局如下:
可以看到增加了cookie,debug header等内容,这也就是前面所说的在堆上分配内存时的情况。
VC6中也有标准分配器std::allocator,但看到源码后就会发现,其本质是调用了全局的operator_new()和operator_delete(),也就是调用了malloc和free,没有特殊设计。
BC5
BC5这个版本也依旧是使用了全局的operator_new()和operator_delete(),没有特殊的设计。
GNU2.9
在GNU2.9的标准库中,std::allocator的实现也与上述一致,没有特殊设计。
但设计者有明确指出,虽然按照惯例实现了std::allocator,但真正使用的并不是它,而是另一个std::alloc
这里G2.9实现的alloc就是我们要说的特殊设计。
注:GNU2.9的alloc在GNU4.9中变为了_pool_alloc,功能一致。
alloc是如何运行的
接下来我们就来剖析alloc的特殊设计,也就是16条链表的结构。
alloc管理着16条链表,每个链表都各司其职,负责8字节到128字节的空间管理。
我们根据场景的变化,来看看alloc到底做了什么。
战备池与嵌入式指针
首先我们需要知道,每次在调用malloc时,其实就相当于挖一块大内存先分配给空闲空间(战备池,也叫内存池),再进行具体的分配操作。
嵌入式指针这部分和我们说的联合体结构一样,把一块空间的前四个字节当作指针。当给客户端时,就是一块完整的数据块,而当归alloc管理时,就会使用指针将其串连,一举两得。
空间的申请与指针管理
开始时链表处于空的状态。
此时申请32bytes的空间,由于现在没有空闲空间,所以直接挖一块32bytes*20*2的空间作为空闲内存池。
注意这里是2倍,因为一半要给链表管理,一半放入内存池
将这块空间调整到16的倍数,为1280
此时把第一块给客户端,剩下的19*32bytes交给3号链表管理(32/16=2 2+1=3)。那么剩下的640就可以放入内存池了。
注意3号链表管理的这块空间在开头是有cookie的(蓝色部分)
继续申请64bytes的空间,本应该是7号链表管理的,而现在上面没有内存,再去看内存空闲池发现有地方。
内存池的640可以分成10个64bytes,把第一个给客户端,后面9个由7号链表管理(注意图中的链表位置)
继续申请96bytes的空间,这时11号链表上没有管理空间,而且内存池没有空闲,所以申请一大块空间。
这块空间是96bytes*20*2这么大,调整到16的倍数就是3920
把前面第一块给客户端,后面19块交给11号链表管理。
最后将剩下的2000bytes交给内存池。
注意这里的空间也是后来申请的,所以开头有cookie(红色)
再申请88bytes的空间,此时内存池有2000的存量,可以分出88*20的空间,剩余240。88由第10号链表管理。
再申请8bytes的空间,内存池空闲240,可以分出20*8的空间,剩余80,8由0号链表管理。
这时再申请104bytes的空间,查找12号链表发现无内容,内存池也不够分配,于是准备继续申请内存。
注意在申请前先将80的余量分配给了9号链表。
申请104*20*2的大小,第一块给客户端,其他的19块交给11号链表管理,剩余2408
申请112的空间,查找13号链表无内容,从内存池分配112*20,剩余168
由链表13管理。
再申请48bytes,内存池有168余量,可以分出来3块。其中一块给客户端,剩余两块由5号链表管理。附带24的空闲空间。
再申请72bytes的空间,查找8号无可用,剩余24不足72,于是先把24交给2号链表管理。
然后想要申请。目前已申请了9688的内存,我们假设系统堆的大小为10000,剩下的就无法满足之前的索取(72*20*2)
我们发现与之最近的链表是9号,可以满足要求。
所以把9号的80给8号,拆出72还给客户端,剩余8。
这时再申请72bytes,余量不够,先将剩余的8交给0号链表。
想要申请但是依旧失败,于是找到最近且可以满足的10号链表(88)
分出72给客户端,剩余16空闲。(注意这里的分出和剩余管理只是链表在变动,不会发生结构变化)
再申请120bytes,先将剩余的16给链表1号管理。但无法索取,因为此时15号链表无内容可用,走到了末路。
一些值得讨论的点
当要求的内存大于128bytes使就会使用一级分配器。小于128bytes才会进入free-lists管理的二级分配器。
这里链表和链表指针的定义为
obj** my_free_list,*p
这是容易让人迷惑的,其实第二个是obj* p
而且我们发现,在整个过程中,只有malloc,而没有对应的free。
在deallocate中也没有调用free,只是把所有区块收集了起来,虽然算不上内存泄漏,但也不是一种非常好的设计。
当然,这种设计对比malloc来说,时间消耗要少很多。
但malloc究竟是不是真的慢,又或者说设计是否真的不够精妙,我们下回分解。
|