项目源码:https://gitee.com/kkkred/thread-caching-malloc
在高并发场景(如百万QPS的API网关、实时游戏服务器)中,内存分配的延迟与碎片是性能瓶颈的核心来源。传统单内存池因全局锁竞争和内存碎片问题,难以满足高吞吐需求。本文将深入解析分层内存池框架的核心组件设计,重点聚焦ThreadCache的哈希桶映射规则、TLS无锁访问机制、CentralCache的Span结构与双链表管理,以及PageCache的Span获取逻辑,揭示高并发场景下内存分配的「无锁化」与「高效协同」之道。其中,ThreadCache的哈希桶索引计算是实现「高效分类、均匀分布」的关键环节,本文将详细展开其计算逻辑与实现细节。
ThreadCache的核心目标是为每个线程提供本地化、无锁的内存分配服务,其关键设计是基于对象大小的哈希桶映射。
内存对象按大小划分为多个「类」(Size Class),每个类对应一个哈希桶。分类规则需满足:
哈希桶索引的计算分为三个阶段:对象大小对齐→Size Class确定→桶索引映射,确保同类对象集中存储,同时避免哈希冲突。
通过位运算将原始大小向上取整到最近的2的幂次:
// 计算对齐后的大小(向上取整到2的幂次)
static size_t align_to_power_of_two(size_t size) {
if (size <= 0) return 8; // 最小对齐粒度为8B(处理0的特殊情况)
size_t aligned = 1;
while (aligned < size) {
aligned <<= 1; // 左移1位(等价于×2),直到超过或等于原始大小
}
return aligned;
}将所有可能的对齐后大小划分为预定义的「Size Class」(对象大小类别),每个类别对应一个哈希桶。Size Class的设计需覆盖高频场景并限制分类数量(通常10~20个)。
示例分类表:
Size Class(对齐后大小) | 对应哈希桶索引 | 典型对象场景 |
|---|---|---|
8B | 0 | 指针、布尔值、小整数 |
16B | 1 | 短字符串、结构体(如struct Point{x,y}) |
32B | 2 | 网络数据包头、小数组 |
64B | 3 | 哈希表条目、中等结构体 |
128B | 4 | 大结构体、小缓存行对象 |
256B | 5 | 文件元数据、IO缓冲区 |
512B | 6 | 网络数据包主体、数据库记录 |
1KB(1024B) | 7 | 大缓存行对象、日志条目 |
2KB(2048B) | 8 | 图像小块、序列化数据 |
4KB(4096B) | 9 | 页级内存块、大对象 |
代码实现(分类逻辑):
// 定义最大Size Class(示例为4KB,即2^12)
#define MAX_SIZE_CLASS 9 // 对应4KB(索引从0到9)
// 将对齐后的大小映射到Size Class索引
static int get_size_class_index(size_t aligned_size) {
static const size_t size_classes[] = {8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096};
for (int i = 0; i < MAX_SIZE_CLASS; i++) {
if (aligned_size <= size_classes[i]) {
return i; // 找到第一个大于等于aligned_size的分类
}
}
return MAX_SIZE_CLASS - 1; // 理论上不会执行(因MAX_SIZE_CLASS对应4KB)
}哈希桶的下标直接使用Size Class的索引,确保每个Size Class对应唯一的桶。这种设计避免了额外的哈希计算,将索引计算复杂度降至O(1)。
示例:
为避免多线程竞争,ThreadCache通过线程本地存储(TLS, Thread-Local Storage)实现「线程私有」的内存管理。每个线程独立拥有一个ThreadCache实例,无需全局锁。
在Linux系统中,通过__thread关键字声明线程本地变量,确保每个线程拥有独立的ThreadCache实例:
// 线程本地存储的ThreadCache实例
__thread ThreadCache* tls_thread_cache = NULL;
// 初始化线程本地ThreadCache
void init_thread_cache() {
if (!tls_thread_cache) {
tls_thread_cache = (ThreadCache*)malloc(sizeof(ThreadCache));
// 初始化哈希桶数组、空闲链表等
for (int i = 0; i < MAX_SIZE_CLASS; i++) {
tls_thread_cache->buckets[i] = NULL; // 初始化为空链表
}
}
}分配与释放操作仅在当前线程的ThreadCache实例中进行,无需加锁。分配时通过哈希桶索引快速定位空闲链表:
// ThreadCache分配函数(无锁,结合哈希桶索引计算)
void* thread_cache_alloc(size_t size) {
if (!tls_thread_cache || size == 0) return NULL;
// 1. 计算哈希桶索引(关键新增逻辑)
size_t aligned_size = align_to_power_of_two(size);
int class_index = get_size_class_index(aligned_size);
if (class_index < 0 || class_index >= MAX_SIZE_CLASS) {
class_index = MAX_SIZE_CLASS - 1; // 保险处理
}
void** bucket = &tls_thread_cache->buckets[class_index];
// 2. 从空闲链表头部取对象(O(1)时间)
if (*bucket) {
void* obj = *bucket;
*bucket = *(void**)obj; // 链表指针前移
return obj;
}
// 3. 本地无空闲对象,向CentralCache批量申请(触发全局协作)
return fetch_from_central_cache(class_index); // 传递Size Class索引
}
// ThreadCache释放函数(无锁,结合哈希桶索引)
void thread_cache_free(void* ptr, size_t size) {
if (!tls_thread_cache || !ptr) return;
// 1. 计算哈希桶索引(关键新增逻辑)
size_t aligned_size = align_to_power_of_two(size);
int class_index = get_size_class_index(aligned_size);
if (class_index < 0 || class_index >= MAX_SIZE_CLASS) {
class_index = MAX_SIZE_CLASS - 1; // 保险处理
}
void** bucket = &tls_thread_cache->buckets[class_index];
// 2. 插入空闲链表头部(O(1)时间)
*(void**)ptr = *bucket;
*bucket = ptr;
}关键优势:TLS无锁访问+哈希桶索引的O(1)计算,将分配/释放延迟降至O(1),彻底规避多线程锁竞争问题。
CentralCache是全局共享的内存池,负责管理ThreadCache无法直接处理的中大对象(>1KB),并通过Span结构和双链表实现高效的内存块管理。
为避免直接使用虚拟地址(可能跨页),CentralCache将内存块按「页」为单位管理(系统页大小通常为4KB)。页号(Page ID)定义为:
// 页号类型(使用uintptr_t保证跨平台兼容性)
using PageID = uintptr_t;每个Page ID对应一个4KB的虚拟页,通过页号可快速计算虚拟地址:
// 虚拟地址 = 页号 × 页大小
void* page_to_addr(PageID page_id) {
return (void*)(page_id * PAGE_SIZE);
}Span是CentralCache管理内存块的基本单元,记录内存块的起始页号、总页数、状态(空闲/已分配)等信息:
// Span结构体(管理连续页的内存块)
struct Span {
PageID start_page; // 起始页号
size_t page_count; // 总页数(如1页=4KB,2页=8KB)
Span* prev; // 双链表前驱指针
Span* next; // 双链表后继指针
bool is_free; // 是否空闲
// 扩展字段(如对象大小分类、引用计数等)
};page_to_addr可转换为虚拟地址);CentralCache通过双链表管理所有空闲Span,支持O(1)时间的插入、删除和合并操作。
// CentralCache结构体
struct CentralCache {
SpinLock lock; // 自旋锁(保护全局操作)
Span* free_spans[MAX_PAGE_COUNT]; // 按页数分类的空闲链表(如1页、2页、4页...)
size_t total_pages; // 总管理的页数
size_t used_pages; // 已使用的页数
};当释放一个Span时,CentralCache会检查相邻的Span是否空闲,若相邻则合并为更大的Span,避免内存碎片:
// 合并相邻的空闲Span
void merge_adjacent_spans(CentralCache* cache, Span* span) {
// 检查前驱Span是否空闲
if (span->prev && span->prev->is_free) {
span->start_page = span->prev->start_page;
span->page_count += span->prev->page_count;
// 从链表中移除前驱Span
span->prev->prev->next = span;
span->prev = span->prev->prev;
cache->free_spans[span->prev->page_count] = span->prev; // 更新链表头
}
// 检查后继Span是否空闲(类似逻辑)
// ...
}关键优势:双链表+按页分类的设计,使CentralCache能在O(1)时间内找到匹配的Span,同时通过合并操作保持内存块的连续性。
PageCache是内存池与操作系统交互的接口,负责管理大内存块(页级及以上),通过虚拟内存映射和延迟释放减少系统调用次数。
PageCache的核心目标是:
mmap/munmap;当CentralCache需要分配大内存块时,会向PageCache申请连续的页内存(Span)。以下是关键步骤:
PageCache首先检查自身的空闲页列表,是否有足够的连续页:
// PageCache结构体
struct PageCache {
SpinLock lock; // 自旋锁(保护全局操作)
Span* free_spans; // 空闲Span的双链表(按页数排序)
size_t total_pages; // 总管理的页数
size_t free_pages; // 空闲页数
};
// 从PageCache获取Span(关键逻辑)
Span* page_cache_get_span(PageCache* cache, size_t required_pages) {
SpinLockAcquire(&cache->lock);
// 1. 查找空闲链表中是否有足够页数的Span
Span* current = cache->free_spans;
while (current) {
if (current->page_count >= required_pages) {
// 找到足够大的Span,分割为两部分(保留剩余页)
Span* allocated = current;
allocated->page_count = required_pages;
allocated->is_free = false;
// 剩余页形成新的Span(若有余)
Span* remaining = (Span*)((char*)current + required_pages * PAGE_SIZE);
remaining->start_page = current->start_page + required_pages;
remaining->page_count = current->page_count - required_pages;
remaining->is_free = true;
// 从空闲链表中移除当前Span
remove_span_from_free_list(cache, current);
// 若剩余页非零,加入空闲链表
if (remaining->page_count > 0) {
add_span_to_free_list(cache, remaining);
}
SpinLockRelease(&cache->lock);
return allocated;
}
current = current->next;
}
// 2. 本地无足够空闲页,向操作系统申请新内存(原有内容优化)
size_t new_pages = required_pages * 2; // 超量申请(减少未来系统调用)
PageID new_start_page = (PageID)mmap(
NULL,
new_pages * PAGE_SIZE,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1,
0
);
if (new_start_page == MAP_FAILED) {
SpinLockRelease(&cache->lock);
return NULL; // 申请失败
}
// 创建新Span并加入空闲链表
Span* new_span = (Span*)malloc(sizeof(Span));
new_span->start_page = new_start_page;
new_span->page_count = new_pages;
new_span->is_free = true;
add_span_to_free_list(cache, new_span);
// 重新查找并分配(此时新Span已在空闲链表)
return page_cache_get_span(cache, required_pages);
}mmap;以分配一个2KB对象为例,三级协作的完整流程如下(结合哈希桶索引计算):
mmap申请8KB页内存(超量申请),分割出4KB Span后返回给CentralCache。
释放时,流程反向:
munmap归还部分页内存给操作系统。高并发内存池框架通过ThreadCache(本地无锁)、CentralCache(全局协调)、PageCache(系统桥梁)的三级架构,实现了:
未来优化方向包括: