Tcmalloc内存分配算法的分析

一、介绍

tcmalloc是Google搞的一个内存管理算法,据说分配速度要比glibc自带的malloc快很多。但是客观分析来说,只是在某些场景下是如此。tmalloc具有现代内存管理器的特点,可以减少碎片,支持多CPU的扩展等等。它的应用还是比较多的,比较有名就是Golang语言中就使用类似的算法。
另外,在比较复杂的多线程环境下,tmalloc也对相关的锁的粒度进行了细化,并且引入了自旋锁,可以更好的处理在多线程情况下的竞态。对于一些很小的内存对象,几乎不会涉及到竞态的情况。另外,tcmalloc对小对象的存储表示有着更节省空间的方式,相对pmalloc来说正如网上描述:
“N 8-byte objects can be allocated while using space approximately 8N * 1.01 bytes. I.e., a one-percent space overhead. ptmalloc2 uses a four-byte header for each object and (I think) rounds up the size to a multiple of 8 bytes and ends up using 16N bytes.”

二、算法说明

说到内存分配,其实就涉及到两个问题:
1、线程内的分配(串行分配)
2、多线程的分配(并行分配)
而具体到内存的分配控制,又可以分为大内存对象分配和小内存对象分配。当然,在一些具体的应用中,又可以分出巨型内存分配和微小内存分配,但是使用分治法的思路是没有改变的。
而从对象形式上来看,对象的分配又可分为定长度内存分配和不定长内存分配。这个在前面分配过,定长内存最适合的就是内存池;同样,一些变长内存分配,可以假设成逼近某种定长内存,这些定长内存可以形成一个队列或者类似的链表都可以。而在Go语言的内存分配中可以看到使用的是bitmap。目的当然很清楚,多快好省的建设社会主义。
同样,内存的逻辑管理分配分析OK了,就要说具体的应对措施是什么了。tcmalloc中也是使用span和具体的Page映射,形成一种数据结构,也就是PageHeap。span这个概念在go中分析过,不明白的回去翻翻,基本一样。有了底层的数据结构的支持,就可以考虑分配时如何管理这些Span。也就是对span的分裂和合并并把合适大小的内存分配给需要的对象。
而这个担子就交给了CentralCache,这个也熟悉吧,和Go其中的长得有点像,功能也很像。它通过链表把各种Span链接起来,在需要分配时,把span分配出去,如果没有了span就从PageHeap中申请新的。
但是在并发的情况下,如果仍然使用CentralCache,就无法保证效率或者说竞态会很激烈,这时就可以用 ThreadCache在内存上面再管理一层。这其实就类似于线程的局部存储。每一个线程有自己一个ThreadCache,维护着各种类别的Span的链表,它负责Span的分配,假如耗尽,就像全局的CentralCache申请一批,如果无法申请就去PageHeap申请,再无法申请就去OS,再无法申请就直接挂掉(实际可能不是这样,这里只是说的简单一些)。
在tcmlloc的内存分配过程中向系统申请内存分配,基本使用提mmap和sbkr来实现的。在malloc中128K是一个分界线,但tcmalloc没有查到这个具体的值,回头看源码吧。不过需要注意的是如果sbrk分配失败会继续调用mmap进行分配。

三、整体构成

通过上面的分析可以看出tcmalloc的内存管理结构的整体构成为成三层:
1、ThreadCache
其本身并不复杂就是一系列的以object为基础单元的FreeList链表管理,有就分配出去。重要的是它是线程私有的,效率会很高,不用担心锁的管理。

2、CentralCache
做为全局内存管理的部分,它仍然是管理以object为单元的FreeList,不过它做为内存管理部分,是通过span这个基本单元来控制object的。即CentralFreeList维护着一个个的span的链表,在每个span内又有切分的object的链表。这样就可以实现span的切合和合并。为了实现快速的内存管理,在其中还有一个Cache,用来暂存object。只有当Cache满时才会把相关的object挂到原来的相关span中去。
为了提高效率,FreeList分为两类,即空的和非空的,这样就不用进行判断就可以自动适配相关的条件。

3、PageHeap
此数据结构实现了page到span的映射关系以及空闲span的伙伴系统。看到伙伴系统,学过Linux系统的同学应该就明白怎么回事。这种映射关系的处理是通过radix tree这种数据结构来实现的,它是一种稀疏的叉树,和HASH比,空间更小,并且避免了冲突。在这里,可以通过PageHeapAllocator来处理和分配span、ThreadCache等。
这里面object的处理很有代表性,它没有一个专门的管理数据结构,在使用了,会全部提供给使用方;但在空闲时,会有一个8字节的长度用来存储指针,用来链到相关链表中去。
在tcmalloc的分配过程中,内存管理的基本单元是span,而在上层应用分配的基本单元是object。tcmalloc的分配顺序是从私有到公有到OS内存交互(PageHeap),搞清楚了这个顺序和基本的控制单元,那么在后面的源码分析中就会清楚操作的手段。

四、分配流程

在tcmalloc中将内存分成三类,即小对象,小于256K的,中型对象,介于256K到1M的,大于1M的为大对象。其具体的分配流程如下:
1、小对象内存
通过size映射到指定的class,先在ThreadCache的FreeList中分配,如果成功直接返回即可;否则,去CentralCache分配batch_size个object,其中一个返回,其它直接进入到ThreadCache相关的FreeList(ThreadCache.list_[class])中。在这个过程中会在前面的提到的空闲链表CentralFreeList.tc_slots_[]里面分配;否则去CentralFreeList.nonempty中分配,如果仍然无法分配成功,则去PageHeap中申请一个span,对应的class包含多个page并将其划分成N个object并返回。

2、中型对象内存
PageHeap先从伙伴系统对应npages的span链表里面查找空闲的span(即k个page到128个page,优先查normal链、然后returned链),找到后将其拆分成两个span,一个为K个Page,另外一个为n-k个Page,前者返回,后者放回原来等于n-k的span链表。

3、大对象内存
直接向PageHeap去申请一个刚好大于等于请求size的span。申请过程与中型对象内存分配一致,只是在失败后,会通过sbrk或mmap向系统内存申请并转换成span链表,再重新走上面的分配过程。
这里面有一个class的概念,其实就是object的一些预定义的大小,也就是说变长类型内存划分成了不同的等长内存处理。

五、回收流程

1、释放指针,通过PageHeap的映射关系,返回到Cache,class为0表示是大对象。
2、小对象内存
将内存对象返回到ThreadCache.list_[class],如果其超过指定值(FreeList.length_>=FreeList.max_length_)或者ThreadCache容量越限(ThreadCache.size_>=ThreadCache.max_size_),则启动回收,object被返回到CentralCache的class对应的CentralFreeList上,和分配相反,先尝试batch_size整块回收,并放到相应的Cache的tc_slots_中。
如果Cache已满或者无法满足整块回收,则将单个object回收到sapn.object链表中。通过radix tree如果发现前后的span均为空闲且在normal链则进行合并;而PageHeap会将多余的span回收到returned链上,并会择机进行合并。
3、中对象内存
中型对象的回收和小对象回收一致。
4、大对象内存
由于其指针就是一个span,所以直接丢回PageHeap即可。
这里需要说明一下,returned的span链表表示PageHeap已经将内存返回会系统而normal的span表示应用程序释放后的内存自己控制下。二者虽然空闲且无连续但不同的span块不可以合并。

六、总结

熟悉使用一种机制后,就会越来越发现其相着的优缺点。在实际应用中,就会有意识的扬长避短,但对于一些基础的应用,有时候无可替代,就只能将就一下。但有些人不原意将就,就自己造一个更适合的基础应用的轮子。在国内,缺少的恰恰就是这种基础类的轮子的制造。所谓大的互联网公司,只不过是在别人的应用的基础上再重复造个轮子,吓唬一下没懂行的罢了。
知已知彼,百战不殆。有问题不怕,怕的是不正视问题。继续努力吧。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值