TCMalloc源码阅读(一)

前言

最近一直在折腾内存管理,先是自己实现了一个非常简单的内存管理,但是和malloc对比测试之后发现效率相差甚多。偶然在网上发现TCMalloc,下载下来之后与malloc做了简单的对比测试,惊奇的发现tcmalloc的速度果然如它自己所述,比glibcmalloc快了很多倍。遂果断扔掉自己实现的那个简单的内存管理,改用tcmalloc。同时也对tcmalloc的实现感到非常的好奇,因此决定阅读研究其源码,一探究竟。


函数入口

我想大多数人想了解一个库的源代码肯怕第一件事就是急切寻找函数的入口,我也是。对于如此神奇的代码,我已经迫不及待想一窥它的究竟,没有耐心在网上搜索,在代码里寻找了,我选择了最简单有效的办法--调试。

Tcmalloc的函数入口在tcmalloc.cc中定义,malloc和free的入口函数代码如下:

// CAVEAT: The code structure below ensures that MallocHook methods are always
//         called from the stack frame of the invoked allocation function.
//         heap-checker.cc depends on this to start a stack trace from
//         the call to the (de)allocation function.
extern "C" PERFTOOLS_DLL_DECL void* tc_malloc(size_t size) __THROW {
  void* result = do_malloc_or_cpp_alloc(size);
  MallocHook::InvokeNewHook(result, size);
  return result;
}
extern "C" PERFTOOLS_DLL_DECL void tc_free(void* ptr) __THROW {
  MallocHook::InvokeDeleteHook(ptr);
  do_free(ptr);
}
// The default "do_free" that uses the default callback.
inline void do_free(void* ptr) {
  return do_free_with_callback(ptr, &InvalidFree);
}


调试到这,我大脑里出现了第一个问题:

1) 测试程序只是简单的链接了下tcmalloc的库,调用malloc时咋就跑这来了?

我想大概是修改glibc的malloc函数入口地址。但是目前这个不是我关心的重点,目前我关注的重点是内存是怎么分配的,为何这么高效。

tc_malloc函数代码非常简单,只有两行。不难理解,第一行实现内存的分配。第二行是干嘛的呢?从注释里可以看出大概是用来跟踪调试的,这个同样不是我目前关心的重点。

继续看do_malloc_or_cpp_alloc代码,代码如下:

// TODO(willchan): Investigate whether or not lining this much is harmful to
// performance.
// This is equivalent to do_malloc() except when tc_new_mode is set to true.
// Otherwise, it will run the std::new_handler if set.
inline void* do_malloc_or_cpp_alloc(size_t size) {
  return tc_new_mode ? cpp_alloc(size, true) : do_malloc(size);
}

看来该函数也只是一个路由函数,根据tc_new_mode来选择不同的分配函数。这个tc_new_mode到底是啥东东,作何用处?

在tcmalloc文件中搜索tc_new_mode,发现它的设置函数:

// This function behaves similarly to MSVC's _set_new_mode.
// If flag is 0 (default), calls to malloc will behave normally.
// If flag is 1, calls to malloc will behave like calls to new,
// and the std_new_handler will be invoked on failure.
// Returns the previous mode.
extern "C" PERFTOOLS_DLL_DECL int tc_set_new_mode(int flag) __THROW {
  int old_mode = tc_new_mode;
  tc_new_mode = flag;
  return old_mode;
}

从注释中不难看出,原来通过设置该值来决定是使用普通的malloc模式还是使用C++里面new的模式,如果该值被设置为1,当内存分配失败时std_new_handler就会被调用,从而向应用程序抛出异常。

真正实现内存分配的函数就是do_malloc,代码如下:

inline void* do_malloc(size_t size) {
  void* ret = NULL;
  // The following call forces module initialization
  ThreadCache* heap = ThreadCache::GetCache();
  if (size <= kMaxSize) {
    size_t cl = Static::sizemap()->SizeClass(size);
    size = Static::sizemap()->class_to_size(cl);
    if ((FLAGS_tcmalloc_sample_parameter > 0) && heap->SampleAllocation(size)) {
      ret = DoSampledAllocation(size);
    } else {
      // The common case, and also the simplest.  This just pops the
      // size-appropriate freelist, after replenishing it if it's empty.
      ret = CheckedMallocResult(heap->Allocate(size, cl));
    }
  } else {
    ret = do_malloc_pages(heap, size);
  }
  if (ret == NULL) errno = ENOMEM;
  return ret;
}

kMaxSize是一个常量值,大小为256*1024。这段代码的逻辑很简单,当要分配的内存不超过kMaxSize时且FLAGS_tcmalloc_sample_paramete >= 0的时候就从ThreadCache中分配。对于FLAGS_tcmalloc_sample_parameter<0的情况暂且不去研究。当要分配的内存大小超过kMaxSize时从页面分配。回顾一下tcmalloc的介绍文档,tcmalloc的内存池分为线程局部缓存内存池和中心共享缓存内存池。当分配的内存大小<32K时从局部缓存分配,超过32K的就去中心页堆进行分配。从代码中基本得到印证,但是文档中说的是32K,而代码里是256K,查看以前版本代码发现kMaxSize在以前版本是32K,在当前最新版本中改成了256K。


总结:

  1. malloc的函数入口为tc_malloc。
  2. free的函数入口为tc_free。
  3. tc_new_mode的设置决定内存分配失败时是否抛出异常。
  4. 小于kMaxSize的内存从线程局部缓存分配,超过的从中心页堆中分配。

下一篇分析TCMalloc如何将size规整对齐。

你可能感兴趣的:(linux,C++)