首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >高并发内存池框架设计

高并发内存池框架设计

作者头像
小文要打代码
发布2025-07-10 10:58:58
发布2025-07-10 10:58:58
1970
举报
文章被收录于专栏:C++学习历程C++学习历程

项目源码:https://gitee.com/kkkred/thread-caching-malloc

引言

在高并发场景(如百万QPS的API网关、实时游戏服务器)中,内存分配的延迟与碎片是性能瓶颈的核心来源。传统单内存池因全局锁竞争和内存碎片问题,难以满足高吞吐需求。本文将深入解析​​分层内存池框架​​的核心组件设计,重点聚焦ThreadCache的哈希桶映射规则、TLS无锁访问机制、CentralCache的Span结构与双链表管理,以及PageCache的Span获取逻辑,揭示高并发场景下内存分配的「无锁化」与「高效协同」之道。其中,ThreadCache的哈希桶索引计算是实现「高效分类、均匀分布」的关键环节,本文将详细展开其计算逻辑与实现细节。


一、ThreadCache:线程本地的「无锁高速仓库」

1.1 哈希桶映射与对齐规则

ThreadCache的核心目标是为每个线程提供​​本地化、无锁​​的内存分配服务,其关键设计是​​基于对象大小的哈希桶映射​​。

1.1.1 对象大小分类与对齐规则

内存对象按大小划分为多个「类」(Size Class),每个类对应一个哈希桶。分类规则需满足:

  • ​对齐到2的幂次​​:对象大小向上取整为2的幂次(如8B→8B,10B→16B,128B→128B),避免跨缓存行访问;
  • ​覆盖高频场景​​:常见对象大小(如8B、16B、32B、64B、128B、256B、512B、1KB、2KB、4KB)单独分类,减少哈希冲突。
1.1.2 哈希桶索引计算

哈希桶索引的计算分为三个阶段:​​对象大小对齐​​→​​Size Class确定​​→​​桶索引映射​​,确保同类对象集中存储,同时避免哈希冲突。

阶段1:对象大小对齐

通过位运算将原始大小向上取整到最近的2的幂次:

代码语言:javascript
复制
// 计算对齐后的大小(向上取整到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;
}
阶段2:Size Class确定

将所有可能的对齐后大小划分为预定义的「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

页级内存块、大对象

​代码实现(分类逻辑)​​:

代码语言:javascript
复制
// 定义最大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)
}
阶段3:桶索引映射

哈希桶的下标直接使用Size Class的索引,确保每个Size Class对应唯一的桶。这种设计避免了额外的哈希计算,将索引计算复杂度降至O(1)。

​示例​​:

  • 对象大小=10B → 对齐后=16B → Size Class索引=1 → 哈希桶下标=1;
  • 对象大小=512B → 对齐后=512B → Size Class索引=6 → 哈希桶下标=6;
  • 对象大小=8KB(超过最大Size Class)→ 对齐后=8192B → 映射到Size Class索引=9(对应4KB)→ 哈希桶下标=9。

1.2 TLS无锁访问机制

为避免多线程竞争,ThreadCache通过​​线程本地存储(TLS, Thread-Local Storage)​​实现「线程私有」的内存管理。每个线程独立拥有一个ThreadCache实例,无需全局锁。

1.2.1 TLS实例的创建与绑定

在Linux系统中,通过__thread关键字声明线程本地变量,确保每个线程拥有独立的ThreadCache实例:

代码语言:javascript
复制
// 线程本地存储的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;  // 初始化为空链表
        }
    }
}
1.2.2 无锁分配与释放流程(结合哈希桶索引)

分配与释放操作仅在当前线程的ThreadCache实例中进行,无需加锁。分配时通过哈希桶索引快速定位空闲链表:

代码语言:javascript
复制
// 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:全局共享的「Span协调中枢」

CentralCache是全局共享的内存池,负责管理ThreadCache无法直接处理的中大对象(>1KB),并通过​​Span结构​​和​​双链表​​实现高效的内存块管理。

2.1 页号类型与Span结构设计
2.1.1 页号类型(Page ID)

为避免直接使用虚拟地址(可能跨页),CentralCache将内存块按「页」为单位管理(系统页大小通常为4KB)。页号(Page ID)定义为:

代码语言:javascript
复制
// 页号类型(使用uintptr_t保证跨平台兼容性)
using PageID = uintptr_t;

每个Page ID对应一个4KB的虚拟页,通过页号可快速计算虚拟地址:

代码语言:javascript
复制
// 虚拟地址 = 页号 × 页大小
void* page_to_addr(PageID page_id) {
    return (void*)(page_id * PAGE_SIZE);
}
2.1.2 Span结构:内存块的元数据(原有内容优化)

Span是CentralCache管理内存块的基本单元,记录内存块的起始页号、总页数、状态(空闲/已分配)等信息:

代码语言:javascript
复制
// Span结构体(管理连续页的内存块)
struct Span {
    PageID start_page;    // 起始页号
    size_t page_count;    // 总页数(如1页=4KB,2页=8KB)
    Span* prev;           // 双链表前驱指针
    Span* next;           // 双链表后继指针
    bool is_free;         // 是否空闲
    // 扩展字段(如对象大小分类、引用计数等)
};
  • ​start_page​​:内存块的起始页号(通过page_to_addr可转换为虚拟地址);
  • ​page_count​​:连续页的数量(如申请8KB内存对应2页);
  • ​prev/next​​:双链表指针,用于快速插入/删除空闲Span;
  • ​is_free​​:标记内存块是否空闲,避免重复分配。
