C++ `jemalloc` / `tcmalloc` 源代码分析:理解高性能分配器的内部机制

哈喽,各位好!

今天咱们来聊聊C++里两员大将:jemalloctcmalloc,它们都是高性能内存分配器,江湖人称“malloc终结者”。这俩家伙可不是盖的,能显著提升程序的性能,尤其是在多线程环境下。咱们深入源代码,揭秘它们的内部运作机制,看看它们是如何做到如此高效的。

一、内存分配的痛点:标准malloc的不足

在了解jemalloctcmalloc之前,先简单回顾一下标准malloc。标准malloc虽然历史悠久,应用广泛,但存在一些固有的问题:

  • 锁竞争: 多线程环境下,多个线程同时调用mallocfree,会产生激烈的锁竞争,导致性能瓶颈。想象一下,只有一个洗手间,一大堆人排队,那效率可想而知。
  • 内存碎片: 频繁地分配和释放不同大小的内存块,容易产生内存碎片,降低内存利用率。就像家里乱扔东西,空间越来越小。
  • 缺乏精细控制: 标准malloc提供的控制选项较少,难以针对特定应用场景进行优化。

二、jemalloc:碎片整理大师

jemalloc("je"代表Jason Evans,作者的名字)以其出色的碎片整理能力和多线程支持而闻名。它的核心思想是:分而治之。

1. Arena:内存分配的隔离区

jemalloc将内存划分为多个独立的区域,称为arena。每个线程优先使用自己的arena进行内存分配,从而减少锁竞争。

// jemalloc中arena的基本概念(简化)
struct arena_s {
  // 用于分配小对象的arena
  extent_heap_t small_objects;
  // 其他元数据...
};

// extent_heap_t 用于管理内存块
typedef struct {
  // 红黑树或其他数据结构,用于存储可用的内存块
  ...
} extent_heap_t;

每个线程都有一个或者多个arena,分配内存时,优先从线程关联的arena中分配。如果线程数量超过arena的数量,某些线程会共享arenaarena本身也可能存在锁,但是由于线程优先使用自己的arena,锁竞争大大降低。

2. Chunk:内存分配的基本单元

arena又被划分为更小的单元,称为chunkchunkjemalloc管理内存的基本单位。通常,chunk的大小是固定的(例如4MB)。

3. Bin:小对象的高效管理者

对于小对象(例如小于几KB),jemalloc使用bin进行管理。bin是一组大小相同的内存块的集合。

// jemalloc中bin的基本概念(简化)
struct bin_s {
  // 一个链表,存储空闲的内存块
  malloc_mutex_t lock;
  extent_list_t  chunks;
  size_t         nfree;
};

typedef struct {
  // 双向链表
  ...
} extent_list_t;

当需要分配小对象时,jemalloc首先查找对应大小的bin,如果bin中有空闲的内存块,则直接返回;否则,从chunk中分割出一块新的内存块。

4. Large Object and Huge Object: 大块头的待遇

对于大对象和巨型对象,jemalloc采用直接分配的方式,从arena中分配连续的内存空间。

可以用以下表格总结:

对象大小 分配方式 优点 缺点
小对象 (Small) Bin分配 速度快,减少碎片 需要预先分配和管理Bin
大对象 (Large) 直接分配 避免Bin的限制 容易产生碎片
巨型对象 (Huge) 特殊处理,直接映射 避免Arena的限制,适用于超大内存需求,可以绕过Arena的限制,使用更大的连续内存空间 需要操作系统支持,可能导致内存管理复杂性增加,不方便进行碎片整理

5. jemalloc的核心优势

  • 多线程友好: 通过arena隔离,减少锁竞争。
  • 碎片整理: 精心设计的内存管理机制,减少内存碎片。
  • 可配置性: 提供了丰富的配置选项,可以根据应用场景进行优化。

代码示例(简化):jemalloc的小对象分配

// 非常简化的jemalloc小对象分配过程
void* je_small_alloc(size_t size) {
  // 1. 确定对象所属的bin
  int bin_index = compute_bin_index(size);

  // 2. 获取当前线程关联的arena
  arena_s* arena = get_thread_arena();

  // 3. 获取对应的bin
  bin_s* bin = &arena->bins[bin_index];

  // 4. 加锁,保护bin
  malloc_mutex_lock(&bin->lock);

  // 5. 从bin中获取空闲内存块
  void* ptr = extent_list_first(&bin->chunks);
  if (ptr == NULL) {
    // 6. 如果bin中没有空闲内存块,则从chunk中分配新的内存块
    ptr = allocate_from_chunk(arena, size);
  } else {
    extent_list_remove(&bin->chunks, ptr);
    bin->nfree--;
  }

  // 7. 解锁
  malloc_mutex_unlock(&bin->lock);

  // 8. 返回内存块指针
  return ptr;
}

三、tcmalloc:Google出品,必属精品

tcmalloc(Thread-Caching Malloc)是Google开发的内存分配器,也是一个高性能的解决方案。它的核心思想是:线程缓存。

1. ThreadCache:线程私有的小金库

tcmalloc为每个线程维护一个独立的缓存,称为ThreadCache。线程分配小对象时,首先从ThreadCache中分配,避免了锁竞争。

// tcmalloc中ThreadCache的基本概念(简化)
class ThreadCache {
 public:
  // FreeLists数组,每个元素对应一种大小的空闲内存块链表
  FreeList freelists_[kNumClasses];

