TCMalloc原理解析(上)
在学习内存管理时,学习TCMalloc的原理肯定是值得的。对于TCMalloc如何加载使用,本章不作介绍,如今AI发展迅猛,加之也有很多使用说明相关的文章,完全上网参考,这里推荐:(8 条消息) TCMalloc解密(一) - 知乎。
这里参考的源码是:tcmalloc: TCMalloc (google-perftools) 是用于优化C++写的多线程应用,比glibc 2.3的malloc快。这个模块可以用来让MySQL在高并发下内存占用更加稳定。
-
TCMalloc的简介
TCMalloc采用了“三级缓存”的架构(ThreadCache、CentralCache和PageHeap)、TLS和TSD机制,显著减少了锁竞争,提高了多线程环境下的内存分配效率,非常适用于并发场景。
1.1 TCMalloc的内存分配算法(简)
TCMalloc把内存按大小分成三类:
-
TCMalloc默认将page(页)的大小对齐为8KB(1 << 13)。
-
小内存对象:这段内存对象由三级缓存(ThreadCache、CentralCache和PageHeap)轮转管理,按括号里的前后顺序来分级分配。
-
小内存对象(0,256KB]。
-
中内存对象(256KB,1MB]。
-
-
大内存对象(1MB,~]。这里的内存大于128page,由有序Set来管理。
ThreadCache即线程持有的本地缓存,在ThreadCache中存在FreeList类,用于管理从CentralCache中分配来的内存对象。
CentralCache中包含Span成员,Span内管理着从PageHeap中分配下来的连续的page对象,一个Span管理的对象(内存对象),大小至少为1个page或着n个page,这些连续的page就称为Span。其中由CentralCache这个类来管理分配的Span,向下给ThreadCache提供/回收内存对象的分配,向上向PageHeap申请/交还内存对象。
PageHeap,在TCMalloc中,由它来作为向OS申请内存的缓存,在它与OS中间又设计了一层缓存来提高分配效率,它就是整个三缓体系的内存来源---向下层的CentralCache提供/回收Span对象。它以Span为单位向OS进行内存申请,Span的大小为1到n个page。如下图↓
1.2 Size Class
在TCMalloc内存分配器中,size class是核心设计概念之一,TCMalloc按内存请求将大小划分为多个size class,每个size class对应一个范围的内存大小,如1~8B对应8B的size class,9~16B对应16B的size class。
TCMalloc为小内存(<=256KB)定义了约85~88个size class,这些size class按大小间隔递增。
-
如8B、16B、24B、......、256KB,其中间隔大小也由内存尺寸变化而变化,小尺寸间隔8B,中尺寸间隔16B,大尺寸间隔32B,以此类推,最大间隔不超过256B。
这样内部碎片率控制在12.5%以内,外部碎片通过页级分配器管理。
1.3 ThreadCache
ThreadCache负责为每个线程提供快速的本地缓存,每个线程都有独立的ThreadCache,在小对象的分配下,其时间复杂度接近O(1)。其中ThreadCache管理着FreeList数组,FreeList数组按size class划分了多个双向空闲链表,每个链表挂在对应大小的内存对象。
小对象的分配可以直接在FreeList上操作,回收的小对象也是优先挂到FreeList上。因为是线程自己持有的,所以不需要加锁。
为了方便统计数据,用双向链表将每个线程的ThreadCache管理了起来。这里使用这张图可以很好得来表示:
1.4 CentralCache
当ThreadCache中内存不足或者内存过多时,就需要向上一层的CentralCache申请或者归还。CentralCache作为全局共享的缓存层,它链接着ThreadCache以及PageHeap,起着桥梁作用,当ThreadCache中的内存用尽时,需要向CentralCache申请,这个过程时需要加锁的,CentralCache向PageHeap申请内存的时候,也是需要加锁的,当然在CentralCache中操作几乎都是要加锁的。
CentralCache中管理着CentralFreeList,与ThreadCache中的FreeList类似,它同样也是按size class来划分管理内存对象的,同样也是双向链表。同样借用知乎大佬的图:
Span是CentralCache中管理内存的基本单位,CentralCache用两个双向链表来管理从PageHeap中申请来的Span,每个Span会被切分为固定大小的内存块,以供ThreadCache使用。
1.5 PageHeap
PageHeap作为CentralCache和ThreadCache的后端核心,直接与操作系统交互,负责大内存(>256KB)的回收和分配,它为CentralCache提供Span,用以支持小对象的分配。
PageHeap采用了两种策略来管理Span。小Span(<=128page)使用链表进行管理,并根据Span的大小分桶管理,大Span(>128page)采用有序集合(如std::set)按大小排序。
-
内存分配
上面讲到内存划分,这里就到各类内存的分配流程,可能并不会那么细致,在后面的源码讲解部分会补充到。
2.1 内存对象分配
对于小中大内存对象的分配,在介绍ThreadCache、CentralCache以及PageHeap的时候已经将流程大致穿插进去,这里就作流程总结:
-
将要分配的内存向上取整对应到相应的size class。
-
到线程本地的ThreadCache中查找对应size class的FreeList链表
-
若查找到,则分配第一个空闲对象给线程,结束。
-
若没有,转到3。
-
-
到CentralCache对应size class的FreeList中查找。
-
获取相应的桶锁。
-
若其中有空闲的内存对象,返回第一个空闲的内存对象,分配结束。
-
若没有,转到4。
-
-
将size class对齐到整数个page,获取PageHeap的锁,向PageHeap申请Span,PageHeap优先从缓存的小Span链表中查找满足需求的Span。
-
若找到,返回Span,或分割更大的Span,将分割剩余的Span链入更小的Span链表,分配结束。
-
上面算是小中内存的分配~~~当上面的流程申请失败,就是去申请大内存对象(>128page---1MB)~~~~~~~~~~~~~~以下就是大内存的分配了
-
若Span链表中未找到,到大Span集合中根据“最佳匹配”算法查找,若找到则分割Span并返回,将剩余的Span链入Span链表或放入大Span集合中,分配结束。若未找到,转到5。
-
-
PageHeap向系统申请Span,调用sbrk或mmap去申请内存,再用Span管理,然后分割,将需要的n页Span返回给CentralCache,将剩余的Span放到小Span链表或大Span集合中(分割后仍然大于128page)。分配结束。
这就是大致的流程了。