个人技术博客链接:https://wallenwang.com/2018/11/tcmalloc/
写在前面
本文首先简单介绍TCMalloc及其使用方法,然后解释TCMalloc替代系统的内存分配函数的原理,然后从宏观上讨论其内存分配的策略,在此之后再深入讨论实现细节。
有几点需要说明:
- 本文只讨论gperftools中TCMalloc部分的代码,对应版本gperftools-2.7。
- 本文是根据TCMalloc源码以及简短的官方介绍作出的个人理解,难免有纰漏之处,且难以覆盖TCMalloc的方方面面,不足之处还请各位看官留言指正。
- 除非特别说明,以下讨论均以32位系统、TCMalloc默认page大小(8KB)为基础,不同架构或不同page大小,有些相关数值可能不一样,但基本原理是相似的。
- 为了控制篇幅,我会尽量少贴大段代码,只给出关键代码的位置或函数名,各位看官可自行参阅TCMalloc相关代码。
TCMalloc是什么
TCMalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc、free,new,new[]等)。
TCMalloc是gperftools的一部分,除TCMalloc外,gperftools还包括heap-checker、heap-profiler和cpu-profiler。本文只讨论gperftools的TCMalloc部分。
git仓库:https://github.com/gperftools/gperftools.git
官方介绍:https://gperftools.github.io/gperftools/TCMalloc.html(里面有些内容已经过时了)
如何使用TCMalloc
安装
以下是比较常规的安装步骤,详细可参考gperftools中的INSTALL。
-
从git仓库clone版本的gperftools的安装依赖autoconf、automake、libtool,以Debian为例:
# apt install autoconf automake libtool
-
生成configure等一系列文件
$ ./autogen.sh
-
生成Makefile
$ ./configure
-
编译
$ make
-
安装
# make install
默认安装在
/usr/local/
下的相关路径(bin、lib、share),可在configure时以--prefix=PATH
指定其他路径。
64位Linux系统需要注意
在64位Linux环境下,gperftools使用glibc内置的stack-unwinder可能会引发死锁,因此官方推荐在配置和安装gperftools之前,先安装libunwind-0.99-beta,最好就用这个版本,版本太新或太旧都可能会有问题。
即便使用libunwind,在64位系统上还是会有问题,但只影响heap-checker、heap-profiler和cpu-profiler,TCMalloc不受影响,因此不再赘述,感兴趣的读者可参阅gperftools的INSTALL。
如果不希望安装libunwind,也可以用gperftools内置的stack unwinder,但需要应用程序、TCMalloc库、系统库(比如libc)在编译时开启帧指针(frame pointer)选项。
在x86-64下,编译时开启帧指针选项并不是默认行为。因此需要指定-fno-omit-frame-pointer
编译所有应用程序,然后在configure时通过--enable-frame-pointers
选项使用内置的gperftools stack unwinder。
使用
以动态库的方式
安装之后,通过-ltcmalloc
或-ltcmalloc_minimal
将TCMalloc链接到应用程序即可。
#include <stdlib.h>
int main( int argc, char *argv[] )
{
malloc(1);
}
$ g++ -O0 -g -ltcmalloc test.cc && gdb a.out
(gdb) b test.cc:5
Breakpoint 1 at 0x7af: file test.cc, line 5.
(gdb) r
Starting program: /home/wanglong/test/https://wallenwang.com/tcmalloc/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, main (argc=1, argv=0x7fffffffddd8) at test.cc:5
5 malloc(1);
(gdb) s
tc_malloc (size=1) at src/tcmalloc.cc:1892
1892 return malloc_fast_path<tcmalloc::malloc_oom>(size);
(gdb)
通过gdb断点可以看到对malloc()
的调用已经替换为TCMalloc的实现。
以静态库的方式
gperftools的README中说静态库应该使用libtcmalloc_and_profiler.a
库而不是libprofiler.a
和libtcmalloc.a,但简单测试后者也是OK的,而且在实际项目中也是用的后者,不知道是不是文档太过老旧了。
$ g++ -O0 -g -pthread test.cc /usr/local/lib/libtcmalloc_and_profiler.a
如果使用了libunwind,需要指定-Wl,--eh-frame-hdr
选项,以确保libunwind可以找到编译器生成的信息来进行栈回溯。
eh-frame(exception handling frame)参考资料:
- http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0803d/pge1446568291678.html
- http://zhaohongjian000.is-programmer.com/posts/29660.html
- https://www.cnblogs.com/catch/p/3619379.html
TCMalloc是如何生效的
为什么指定-ltcmalloc
或者与libtcmalloc_and_profiler.a
连接之后,对malloc、free、new、delete等的调用就由默认的libc中的函数调用变为TCMalloc中相应的函数调用了呢?答案在libc_override.h
中,下面只讨论常见的两种情况:使用了glibc,或者使用了GCC编译器。其余情况可自行查看相应的libc_override头文件。
使用glibc(但没有使用GCC编译器)
在glibc中,内存分配相关的函数都是弱符号(weak symbol),因此TCMalloc只需要定义自己的函数将其覆盖即可,以malloc和free为例:
libc_override_redefine.h
extern "C" {
void* malloc(size_t s) { return tc_malloc(s); }
void free(void* p) { tc_free(p); }
} // extern "C"
可以看到,TCMalloc将malloc()
和free()
分别定义为对tc_malloc()
和tc_free()
的调用,并在tc_malloc()
和tc_free()
中实现具体的内存分配和回收逻辑。
new和delete也类似:
void* operator new(size_t size) { return tc_new(size); }
void operator delete(void* p) CPP_NOTHROW { tc_delete(p); }
使用GCC编译器
如果使用了GCC编译器,则使用其支持的函数属性:alias。
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"
将宏展开,__attribute__ ((alias ("tc_malloc"), used))
表明tc_malloc是malloc的别名。
具体可参阅GCC相关文档:
alias (“target”)
The alias attribute causes the declaration to be emitted as an alias for another symbol, which must be specified. For instance,
void __f () { /* Do something. */; }
void f () __attribute__ ((weak, alias ("__f")));
definesf
to be a weak alias for__f
. In C++, the mangled name for the target must be used. It is an error if__f
is not defined in the same translation unit.
Not all target machines support this attribute.
used
This attribute, attached to a function, means that code must be emitted for the function even if it appears that the function is not referenced. This is useful, for example, when the function is referenced only in inline assembly.
When applied to a member function of a C++ class template, the attribute also means that the function will be instantiated if the class itself is instantiated.
TCMalloc的初始化
何时初始化
TCMalloc定义了一个类TCMallocGuard
,并在文件tcmalloc.cc中定义了该类型的静态变量module_enter_exit_hook
,在其构造函数中执行TCMalloc的初始化逻辑,以确保TCMalloc在main()
函数之前完成初始化,防止在初始化之前就有多个线程。
tcmalloc.cc:
static TCMallocGuard module_enter_exit_hook;
如果需要确保TCMalloc在某些全局构造函数运行之前就初始化完成,则需要在文件顶部创建一个静态TCMallocGuard实例。
如何初始化
TCMallocGuard的构造函数实现:
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()
将其释放。至于为什么在InitTSD前后各申请释放一次,不太清楚,猜测是为了测试在TSD(Thread Specific Data,详见后文)初始化之前也能正常工作。
初始化内容
那么TCMalloc在初始化时都执行了哪些操作呢?这里先简单列一下,后续讨论TCMalloc的实现细节时再逐一详细讨论:
- 初始化SizeMap(Size Class)
- 初始化各种Allocator
- 初始化CentralCache
- 创建PageHeap
总之一句话,创建TCMalloc自身的一些元数据,比如划分小对象的大小等等。
TCMalloc的内存分配算法概览
TCMalloc的官方介绍中将内存分配称为Object Allocation,本文也沿用这种叫法,并将object翻译为对象,可以将其理解为具有一定大小的内存。
按照所分配内存的大小,TCMalloc将内存分配分为三类:
- 小对象分配,(0, 256KB]
- 中对象分配,(256KB, 1MB]
- 大对象分配,(1MB, +∞)
简要介绍几个概念,Page,Span,PageHeap:
与操作系统管理内存的方式类似,TCMalloc将整个虚拟内存空间划分为n个同等大小的Page,每个page默认8KB。又将连续的n个page称为一个Span。
TCMalloc定义了PageHeap类来处理向OS申请内存相关的操作,并提供了一层缓存。可以认为,Page