周末抽空看了一下tcmalloc,了解了个大概。下面记录一下。
一. 原理
tcmalloc就是一个内存分配器,管理堆内存,主要影响malloc和free,用于降低频繁分配、释放内存造成的性能损耗,并且有效地控制内存碎片。glibc中的内存分配器是ptmalloc2,tcmalloc号称要比它快。一次malloc和free操作,ptmalloc需要300ns,而tcmalloc只要50ns。同时tcmalloc也优化了小对象的存储,需要更少的空间。tcmalloc特别对多线程做了优化,对于小对象的分配基本上是不存在锁竞争,而大对象使用了细粒度、高效的自旋锁(spinlock)。分配给线程的本地缓存,在长时间空闲的情况下会被回收,供其他线程使用,这样提高了在多线程情况下的内存利用率,不会浪费内存,而这一点ptmalloc2是做不到的。
tcmalloc区别的对待大、小对象。它为每个线程分配了一个线程局部的cache,线程需要的小对象都是在其cache中分配的,由于是thread local的,所以基本上是无锁操作(在cache不够,需要增加内存时,会加锁)。同时,tcmalloc维护了进程级别的cache,所有的大对象都在这个cache中分配,由于多个线程的大对象的分配都从这个cache进行,所以必须加锁访问。在实际的程序中,小对象分配的频率要远远高于大对象,通过这种方式(小对象无锁分配,大对象加锁分配)可以提升整体性能。
线程级别cache和进程级别cache实际上就是一个多级的空闲块列表(Free List)。一个Free List以大小为k bytes倍数的空闲块进行分配,包含n个链表,每个链表存放大小为nk bytes的空闲块。在tcmalloc中,<=32KB的对象被称作是小对象,>32KB的是大对象。在小对象中,<=1024bytes的对象以8n bytes分配,1025<size<=32KB的对象以128n bytes大小分配,比如:要分配20bytes则返回的空闲块大小是24bytes的,这样在<=1024的情况下最多浪费7bytes,>1025则浪费127bytes。而大对象是以页大小4KB进行对齐的,最多会浪费4KB - 1 bytes。下图就是一个基本的free list的示意图:
实际上,一个free list(我称之为空闲块列表)就是一个数组索引多个链表,每个链表存放相同大小的块。可以根据要分配的内存大小size算出合适的块在free list中的下标,然后找到对应的空闲块链表。
tcmalloc的数据结构组织如下:
Thread-local free list:线程本地的空闲块cache,用于分配小对象。
Heap free list:中心free list,全局唯一,用于按页对齐分配大对象或者是将连续的多个页(被称作span)分割成多个小对象的空闲块分配给thread-local free list。
Page array:用于描述当前tcmalloc持有的内存状态,完成的是从page number到span的映射。
下面看一下小对象的分配:
(1)根据分配的size计算出对应的空闲块大小,从而确定对应空闲块链表,然后从thread local的free list进行分配。
(2)如果的空闲块链表非空,直接将头结点对应的空闲块返回并从空闲块链表中将其删除。
(3)如果空闲块链表是空的,需要从heap free list获取一个span。如果heap free list非空,则将span切分成多个相同大小的空闲块插入空闲块链表中,然后返回头结点。
(4)如果heap free list是空的,则调用sbrk或者mmap进行内存的分配一系列连续的内存页,作为span,然后切分成多个相同大小的空闲块插入空闲块链表,然后返回头结点。
大对象的分配就要简单多了,直接从heap free list分配4nKB大小的空闲块即可,如果heap free list不存在该大小的空闲块,通过系统调用分配连续的内存页。
tcmalloc还会对thread local cache进行垃圾收集,从而避免内存浪费。
二. 安装和使用
tcmalloc属于gperftools,安装比较简单,需要注意一下,在64bit系统上需要先安装libunwind(http://download.savannah.gnu.org/releases/libunwind/libunwind-0.99-beta.tar.gz,只能是这个版本),这个库为基于64位CPU和操作系统的程序提供了基本的堆栈辗转开解功能,其中包括用于输出堆栈跟踪的API、用于以编程方式辗转开解堆栈的API以及支持C++异常处理机制的API,32bit系统不需安装。在安装过程中,可能出现下列错误:
gcc -DHAVE_CONFIG_H -I. -I../include -I../include -I../include/tdep-x86_64 -I. -D_GNU_SOURCE -DNDEBUG -g -O2 -fexceptions -Wall -Wsign-compare -MT setjmp/longjmp.lo -MD -MP -MF setjmp/.deps/longjmp.Tpo -c setjmp/longjmp.c -fPIC -DPIC -o setjmp/.libs/longjmp.o
/usr/include/x86_64-linux-gnu/bits/setjmp2.h:26:13: error: 'longjmp' aliased to undefined symbol '_longjmp
是因为缺少编译选项U_FORTIFY_SOURCE,解决方法有两种:
1,尚未调用configure进行配置,则执行CPPFLAGS=-U_FORTIFY_SOURCE ./configure ... 会自动将编译选项添加到Makefile中。
2,已经配置过,直接修改Makefile,查找CPPFLAGS然后添加上-U_FORTIFY_SOURCE。
然后就是安装gperftools,这个正常安装即可,默认安装到/usr/local/lib下,完成后调用lddconfig添加到动态链接库缓存,然后就可以使用了。
使用方法,很简单,在编译时加上tcmalloc动态链接库即可
g++ test.cpp -ltcmalloc
源码不需任何修改,tcmalloc会自动替换掉glibc默认的malloc和free,简简单单的一条命令就可以提升不少性能,very good。