  // 从ThreadCache中分配内存
  void* Allocate(size_t size);

  // 将内存释放到ThreadCache中
  void Deallocate(void* ptr, size_t size);
};

ThreadCache中存储了不同大小的空闲内存块,组织形式通常是FreeList

2. CentralCache:线程缓存的后援

ThreadCache中的空闲内存块不足时,tcmalloc会从CentralCache中获取。CentralCache是一个全局的缓存,由所有线程共享。

// tcmalloc中CentralCache的基本概念(简化)
class CentralCache {
 public:
  // SpanList数组,每个元素对应一种大小的Span链表
  SpanList freelists_[kNumClasses];

  // 从CentralCache中获取Span
  Span* FetchFromSpans(int size_class);

  // 将Span释放到CentralCache中
  void ReleaseToSpans(Span* span, int size_class);
};

CentralCache也维护了不同大小的空闲内存块,这些内存块被组织成SpanSpan是一组连续的页面(Page),是tcmalloc管理内存的基本单位。

3. PageHeap:内存的最终来源

CentralCache中的空闲内存块也可能不足,这时,tcmalloc会向PageHeap申请新的内存。PageHeap负责管理整个堆内存。

// tcmalloc中PageHeap的基本概念(简化)
class PageHeap {
 public:
  // 使用某种数据结构(例如树)来管理空闲页面
  ...

  // 从PageHeap中分配内存
  void* AllocatePages(int num_pages);

  // 将内存释放到PageHeap中
  void DeallocatePages(void* ptr, int num_pages);
};

PageHeap通常使用树形结构或其他数据结构来管理空闲的页面,以便快速查找和分配内存。

4. Span:连接一切的纽带

Spantcmalloc中一个非常重要的概念,它代表一组连续的页面。SpanThreadCacheCentralCachePageHeap之间流动,是内存分配和释放的纽带。

5. tcmalloc的核心优势

  • 线程缓存: 通过ThreadCache减少锁竞争,提高性能。
  • 分级缓存: ThreadCacheCentralCachePageHeap形成分级缓存体系,提高内存利用率。
  • 高效的内存管理: 精心设计的内存管理机制,减少内存碎片。

代码示例(简化):tcmalloc的小对象分配

// 非常简化的tcmalloc小对象分配过程
void* tc_small_alloc(size_t size) {
  // 1. 获取当前线程的ThreadCache
  ThreadCache* cache = GetThreadCache();

  // 2. 确定对象所属的size class
  int size_class = SizeToClass(size);

  // 3. 从ThreadCache中获取空闲内存块
  void* ptr = cache->Allocate(size_class);

  // 4. 如果ThreadCache中没有空闲内存块,则从CentralCache中获取
  if (ptr == NULL) {
    ptr = GetCentralCache()->FetchFromSpans(size_class);
    if(ptr != NULL){
       cache->Allocate(size_class); //放入ThreadCache
    }
  }
  // 5. 如果CentralCache中也没有空闲内存块,则从PageHeap中获取
  if (ptr == NULL) {
    // 从PageHeap中申请内存,比较复杂,省略...
  }

  // 6. 返回内存块指针
  return ptr;
}

四、jemalloc vs tcmalloc:英雄惜英雄

jemalloctcmalloc都是优秀的内存分配器,它们各有优缺点:

特性 jemalloc tcmalloc
核心思想 arena隔离,减少锁竞争,碎片整理 线程缓存,分级缓存,提高内存利用率
多线程支持 优秀 优秀
碎片整理 出色 良好
可配置性 丰富 较少
适用场景 对碎片整理要求较高的场景,例如长时间运行的服务 对内存利用率要求较高的场景,例如大规模并发应用

五、实际应用:如何选择?

选择哪个内存分配器,取决于具体的应用场景。一般来说:

  • 对性能要求高,且内存碎片问题比较突出的应用,可以选择jemalloc
  • 对内存利用率要求高,且并发量大的应用,可以选择tcmalloc

当然,最好的方法是进行实际测试,对比不同分配器在特定应用场景下的性能表现。

六、使用方法

使用jemalloctcmalloc,通常只需要在编译时链接对应的库即可。例如,在使用gccclang编译时,可以添加-ljemalloc-ltcmalloc选项。

g++ -o my_program my_program.cpp -ljemalloc  # 使用jemalloc
g++ -o my_program my_program.cpp -ltcmalloc  # 使用tcmalloc

此外,还可以通过环境变量来控制jemalloctcmalloc的行为,例如:

  • MALLOC_CONF:用于配置jemalloc的参数。
  • TCMALLOC_RELEASE_RATE:用于控制tcmalloc将空闲内存释放给操作系统的频率。

七、总结与思考

jemalloctcmalloc都是优秀的内存分配器,它们通过精心的设计和优化,解决了标准malloc的一些问题,提高了程序的性能和内存利用率。深入理解它们的内部机制,可以帮助我们更好地选择和使用内存分配器,优化程序的性能。

希望今天的分享能够帮助大家更好地理解jemalloctcmalloc,并在实际项目中灵活运用。

八、进一步学习

如果想更深入地了解jemalloctcmalloc,可以参考以下资料:

内存分配是一个复杂而有趣的话题,希望大家能够继续探索,不断学习。

好啦,今天的分享就到这里,感谢大家的聆听!如果大家有什么问题,欢迎提问。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注