哈喽,各位好!今天咱们来聊聊 C++ 里那些内存分配器中的线程局部缓存(Thread-Local Cache,简称 TLC)。这玩意儿听起来玄乎,但实际上是内存分配器为了提升性能,耍的一个小聪明。咱们以 jemalloc
和 tcmalloc
为例,深入浅出地扒一扒它的原理。
一、内存分配器的前世今生:从malloc
到jemalloc/tcmalloc
话说当年,C 语言横行天下,malloc
和 free
这对好基友几乎成了动态内存分配的代名词。但随着应用越来越复杂,多线程编程越来越普及,malloc
的缺点也暴露出来了:
- 锁的争用:
malloc
内部通常使用全局锁来保护堆,多个线程同时申请内存时,必须排队等锁,效率低下。想象一下,只有一个厕所,大家都要上,那酸爽… - 内存碎片: 频繁的分配和释放会导致堆中出现很多小的、不连续的空闲块,导致大块内存无法分配,明明还有总容量,却不能分配,这是内存碎片化。
为了解决这些问题,各种高级内存分配器应运而生,比如 jemalloc
(Facebook 出品),tcmalloc
(Google 出品),还有 mimalloc
(Microsoft 出品) 等等。它们的核心思想都是尽量减少锁的争用,提高内存分配的效率。而 TLC 就是它们提高效率的一个重要武器。
二、什么是线程局部缓存(TLC)?
简单来说,TLC 就是每个线程私有的、用于缓存小块内存的区域。 每个线程都有自己的一个“小金库”,当线程需要分配小块内存时,先从自己的“小金库”里找,如果够用就直接分配,不需要去全局堆里抢锁。 释放的时候也一样,先放回自己的“小金库”。
TLC 的好处:
- 减少锁的争用: 线程从自己的 TLC 中分配和释放内存,不需要和其他线程竞争全局锁,大大提高了并发性能。
- 提高缓存命中率: 线程频繁使用的内存块更有可能留在 TLC 中,下次使用时直接命中,速度更快。
TLC 的缺点:
- 空间浪费: 每个线程都有自己的 TLC,即使线程使用的内存很少,也需要占用一定的空间。
- 内存迁移: 如果一个线程释放的内存被另一个线程需要,需要进行内存迁移,会增加开销。
三、jemalloc
的 TLC 实现
jemalloc
的 TLC 实现比较复杂,它采用了多层级的缓存结构:
- Thread-Local Cache (TLC): 最底层的缓存,每个线程独占。
- Arena: 一组相关的内存页的集合,可以理解为一个小型的堆。
- Global Arena: 最大的堆,所有线程共享。
当一个线程需要分配内存时,jemalloc
会按以下步骤进行:
- TLC 查找: 首先在线程自己的 TLC 中查找是否有合适的空闲块。
- Arena 填充: 如果 TLC 中没有合适的空闲块,
jemalloc
会尝试从与该线程关联的 Arena 中填充 TLC。 - Global Arena 分配: 如果 Arena 中也没有合适的空闲块,
jemalloc
才会从 Global Arena 中分配新的内存块,并填充到 Arena 中。
释放内存的过程则相反:
- TLC 释放: 优先将释放的内存块放回线程自己的 TLC。
- Arena 回收: 如果 TLC 满了,会将一部分内存块返回给 Arena。
- Global Arena 释放: 如果 Arena 也满了,才会将内存块释放回 Global Arena。
jemalloc
使用 extent
来管理内存块。 extent
是连续的虚拟内存区域,可以被分割成更小的块来满足分配请求。
jemalloc
的 TLC 相关数据结构 (简化版):
// 线程局部缓存 (Thread-Local Cache)
typedef struct {
malloc_bin_t *bins[MALLOCX_NCPUS][NBINS]; // 二级指针,bins是一个数组,数组的元素是指针,指向malloc_bin_t类型的结构体
// ... 其他字段
} tcache_t;
// Arena,管理一组内存页的集合
typedef struct {
// ... 其他字段
} arena_t;
// 内存块管理结构
typedef struct malloc_bin_s malloc_bin_t;
struct malloc_bin_s {
size_t size;
uint64_t n_allocated;
size_t n_cached;
malloc_bin_t* next;
};
jemalloc
的 TLC 相关代码示例 (简化版):
// 从 TLC 中分配内存
void* tcache_alloc(tcache_t* tcache, size_t size) {
// 根据 size 找到对应的 bin
malloc_bin_t* bin = get_bin(size);
// 从 bin 中取出一个空闲块
if (bin->n_cached > 0) {
void* ptr = bin->next; // 假设 bin 使用链表存储空闲块
bin->next = ((malloc_bin_t*)ptr)->next;
bin->n_cached--;
bin->n_allocated++;
return ptr;
}
// TLC 中没有空闲块,需要从 Arena 或 Global Arena 中分配
return NULL; // 简化处理,实际实现更复杂
}
// 将内存块释放到 TLC
void tcache_dealloc(tcache_t* tcache, void* ptr, size_t size) {
// 根据 size 找到对应的 bin
malloc_bin_t* bin = get_bin(size);
// 将内存块添加到 bin 的链表头
((malloc_bin_t*)ptr)->next = bin->next;
bin->next = (malloc_bin_t*)ptr;
bin->n_cached++;
bin->n_allocated--;
// 如果 TLC 满了,需要将一部分内存块返回给 Arena
// ...
}
四、tcmalloc
的 TLC 实现
tcmalloc
的 TLC 实现相对简单一些,它也采用了线程局部缓存,但没有 jemalloc
那么多层级的结构。
tcmalloc
将内存分为两种类型:
- Small Object: 小于 32KB 的对象。
- Large Object: 大于等于 32KB 的对象。
tcmalloc
的 TLC 主要用于缓存 Small Object。 每个线程都有一个 ThreadCache
对象,其中包含多个 FreeList
,每个 FreeList
对应一种大小的 Small Object。
当一个线程需要分配 Small Object 时,tcmalloc
会按以下步骤进行:
- ThreadCache 查找: 首先在线程自己的
ThreadCache
中查找是否有对应大小的空闲块。 - CentralCache 填充: 如果
ThreadCache
中没有空闲块,tcmalloc
会从CentralCache
中填充ThreadCache
。CentralCache
是所有线程共享的缓存,但它使用了锁来保护。 - PageHeap 分配: 如果
CentralCache
中也没有空闲块,tcmalloc
才会从PageHeap
中分配新的内存页,并填充到CentralCache
中。PageHeap
是tcmalloc
的底层内存管理机制。
释放 Small Object 的过程则相反:
- ThreadCache 释放: 优先将释放的内存块放回线程自己的
ThreadCache
。 - CentralCache 回收: 如果
ThreadCache
满了,会将一部分内存块返回给CentralCache
。 - PageHeap 释放: 如果
CentralCache
也满了,才会将内存页释放回PageHeap
。
Large Object 直接从 PageHeap
分配和释放,不经过 TLC。
tcmalloc
的 TLC 相关数据结构 (简化版):
// 线程缓存 (ThreadCache)
class ThreadCache {
public:
FreeList free_lists_[kNumClasses]; // FreeList 数组,每个 FreeList 对应一种大小的 Small Object
// ... 其他字段
};
// 空闲列表 (FreeList)
class FreeList {
public:
void* list_head_; // 空闲块链表的头指针
size_t length_; // 空闲块的数量
// ... 其他字段
};
// CentralCache,所有线程共享的缓存
class CentralCache {
public:
// ... 其他字段
};
// PageHeap,底层内存管理机制
class PageHeap {
public:
// ... 其他字段
};
tcmalloc
的 TLC 相关代码示例 (简化版):
// 从 ThreadCache 中分配内存
void* ThreadCache::Allocate(size_t size) {
int class_index = SizeMap::SizeToClass(size); // 根据 size 找到对应的 class index
FreeList* free_list = &free_lists_[class_index];
// 从 FreeList 中取出一个空闲块
if (free_list->length_ > 0) {
void* ptr = free_list->list_head_;
free_list->list_head_ = ((void**)ptr)[0]; // 假设 FreeList 使用链表存储空闲块
free_list->length_--;
return ptr;
}
// ThreadCache 中没有空闲块,需要从 CentralCache 中填充
return NULL; // 简化处理,实际实现更复杂
}
// 将内存块释放到 ThreadCache
void ThreadCache::Deallocate(void* ptr, size_t size) {
int class_index = SizeMap::SizeToClass(size); // 根据 size 找到对应的 class index
FreeList* free_list = &free_lists_[class_index];
// 将内存块添加到 FreeList 的链表头
((void**)ptr)[0] = free_list->list_head_;
free_list->list_head_ = ptr;
free_list->length_++;
// 如果 ThreadCache 满了,需要将一部分内存块返回给 CentralCache
// ...
}
五、TLC 的配置和使用
jemalloc
和 tcmalloc
都提供了一些配置选项,可以用来调整 TLC 的行为。
jemalloc
: 可以通过环境变量MALLOC_CONF
来配置jemalloc
。例如,可以设置opt.tcache:false
来禁用 TLC。tcmalloc
: 可以通过MallocExtension
类来配置tcmalloc
。例如,可以使用MallocExtension::SetMaxPerThreadCacheSize
来设置每个线程的 TLC 的最大大小。
代码示例:
// tcmalloc 的配置示例
#include <gperftools/malloc_extension.h>
int main() {
// 设置每个线程的 TLC 的最大大小为 1MB
MallocExtension::instance()->SetMaxPerThreadCacheSize(1024 * 1024);
// ...
return 0;
}
六、TLC 的优缺点分析
特性 | 优点 | 缺点 |
---|---|---|
性能 | 减少锁的争用,提高并发性能 | 内存迁移会增加开销,特别是线程间频繁传递数据时 |
内存利用率 | 可以更好地利用缓存,提高缓存命中率 | 可能会导致内存碎片,特别是线程分配的内存大小差异较大时 |
空间占用 | 无 | 每个线程都需要分配 TLC,可能会占用较多的内存,特别是线程数量较多时 |
适用场景 | 多线程应用,频繁分配和释放小块内存的应用 | 单线程应用,内存分配量较小的应用 |
调试难度 | 无 | TLC 的存在会增加内存泄漏和内存错误的调试难度,因为内存分配和释放发生在不同的线程中 |
七、总结
TLC 是高级内存分配器为了提高并发性能而采用的一种重要技术。它通过为每个线程提供私有的缓存,减少了锁的争用,提高了缓存命中率。但是,TLC 也有一些缺点,比如空间浪费和内存迁移开销。在实际应用中,需要根据具体的场景和需求来选择是否使用 TLC,以及如何配置 TLC。
希望今天的讲解能帮助大家更好地理解 C++ 内存分配器中的 TLC 技术。 记住,理解这些底层原理,能让你在遇到性能问题时,多一些排查和优化的思路。 祝大家编程愉快!