原文:http://blog.chinaunix.net/uid-24990614-id-3911071.html
本文根据How tcmalloc Works翻译而来,作者是James Golick,原文地址:http://jamesgolick.com/2013/5/19/how-tcmalloc-works.html
前言
tcmalloc是一款专为高并发而优化的内存分配器。tcmalloc的tc含义是thread cache,tcmalloc正是通过thread cache这种机制实现了大多数情
况下的无锁内存分配。这可能是我有幸拜读的最为精密设计的软件,虽然我不能详述tcmalloc的细节,但是我会尽量谈及到tcmalloc的所有重点。
tcmalloc与大多数现代分配器一样,使用的是基于页的内存分配,也就是说,这种内存分配的内部度量单位是页,而不是字节。这种内存分配
可以有效地减少内存碎片,同时,也可以增加局部性。此外,也可以使得元数据的跟踪更为简单。tcmalloc定义一页为8K字节,在大多数的linux
系统中,一页是4K字节,也就是tcmalloc的一页是linux的两页。
tcmalloc中的内存分配块整体来说分为两类,“小块”和“大块”,“小块”是小于kMaxPages的内存块,“小块”可以进一步分为size classes,
而且“小块”的内存分配是通过thread cache或者central per-size class cache而实现。“大块”是大于等于kMaxPages的内存块,“大块”的
内存分配是通过central PageHeap实现。
Size Class
一般来说,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 Caches
thread cache是一种惰性初始化的thread-local数据结构,每个size class包含一个free list(单向),此外,他们包含了表示他们容量的总大
小的元数据。
在最佳的情况下,从thread cache分配内存和释放内存是无锁的,并且时间复杂度是线性的。如果当前thread cache不包含需要分配的内存块时,
thread cache从central cache获取那种class的内存块。如果thread cache的空间太多,thread cache的内存块会返还给central cache,每一个
central cache都有一个锁,这个锁用来减少内存返还时的竞争。
虽然thread cache会有内存块的迁移,但是thread cache的大小会根据两个有趣的方式限定在一个范围内。
其一,所有的thread cache的总大小会有一个上限。每一个thread cache都会在内存块的迁移,分配和释放时跟踪它当前的内存块总容量。起初,
每一个 thread cache都会被分配相同的内存空间。但是,当thread cache的容量动态变化时,会有一个算法使得一个thread cache可以从其他的
thread cache偷取 没有使用的空间。
其二,每一个free list都有一个上限,这个上限会随着内存块从central cache迁入时以一种有趣的方式增加,如果list超过了上限,内存块会释
放给central cache。
当内存的释放或者有central cache的内存块的迁入而导致thread cache超过了上限,thread cache首先会试图查找free list中特别的headroom,
以检查thread cache是否有多余的需要释放给central cache的内存。 当free list满足了条件时,被加进free list的内存块就是多余的 。如果
还是没有空出足够的空间时, thread cache会从其他的thread cache中偷取空间,当然需要拥有pageheap_lock。
central cache 需要更多的空间时,他们可以使用thread cache类似的方式从其他的central cache偷取内存空间。当thread cache需要返还内存块给
central cache,而central cache又已经满了无法获取更多的空间时,central cache会释放这些内存对象给PageHeap,也就是起初central cache
获取他们的地方。
Page Heap
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在内存释放时非常有用。
总结
一,这个博客相当长,恭喜你看到了这里,同时我觉得我似乎什么也没说。
二,如果你觉得这个问题很有趣,我非常推荐你看源代码。虽然tcmalloc很复杂,但是代码写得很易读,也有很多注释。我对C++不怎么熟悉,但是
仍然写了大量补丁,尤其以这篇博文为指南,tcmalloc就没什么可怕的了。
三,我将在接下来谈论jemalloc。
四,可以关注我以及Joe Damato的podcast,里面都是这种东西。