2.2 双链表管理空闲Span

CentralCache通过​​双链表​​管理所有空闲Span,支持O(1)时间的插入、删除和合并操作。

2.2.1 空闲链表的结构
代码语言:javascript
复制
// CentralCache结构体
struct CentralCache {
    SpinLock lock;               // 自旋锁(保护全局操作)
    Span* free_spans[MAX_PAGE_COUNT];  // 按页数分类的空闲链表(如1页、2页、4页...)
    size_t total_pages;          // 总管理的页数
    size_t used_pages;           // 已使用的页数
};
  • ​按页数分类​​:空闲链表按Span的页数(如1页、2页、4页)分类存储,分配时优先匹配最接近的页数,减少碎片;
  • ​双链表操作​​:插入/删除Span时,仅需修改前驱和后继指针,无需遍历链表。
2.2.2 Span的合并与分割

当释放一个Span时,CentralCache会检查相邻的Span是否空闲,若相邻则合并为更大的Span,避免内存碎片:

代码语言:javascript
复制
// 合并相邻的空闲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是内存池与操作系统交互的接口,负责管理大内存块(页级及以上),通过​​虚拟内存映射​​和​​延迟释放​​减少系统调用次数。

3.1 PageCache的核心功能

PageCache的核心目标是:

  • ​减少系统调用​​:批量申请/释放页内存,避免频繁调用mmap/munmap
  • ​管理空闲页​​:维护空闲页列表,按需向操作系统申请或归还内存。
3.2 从PageCache获取Span的流程

当CentralCache需要分配大内存块时,会向PageCache申请连续的页内存(Span)。以下是关键步骤:

3.2.1 检查本地空闲页

PageCache首先检查自身的空闲页列表,是否有足够的连续页:

代码语言:javascript
复制
// 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);
}
3.2.2 超量申请与延迟释放
  • ​超量申请​​:为减少未来系统调用,PageCache会超量申请页内存(如申请2倍于需求的页数),避免频繁调用mmap
  • ​延迟释放​​:空闲页不会立即归还给操作系统,而是保留一段时间(如5分钟),通过定时任务清理长期空闲的页。

四、三级协作:从分配到释放的完整链路

以分配一个2KB对象为例,三级协作的完整流程如下(结合哈希桶索引计算):

  1. ​ThreadCache本地分配​​: 线程首先检查本地ThreadCache的2KB Size Class链表(哈希桶索引为8)。若链表非空,直接取出空闲对象(O(1)时间)。
  2. ​CentralCache全局协调​​: 若ThreadCache无空闲对象,向CentralCache申请2KB内存。CentralCache检查自身空闲Span列表,找到一个4KB的Span(包含两个2KB块),分割出1个2KB块分配给ThreadCache,剩余2KB块保留在CentralCache中。
  3. ​PageCache系统级申请​​: 若CentralCache无足够空闲Span,向PageCache申请4KB页内存。PageCache检查空闲页列表,若不足则调用mmap申请8KB页内存(超量申请),分割出4KB Span后返回给CentralCache。

释放时,流程反向:

  1. 对象回到ThreadCache的2KB链表;
  2. 若ThreadCache链表已满(如达到16个对象),批量回收到CentralCache;
  3. CentralCache将回收的2KB块合并到空闲Span中,若Span总页数超过阈值(如32页),批量回收到PageCache;
  4. PageCache若空闲页过多,调用munmap归还部分页内存给操作系统。

五、总结与性能优化

高并发内存池框架通过​​ThreadCache(本地无锁)、CentralCache(全局协调)、PageCache(系统桥梁)​​的三级架构,实现了:

  • ​低延迟​​:ThreadCache的无锁操作+哈希桶索引的O(1)计算,将分配/释放延迟降至O(1);
  • ​低碎片​​:按Size Class分类+页级对齐的设计,减少内存碎片;
  • ​高并发​​:线程本地存储避免了全局锁竞争,分段锁/双链表优化了多线程协作。

未来优化方向包括:

  • ​NUMA感知​​:根据CPU节点分配本地内存,减少跨节点访问延迟;
  • ​智能预分配​​:基于历史分配模式预分配内存,减少动态调整开销;
  • ​无锁化升级​​:通过CAS或事务内存实现全局无锁操作,进一步提升并发能力。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-06-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
    • 一、ThreadCache:线程本地的「无锁高速仓库」
      • 1.1 哈希桶映射与对齐规则
      • 1.2 TLS无锁访问机制
    • 二、CentralCache:全局共享的「Span协调中枢」
      • 2.1 页号类型与Span结构设计
      • 2.2 双链表管理空闲Span
    • 三、PageCache:系统级的「页内存桥梁」
      • 3.1 PageCache的核心功能
      • 3.2 从PageCache获取Span的流程
    • 四、三级协作:从分配到释放的完整链路
    • 五、总结与性能优化
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档