详细内容

使用

动态库

通过-ltcmalloc-ltcmalloc_minimal链接

g++ -O0 -g -ltcmalloc xxx.cc

静态库

应该使用libtcmalloc_and_profiler.a库而不是libprofiler.alibtcmalloc.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_FREELISTON_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_FREELISTON_RETURNED_FREELIST状态的span。因此,实际的PageHeap是这样的:


ShengYg

Step after step the ladder is ascended.


Tags • note