Jacobwpeng.github.io
Github Pages
Project maintained by jacobwpeng
Hosted on GitHub Pages — Theme by mattgraham
TCMalloc
1.简介
- Google出品
- 全名 Thread-Cached Malloc, 针对多线程情况做了大量的优化
- 通过线程私有分配区和中央分配区来避免锁争用
- 利用Thread-Local Storage, 为每个线程提供一个私有的分配区,理想情况下所有的小内存分配都发生在线程私有分配区上,当缓存的小内存块不足以满足要求时会从中央分配区索要一堆同等大小的内存块缓存下来
- 借鉴了垃圾收集(garbage collection)的思想,动态的调节每个线程私有分配区所缓存的内存大小,当缓存内存超过某个值时将部分缓存的内存块还给中央分配区,中央分配区缓存内存超限时也会归还给OS
2.小内存申请
- 判断当前线程是否已经初始化了私有分配区
- 根据请求大小找到对应的SizeClass, 然后通过SizeClass找到对应的FreeLis
t, 判断FreeList中是否有空闲内存块
- 当FreeList中没有空闲内存块时,向中央分配区申请一定数量(N)的这个SizeClass所代表的内存块
- 1. 对中央分配区加锁
- 2. 查看中央分配区中SizeClass对应的CentralFreeList中空闲内存块是否超过N
- 3. 如果CentralFreeList中的内存块不足时,根据SizeClass计算申请向PageHeap申请一定数量(npage)的页.
- 4. 对PageHeap加锁
- 5. PageHeap首先查找npage对应的大小对应的SpanList,查看是否有能够直接满足这个需求的span存在
- 6. 若找不到合适的span则继续向更大的SpanList中查找,找到后对大span做切割并放到对应的FreeList中
- 7. 若还是找不到则向系统申请内存, 添加对应到SpanList中
- 8. 将从PageHeap申请的span按照SizeClass所代表的内存块大小进行切割,添加到对应的CentralFreeList中, 同时将span加到该CentralFreeList中的non_empty_span_list_中, 在PageHeap中设置span对应的SizeClass
- 9. 将申请到的内存块返回给线程私有分配区
- 返回一个刚申请的内存块,malloc调用返回
3.大内存申请
- 根据请求大小计算需要申请多少个页(N)
- 对PageHeap加锁
- 向PageHeap申请N个页, 返回包含这些页的spanm, 在PageHeap中设置span对应的SizeClass
- 返回刚申请到的span,malloc调用返回
4.内存释放(free)
- 首先根据释放的指针判断指针所属的PageID(ptr >> kPageShift)
- 根据PageID从PageHeap中找到所属的SizeClass
- 如果SizeClass属于大内存,那么直接调用PageHeap的
Delete
函数来回收这个Span, free返回
- 如果SizeClass属于小内存, 那么将这个内存块缓存在线程私有分配区中对应的FreeList中
- 判断本线程缓存的内存块总和大小
size_
是否超过本线程的缓存上限max_size_
- 如果超过则遍历FreeList,根据每个FreeList的low water mark来判断需要归还多少个内存块给中央分配区,然后增加本线程的
max_size_
,防止非常活跃的线程调用free频繁触发内存块归还操作
- 判断FreeList是否过长,如果过长,则返回一个
batch_size
的内存块给对应的CentralFreeList
- free返回
5.Span
Span代表了一块连续的内存页
start
(Span第一个页对应的PageID)
length
(Span包含的页数量)
objects
(Span被切割后对应的内存块组成的单链表)
refcount
(Span包含的内存块中已被使用的个数)
location
(Span所属的状态)
- 如果Span被切割后分配给任何FreeList或者当做大内存返回给用户,则为IN_USE
- 如果Span没有被任何FreeList引用,而且内存由PageHeap缓存着,则为NORMAL_FREELIST
- 如果Span没有被任何FreeList引用,而且内存已经通过madvice返回给操作系统,则为RETURNED_FREELIST
Span一般通过双链表方式保存在CentralFreeList的non_empty_
或者empty_
中
6.中央分配区管理
中央分配区实际上是一个CentralFreeList对应的数组,每个SizeClass都会对应一个CentralFreeList
CentralFreeList
关键成员
size_class_
代表当前CentralFreeList所属的SizeClass
non_empty_
中保存了被切割成SizeClass代表大小且没有完全被完全使用的SpanList双链表
empty_
中保存了被切割成SizeClass代表大小且被完全分配给线程私有分配区或者缓存在CentralFreeList中的SpanList双链表
tc_slots_
TCEntry数组,TCEntry是一个双链表,每个双链表保存了从ThreadCache归还的数量为size_class_
对应的batch_size
个内存块,避免每次申请都需要从non_empty_
中的span里一个一个切割返回,加快分配速度
used_slots_
保存了tc_slots_
数组当前已经使用的索引
cache_size_
当前允许的最大used_slots_
值,防止缓存过多内存块导致Span无法被回收, 并根据当前CentralFreeList的繁忙程度动态调节
max_cache_size_
保存cache_size_
的最大值
关键操作
FetchFromSpans
从non_empty_
的span中取出一个内存块
Populate
根据size_class_
向PageHeap申请一个span
ReleaseToSpans
将内存块返回给对应的span, 如果span已经没有被引用,则归还给PageHeap
ShrinkCache
减少缓存在usedslots中的TCEntry数量
MakeCacheSpace
尝试能否缓存当前归还的内存块,如果used_slots_
达到cache_size_
却还没到max_cache_size_
时,尝试调用其它CentralFreeList的ShrinkCache
,然后自增cached_size_
InsertRange
线程私有存储区将内存归还给CentralFreeList, 如果返回的内存块数量为batch_size_
并且MakeCacheSpace
成功,则缓存到used_slots_
中,否则直接归还给对应的span
7.PageHeap
PageHeap直接管理从OS申请过来的内存,并提供Page级别的内存分配,同时在内部维护PageID到Span的映射关系
调用PageHeap的Delete
函数时并不会直接将内存返回给OS,而是缓存在对应的SpanList中,并增加已经释放的内存页总大小,如果总大小超过阈值时尝试从SpanList归还一些内存给OS
关键成员
pagemap_
PageID->Span, 内部根据指针大小使用两层(32-bit)或三层(64-bit)Radix Tree压缩占用空间
pagemap_cache_
PageID->SizeClass,内部使用一个包含64K个元素的一维数组来实现Cache查找
SpanList
包含两个成员(normal, returned),其中normal保存处于NORMAL_FREELIST状态的span,returned保存处于RETURNED_FREELIST状态的span
free_
SpanList数组,保存从[1-128)KiB每个大小对应的Span的双链表
large_
SpanList, 包括大于等于128KiB的Span组成的双链表
release_index_
下一个归还span给OS的SpanList, Round-Robin算法
关键操作
New
申请长度为n个Page的内存, 先调用SearchFreeAndLargeLists
,如果失败则查看当前缓存的span占用的内存总和是否超过已申请内存的1/4, 并且距离上一次失败后已经至少申请了128M内存, 如果满足条件则释放掉所有缓存的SpanList中的Normal Span
Delete
回收内存
SearchFreeAndLargeLists
根据传入的长度N在free_
和large_
中查找大于等于N个Page的span, 使用Best-Fit
Carve
将一个缓存的Span根据传入的长度切割出一个长为N的Span, 如果有剩余则将剩余的span放到对应的FreeList中
GrowHeap
向OS申请内存, 并把申请到的内存保存为一个span, 挂到对应的FreeList下
ReleaseAtLeastNPages
将缓存在large_
和free_
中处于normal状态的span通过madvice将内存归还给OS, 同时将span置于returned状态
8.细节设计
FreeList的low water mark(L)
- 表示自从上次GC之后FreeList所缓存的未被使用内存块的数量
- 每次GC都会返回L/2个内存块给中央分配区
- 避免线程缓存过多不需要的内存块
- 避免再次增大对该大小内存块的需求时短时间再次出发对中央分配区的访问
- 如果线程不再需要本大小内存块时,会以Log(n)的速率快速下降
- GC完成后会尝试增加自己的缓存配额
- 触发GC的线程对内存有更多的需求,增加缓存配额可以减少对中央分配区的访问次数
- 先从全局配额中申请,全局配额不足时从其它线程夺取配额
通过这种配额的动态调整可以增加内存活跃线程的配额,减少不活跃线程的配额,从而降低全局访问中央分配区的数量
PageHeap
- PageHeap的PageMap
- 如果直接使用一维数组保存PageID到Span的映射,那么在64-Bit下也需要使用大量的空间,而且其中大部分空间可能都用不上
- 为了提高内存使用效率,在64-Bit下使用Level-3 Radix Tree, 只为使用到的PageID分配空间来大幅度降低附加数据占用的空间
- PageHeap的PageMapCached
- 对于小内存来说,释放的时候只需要知道当前指针对应的SizeClass就足够了,这时候如果使用PageMap仍然会进行几次查找才能找到对应的span, 然后才能查看span对应的SizeClass
- 使用长度为2^16的一维数组来缓存常用的PageID到SizeClass的映射
- 在32-Bit下value的类型为uint16_t, 使用PageID的低16Bit为Key, SizeClass组成value的低7位(88个SizeClass), 高位由PageID的[17-19]位组成
- 在64-Bit下与32-Bit基本一致,只是由于value使用uint64_t表示,value的高位由整个PageID组成
- 使用这种扁平的数组在系统频繁使用小内存时可以尽量保持数组更多的项在cache中,提高处理速度
通过使用PageMap和PageMapCached不但压缩了附加数据的内存占用,而且对cache更加友好
内存释放
- PageHeap内部默认使用两种内存申请方式,优先使用
sbrk
, 以mmap
为备用机制
- 当PageHeap缓存了比较多的Span的时候,会试图将处于NORMAL_FREELIST中的Span所占用的内存通过
madvise
系统调用释放,并将span置于RETURNED_FREELIST中
- 使用MADV_DONTNEED调用
madvise
, 会释放虚拟内存区所绑定的物理内存,从而降低进程缩消耗的物理内存,同时由于默认使用sbrk
,避免库内部和应用层同时使用mmap导致Memory Map Region出现大量的碎片
9.概念
CentralCache(中央分配区)
ThreadCache(线程私有分配区)
- 使用TLS(Thread-Local Cache)保存分配区
- 操作不加锁
- 保存FreeList数组, 每个线程最多缓存4MiB, 最少512KiB, 所有线程最多32MiB
SmallChunk
- 小于 256KiB的内存块
- 能够被ThreadCache缓存的最大内存块
LargeChunk
Page
- 8KiB
- 每次分配的最小单位
- 由PageHeap管理
- 每一个Page对应一个PageID
Span
- 连续的Page
- PageHeap管理的最小单位
- 分为 IN_USE, NORMAL_FREE, RETURNED_FREE
- 每个Span对应一个PageID区间
- 每个Span可能被用来切割为一堆更小的内存块,或者整体作为一块大内存时用
PageHeap
- 最底层的内存分配区,直接从OS申请内存并进行管理
- 内部由几个SpanList组成
- 通过PageMap保存PageID到Page对应的Span
- 通过PageMapCached缓存PageID到Page对应切割成的
SpinLock
- 自旋锁
- 用户态忙等
while (flag) continue;
- 适用于临界区很短,处理很快的场景
SizeClass
- 内部分配的小内存块大小类型
- 根据用户申请的内存块像上取最接近的块