热爱推理
分类: C/C++
2013-09-21 10:51:38
一般来说,tcmalloc为“小块”创建了86个size classes,每一个class都会定义thread cache的特性,碎片化以及waste特征。
对于一个特定的size class,一次性分配的页数就是一个thread cache特性。tcmalloc细致地定义了这个页数,使得central cache和thread cache
之间的转换能够保持以下两者的平衡:thread cache周边的wasting chunk,以及访问central cache太过于频繁而导致的锁竞争。定义这个页数的程
序代码也保证了每一种class的waste比例最多为12.5%,当然,malloc API需要保证内存对齐。
size class data存于SizeMap中,并且是启动阶段第一个初始化的对象。
thread cache是一种惰性初始化的thread-local数据结构,每个size class包含一个free list(单向),此外,他们包含了表示他们容量的总大
小的元数据。
在最佳的情况下,从thread cache分配内存和释放内存是无锁的,并且时间复杂度是线性的。如果当前thread cache不包含需要分配的内存块时,
PageHeap算是整个系统的根本,当内存块没有作为cache,或者没有被应用程序申请时,他们位于PageHeap的free list中,也就是他们起初被
TCMalloc_SystemAlloc分配的位置,最终又会被TCMalloc_SystemRelease释放给操作系统。当“大块”内存被申请时,PageHeap也提供了接口跟
踪heap元数据。
PageHeap管理Span对象,Span对象表示连续的页面。每一个Span有几个重要的特性。
一,PageID start是由Span描述的内存起始地址,PageID的类型是uintptr_t。
二,Length length是Span页面的数量,Length的类型也是uintptr_t。
三,Span *next和Span *prev是Span的指针,当Span位于PageHeap的free list双向链表中。
四,一堆更多的东西,但限于篇幅就不谈了。
PageHeap有kMaxPages + 1的free list,span length从0到kMaxPages一一对应一个free list,大于kMaxPages的有一个free list,这些list都是
双向的,并且分为了normal和returned两个部分。
一,normal部分包含这样的Span,他们的页面明确地映射到进程地址空间。
二,returned部分包含这样的Span,他们的页面通过调用含有MADV_FREE参数的madvise返还给操作系统。操作系统在必要的时候可以回收这些页面,
但是当应用程序在操作系统回收前使用了这些页面,madvise的调用实际上是无效的。甚至当内存已经被回收,内核会重新把这些地址映射到一块全
零的内存。因此,重新利用returned的页面不仅是安全的,而且还是减少heap碎片化的一种重要的策略。
PageHeap包含了PageMap,PageMap是一个radix tree数据结构,会映射到他们对应的Span对象。PageHeap也包含PageMapCache,PageMapCache会
映射内存块的PageID到他们在cache中的内存块对应的size class。这是tcmalloc存储元数据的机制,而不是使用headers和footers对应实际的指针。
尽管这样有些浪费空间,但是这样在实质上可以更有效地缓存,因为所有相关的数据结构都被“slab”式地分配了。
PageHeap通过调用PageHeap::New(Length n)分配内存,其中n是需要分配的页面数。
一,大于等于n的free list(除非n大于等kMaxPages)会被遍历一遍,查找是否有足够大的Span。如果找到了这样的Span,这个Span会从list移除,
然后返回这个Span,这种分配是最合适的,但是因为地址不是有序的,因此从内存碎片化的角度来说是次优的,大概算是一种性能上的折中。normal
list会在继续检查returned list前全被检查一遍。但是我也不知道为什么。
二,如果步骤一没有找到合适的Span,算法将会遍历“大块”list,并且查找最合适的地址有序的Span。这个算法的时间复杂度是O(n),它不仅会
遍历所有的“大块”list,在并发大幅波动的情况下,这可能会非常耗时,而且还会遍历有碎片的heap。我针对这种情况写过一个补丁,当“大块”
list超过了一个可配置的总大小时,通过重组“大块”list为一个skip list来提高应用程序的大内存分配的性能。
三,如果找到的Span比需要分配的内存大至少一个页面尺寸时,这个Span会被切分为适合内存分配的尺寸,在返回分配内存块之前,剩下的内存会
添加到合适的free list中。
四,如果没有找到合适的Span,PageHeap会在重复这些步骤前尝试增长至少n个页面。如果第二次查找还是没有找到合适的内存块,这个内存分配最
终会返回ENOMEM。
内存释放是通过PageHeap::Delete(Span *span)实现,该方法的作用是把Span合并到合适的free list中。
一,从PageMap查找该Span的相邻Span对象(左和右),如果在一边或者两边找到了free的内存,他们会从free list中去除,并且合并到Span中。
二,预先要知道span现在属于哪个free list。
三,PageHeap会在检查是否需要释放内存给操作系统,如果确实需要,则释放。
每次Span返还给PageHeap,Span的成员scavenge_counter_会减少Span的length,如果scavenge_counter_降到0,则从free list或者“大块”list
释放的Span会从normal list中去除,并添加到合适的returned list中等待回收。scavenge_counter_被重置为:
min(kMaxReleaseDelay, (1000.0 / FLAGS_tcmalloc_release_rate) * number_of_pages_released)。
因此,调整FLAGS_tcmalloc_release_rate在内存释放时非常有用。