TCMallloc解密

TCMalloc是一种高效的多线程内存管理系统,替代标准库的内存分配函数。文章介绍了TCMalloc的安装、使用方法,以及其如何生效,重点讨论了TCMalloc的初始化、内存分配策略、实现细节,包括Size Class、ThreadCache、CentralCache和PageHeap的工作原理。文章还涵盖了内存回收、对象分配的分类(小、中、大对象)以及TCMalloc如何管理不同大小的对象。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

个人技术博客链接: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

  1. 从git仓库clone版本的gperftools的安装依赖autoconf、automake、libtool,以Debian为例:

    # apt install autoconf automake libtool
    
  2. 生成configure等一系列文件

    $ ./autogen.sh
    
  3. 生成Makefile

    $ ./configure
    
  4. 编译

    $ make
    
  5. 安装

    # 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")));
​ defines f 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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值