使用
动态库
通过-ltcmalloc
或-ltcmalloc_minimal
链接
g++ -O0 -g -ltcmalloc xxx.cc
静态库
应该使用libtcmalloc_and_profiler.a
库而不是libprofiler.a
和libtcmalloc.a
g++ -O0 -g -pthread test.cc /usr/local/lib/libtcmalloc_and_profiler.a
如何生效
指定-ltcmalloc
或者与libtcmalloc_and_profiler.a
连接之后,对malloc、free、new、delete等的调用就由默认的libc中的函数调用变为TCMalloc中相应的函数调用,原因在在libc_override.h中。
使用glibc,但没有GCC
在glibc中,内存分配相关的函数都是弱符号(weak symbol),因此TCMalloc只需要定义自己的函数将其覆盖即可。
// libc_override_redefine.h
extern "C" {
void* malloc(size_t s) { return tc_malloc(s); }
void free(void* p) { tc_free(p); }
} // extern "C"
void* operator new(size_t size) { return tc_new(size);}
void operator delete(void* p) CPP_NOTHROW { tc_delete(p); }
使用GCC
GCC支持alias,表明tc_malloc是malloc的别名
// libc_override_gcc_and_weak.h:
#define ALIAS(tc_fn) __attribute__ ((alias (#tc_fn), used))
extern "C" {
void* malloc(size_t size) __THROW ALIAS(tc_malloc);
void free(void* ptr) __THROW ALIAS(tc_free);
} // extern "C"
初始化
TCMalloc定义了一个类TCMallocGuard
,并在文件tcmalloc.cc中定义了该类型的静态变量module_enter_exit_hook
,在其构造函数中执行TCMalloc的初始化逻辑,以确保TCMalloc在main()函数之前完成初始化,防止在初始化之前就有多个线程。
// tcmalloc.cc:
static TCMallocGuard module_enter_exit_hook;
static int tcmallocguard_refcount = 0; // no lock needed: runs before main()
TCMallocGuard::TCMallocGuard() {
if (tcmallocguard_refcount++ == 0) {
ReplaceSystemAlloc(); // defined in libc_override_*.h
tc_free(tc_malloc(1));
ThreadCache::InitTSD();
tc_free(tc_malloc(1));
// Either we, or debugallocation.cc, or valgrind will control memory
// management. We register our extension if we're the winner.
#ifdef TCMALLOC_USING_DEBUGALLOCATION
// Let debugallocation register its extension.
#else
if (RunningOnValgrind()) {
// Let Valgrind uses its own malloc (so don't register our extension).
} else {
MallocExtension::Register(new TCMallocImplementation);
}
#endif
}
}
可以看到,TCMalloc初始化的方式是调用tc_malloc()
申请一字节内存并随后调用tc_free()
将其释放。
TCMalloc的初始化工作:
- 初始化SizeMap(Size Class)
- 初始化各种Allocator
- 初始化CentralCache
- 创建PageHeap
内存分配算法
TCMalloc将内存分配分为三类:
- 小对象分配,(0, 256KB]
- 中对象分配,(256KB, 1MB]
- 大对象分配,(1MB, +∞)
几个概念
- Page:TCMalloc将整个虚拟内存空间划分为n个同等大小的Page,每个Page默认8KB
- Span:连续的n个Page称为一个Span
- PageHeap:用于向OS申请内存相关的操作,并提供了一层缓存。申请时以span为单位,申请到的span可能只有一个page,也可能包含n个page。可能会被划分为一系列的小对象,也可能当做一整块。
几个概念
Page
Page是TCMalloc管理内存的基本单位(这里的page要区分于操作系统管理虚拟内存的page),默认大小为8KB,可在configure时通过选项调整为32KB或64KB。
./configure <other flags> --with-tcmalloc-pagesize=32
page越大,TCMalloc的速度相对越快,但其占用的内存也会越高。简单说,就是空间换时间的道理。
PageID
TCMalloc将整个虚拟内存空间(并非堆内存)都看做是page的集合。从内存地址0x0开始,每个page对应一个递增的PageID。对于任意内存地址ptr,都可通过简单的移位操作来计算其所在page的PageID:
static const size_t kPageShift = 13; // page大小:1 << 13 = 8KB
const PageID p = reinterpret_cast<uintptr_t>(ptr) >> kPageShift;
Span
一个或多个连续的Page组成一个Span,TCMalloc以Span为单位向系统申请内存。
一个span记录了起始page的PageID(start
),以及所包含page的数量(length
),两个Span类型的指针(prev
, next
),用于将多个span以链表的形式存储。
一个span要么被拆分成多个相同size class的小对象用于小对象分配,要么作为一个整体用于中对象或大对象分配。当作用作小对象分配时,span的sizeclass
成员变量记录了其对应的size class。
一个span处于以下三种状态中的一种:
- IN_USE
- ON_NORMAL_FREELIST
- ON_RETURNED_FREELIST
从PageHeap的角度看,IN_USE
比较好理解,要么被拆分成小对象分配给CentralCache或者ThreadCache了,要么已经分配给应用程序了;ON_NORMAL_FREELIST
和ON_RETURNED_FREELIST
都可以认为是空闲状态,区别在于,ON_RETURNED_FREELIST是指span对应的内存已经被PageHeap释放给系统了,其虚拟内存地址依然是可访问的,只是对这些内存的修改丢失,在下一次访问时会导致page fault以用0来重新初始化。
PageMap
PageMap缓存了PageID到Span的对应关系。在root_
数组中包含512个指向Leaf的指针,每个Leaf
又是1024个void*的数组,数组索引为PageID,数组元素为page所属Span的指针。
使用两级map可以减少TCMalloc元数据的内存占用,因为初始只会给第一层(即root_数组)分配内存(2KB),第二层只有在实际用到时才会实际分配内存。
小对象分配
Size class
TCMalloc按大小划分了若干类别称为Size Class,每个Size Class对应一个大小,如8字节,16字节,32字节。程序申请内存时,TCMalloc会向上取整到Size class的大小。这里会产生内部碎片。如何控制内部碎片?
对于每个size class,TCMalloc向系统申请内存时一次性申请n个page(一个span),然后均分成多个小对象进行缓存,以此来均摊系统调用的开销。如何决定n的大小呢?从1个page开始递增,一直到均分成若干小对象后所剩的空间小于span总大小的1/8为止。因此,浪费的内存被控制在12.5%以内。这是TCMalloc减少内部碎片的一种措施。
ThreadCache
对于每个线程,TCMalloc都为其保存了一份单独的缓存,称之为ThreadCache。每个ThreadCache中对于每个size class都有一个单独的FreeList,缓存了n个还未被应用程序使用的空闲对象。ThreadCache之间以双向链表连接。
小对象的分配直接从ThreadCache的FreeList中返回一个空闲对象,相应的,小对象的回收也是将其重新放回ThreadCache中对应的FreeList中。
由于每线程一个ThreadCache,因此从ThreadCache中取用或回收内存是不需要加锁的,速度很快。

