(一)简介
tcmalloc是与glibc、malloc同一级别的内存管理库,tcmalloc会hack所有glibc提供的接口,为调用者提供透明的内存分配。
(二)总体结构
- pageheap
内存管理单位:span(连续的page的内存)
- centralcache
内存管理单位:object(由span切成的小块,同一个span切出来的object都是相同的规格)
- threadcache
线程私有的缓存,理想情况下,每个线程的内存需求都在自己的threadcache中完成,线程之间不需要竞争,非常高效。
内存管理单位:class(由span切成的小块)
(三)分配与回收
基本思想:前面的层次分配内存失败,则从下一层分配一批补充上来;前面的层次释放了过多的内存,则回收一批到下一层次。
- 分配
1)小块内存(<256kb)。
threadcache:先尝试在list_[class]的freelist分配。
centralcache:找到对应class的tc_slots链表,从链表中分配 -> 从nonempty_链表分配(尽量分配batch_size个object)
headpage:伙伴系统对应的npages的span链表 (normal->returned)-> 更大的npages的span链表,拆小
kernel:申请若干个page的内存(可能大于npages)
2)大块内存(>256kb)。
headpage:伙伴系统对应的npages的span链表 (normal->returned)-> 更大的npages的span链表,拆小
kernel:申请若干个page的内存(可能大于npages)
- 回收
与申请流程类似
1)threadcache => centralcache
threadcache容量限额:
a、为每一个threadcache初始化一个比较小的限额,然后每当threadcache由于cache超限而触发object到centralcache的回收时,就增大限额。
b、预设所有threadcache的总容量,一个threadcache容量不够时,从其他threadcache收刮(轮询)。
c、每个threadcache也有最大最小值限制,不能无限增大限额。
d、每个threadcache超过限额时,对其每个freelist回收。
单个freelist的限额:
a、慢启动。初始长度限制为1,限额1~batch_size之间为慢启动,每次限额 1。
b、超过batch_size,限额按照batch_size整数倍扩展。
c、freelist限额超限,直接回收batch_size个object。
2)centralcache => pageheap
只要一个span里面的object都空闲了,就将它回收到pageheap。
3)pagehead中的normal => returned
a、每当pageheap回收到n个page的span时(这个过程中可能伴随着相当数目的span分配),触发一次normal到returned的回收,只回收一个span。
b、这个n值初始化为1g内存的page数,每次回收span到returned链之后,可能还会增加n值,但是最大不超过4g。
c、触发回收的过程,每次进来轮询伙伴系统中的一个normal链表,将链尾的span回收即可。
(四)数据结构
- pageheap
1)page到span的映射关系通过radix tree来实现,逻辑上理解为一个大数组,以page的值作为偏移,就能访问到page对应的span节点。
2)为减少查询radix tree的开销,pageheap还维护了一个最近最常使用的若干个page到object的对应关系cache。为了保持cache的效率,cache只提供64个固定坑位。
3)空闲span的伙伴系统为上层提供span的分配与回收。当需要的span没有空闲时,可以把更大尺寸的span拆小;当span回收时,又需要判断相邻的span是否空闲,以便组合他们
4)normal和returned:多余的内存放到returned中,与normal隔离。normal的内存总是优先被使用,kernel倾向于一直保留他们;而returned的内存不常被使用,kernel内存不足时优先swap他们。
- centralfreelist
1)维护span的链表,每个span下面再挂一个由这个span切分出来的object的链。便于在span内的object都已经free的情况下,将span整体回收给pageheap;每个回收回来的object都需要寻找自己所属的span后才挂进freelist,比较耗时。
2)empty的span链和nonempty的span链:centralfreelist中的span链表有nonempty_和empty_两个,根据span的object链是否有空闲,放入对应链表。如果span的内存已经用完则把这个span移到empty链表中。
3)通过页找到对应span:被centralfreelist使用的span,都会把这个span上的所有页都注册到radixtree中,这样对于这个span上的任意页都可以通过页id找到这个span。
4)如果span的内存已经完全被释放(span->refcount==0),则把这个span归还到pagehead中。
- threadcache
1)tcmalloc为每个线程创建一个threadcache对象,当线程结束的时候,threadcache对象会随之销毁。
2)threadcache为每种类型的内存都保存了一个单项链表。
(五)适用场景
tcmalloc适用线程内小内存分配需求,一般情况下只有大空间分配才使用中央堆,中央堆分配回收我记得是需要加锁,成本高。
(六)使用遇到的坑
在一个项目中,使用thrift的threadpool模型 上游短连接 tcmalloc时,性能大幅下降,大概只有使用普通malloc的1/10。
效果对比图如下:
但是同样是使用tcmalloc,在短连接 多线程or长连接 线程池场景下,性能却不受影响:
可以确定,是 tcmalloc 短连接 thrift线程池模型,才会出现这个坑。
使用oprofile工具对事件:1)分支预测错误;2)cpu时钟;3)指令数;4)l2缓存未命中 采样监控,观察服务一段时间,如下:
可以看到,cpu时钟占用的排序为:
tcmalloc::centralfreelist::fetchfromspans(spans->centralfreelist)、
tcmalloc::centralfreelist::releasetospans(centralfreelist->spans)、
tcmalloc::threadcache::releasetocentralcache(threadcache->centralcache)、
spinlock::spinloop(分配pageheap时使用)、
tcmalloc::centralfreelist::releaselisttospans(centralfreelist->spans)
可以发现,服务过程中有频繁的centralfreelist与pageheap之间的内存申请and回收,而tcmalloc的pagehead申请是使用的spinlock锁,消耗大。