CentralCache
CentralCache是所有线程公用的缓存,在TCMalloc中并不是一个类,只是一个逻辑上的概念。CentralCache中对于每个size class也都有一个单独的链表来缓存空闲对象,称之为CentralFreeList,供各线程的ThreadCache从中取用空闲对象。
由于是所有线程公用的,因此从CentralCache中取用或回收对象,是需要加锁的。为了平摊锁操作的开销,ThreadCache一般从CentralCache中一次性取用或回收多个空闲对象。默认每次移动64KB大小的内存,可以通过环境变量TCMALLOC_TRANSFER_NUM_OBJ
调整。
PageHeap
PageHeap用来给CentralCache提供空闲对象。当CentralCache中的空闲对象不够用时,CentralCache会向PageHeap申请一块内存(可能来自PageHeap的缓存,也可能向系统申请新的内存),并将其拆分成一系列空闲对象,添加到对应size class的CentralFreeList中。
PageHeap内部根据内存块(span)的大小采取了两种不同的缓存策略。128个page以内的span,每个大小都用一个链表来缓存,超过128个page的span,存储于一个有序set(std::set)。

内存回收
应用程序调用free()或delete一个小对象时,仅仅是将其插入到ThreadCache中其size class对应的FreeList中而已,不需要加锁,因此速度也是非常快的。
只有当满足一定的条件时,ThreadCache中的空闲对象才会重新放回CentralCache中,CentralCache中的空闲对象再还给PageHeap,PageHeap再还给系统。
中对象分配
对于中对象(超过256KB但不超过1MB),TCMalloc会将应用程序所要申请的内存大小向上取整到整数个page(8kB),然后向PageHeap申请一个指定page数量的span并返回其起始地址即可:
Span* span = Static::pageheap()->New(num_pages);
result = (PREDICT_FALSE(span == NULL) ? NULL : SpanToMallocResult(span));
return result;
问题在于,PageHeap是如何管理这些span的?即PageHeap::New()
是如何实现的。
对于中对象,不同大小的span以链表形式缓存。假设需要从PageHeap取一个大小为k个page的span,过程如下:
- 从k个page的span链表开始,到128个page的span链表,按顺序找到第一个非空链表。
- 取出这个非空链表中的一个span,假设有n个page,将这个span拆分成两个span:
- 一个span大小为k个page,作为分配结果返回。
- 另一个span大小为n - k个page,重新插入到n - k个page的span链表中。
- 如果找不到非空链表,则将这次分配看做是大对象分配。
大对象分配
大对象分配用到的span的缓存是一个按span大小排序的有序set。假设需要从PageHeap取一个大小为k个page的span,过程如下:
- 搜索set,找到不小于k个page的最小的span(best-fit),假设该span有n个page。
- 将这个span拆分为两个span:
- 一个span大小为k个page,作为结果返回。
- 另一个span大小为n - k个page,如果n - k > 128,则将其插入到大span的set中,否则,将其插入到对应的小span链表中。
- 如果找不到合适的span,则使用sbrk或mmap向系统申请新的内存以生成新的span,并重新执行中对象或大对象的分配算法。
小结

实现细节
PageHeap
TCMalloc使用Page和Span对内存进行划分,使用PageHeap管理划分后的内存
PageHeap对page数不同的span分开管理,同时也分开管理ON_NORMAL_FREELIST
和ON_RETURNED_FREELIST
状态的span。因此,实际的PageHeap是这样的:
