在高并发内存池的设计中,"大页内存管理"和"元数据开销优化"是两个核心痛点:原生 malloc/free 在大内存分配时频繁触发系统调用,而 new/delete 管理内存池元对象(如 ThreadCache、span)会引入额外性能损耗。本文基于 TCMalloc 思想,拆解高并发内存池中大页内存的申请/释放逻辑,以及如何通过**定长内存池(ObjectPool)**彻底脱离 new/delete,实现元数据的零开销管理。
一、背景:为什么要单独处理大页内存?
在高并发内存池的三级缓存架构(ThreadCache→CentralCache→PageCache)中,我们将内存分为"小对象(≤256KB)"和"大对象(>256KB)":
- 小对象:走三级缓存,利用线程私有、桶锁、批量分配降低竞争
- 大对象:若仍走缓存,会导致"缓存污染"(大内存占满缓存,小对象无空间可用),且大内存分配频率低,缓存收益有限
因此,大对象直接走 PageCache 申请连续物理页(大页),跳过 ThreadCache 和 CentralCache,核心目标是:
- 减少系统调用次数(批量申请连续页,而非单次小内存)
- 避免大内存碎片化(用 span 管理连续页块,空闲后自动合并)
- 保证高并发下的线程安全(全局页锁 + 细粒度控制)
二、核心 1:大页内存的申请与释放实现
2.1 大页内存的定义与申请策略
(1)大页判定规则
我们定义"大对象"为超过 256KB 的内存(可根据场景调整),对应物理页数量为:
// Common.h 核心宏定义
#define MAX_BYTES (256 * 1024) // 小对象上限
#define PAGE_SIZE 8192 // 单页 8KB
#define PAGE_SHIFT 13 // 2^13=8192,用于页 ID 与地址转换
#define MAX_PAGE_BUCKETS 128 // 最大连续页数量(128*8KB=1024KB)
typedef size_t PAGE_ID; // 页 ID 类型
当申请内存 size > MAX_BYTES 时,判定为大对象,直接走 PageCache 申请连续页(大页),而非三级缓存。
(2)大页内存申请核心逻辑
大页申请的核心是"以页为单位分配连续物理页",避免原生 malloc 的碎片化问题,核心代码如下:
// ConcurrentAlloc.h - 用户层大对象申请接口
void* ConcurrentAlloc(size_t size) {
if (size > MAX_BYTES) {
// 1. 内存对齐:大对象对齐到页大小,避免跨页浪费
size_t alignSize = SizeClass::RoundUp(size);
// 2. 计算需要的连续页数
size_t kPages = alignSize >> PAGE_SHIFT;
// 3. 加全局页锁,保证线程安全
PageCache::GetInstance()->_pageMutex.lock();
// 4. 向 PageCache 申请连续 kPages 页的 span(大页)
span* bigSpan = PageCache::GetInstance()->NewSpan(kPages);
PageCache::GetInstance()->_pageMutex.unlock();
// 5. 页 ID 转换为内存地址(核心:页 ID * 页大小 = 起始地址)
uintptr_t addr = (uintptr_t)bigSpan->_pageId * PAGE_SIZE;
return (void*)addr;
}
// 小对象走 ThreadCache...
}
(3)PageCache::NewSpan 大页申请核心
NewSpan 是大页申请的核心,逻辑为"先查空闲 span→无则向系统申请连续页":
// PageCache.cpp
span* PageCache::NewSpan(size_t kPages) {
// 1. 优先从对应页数的空闲 span 链表中取
if (!_spanlist[kPages].Empty()) {
span* span = _spanlist[kPages].Pop_front();
// 初始化页 ID 到 span 的映射(仅首尾页,高效)
_idSpanMap[span->_pageId] = span;
_idSpanMap[span->_pageId + span->_n - 1] = span;
span->_isUse = true;
return span;
}
// 2. 无空闲 span,向更大页数的 span 链表找(拆分)
for (size_t i = kPages + 1; i < MAX_PAGE_BUCKETS; ++i) {
if (!_spanlist[i].Empty()) {
span* bigSpan = _spanlist[i].Pop_front();
// 拆分出 kPages 页的 span
span* newSpan = new span;
newSpan->_pageId = bigSpan->_pageId;
newSpan->_n = kPages;
newSpan->_isUse = true;
// 剩余页放回 PageCache
bigSpan->_pageId += kPages;
bigSpan->_n -= kPages;
_spanlist[bigSpan->_n].Push_front(bigSpan);
// 更新映射
_idSpanMap[newSpan->_pageId] = newSpan;
_idSpanMap[newSpan->_pageId + newSpan->_n - 1] = newSpan;
_idSpanMap[bigSpan->_pageId] = bigSpan;
_idSpanMap[bigSpan->_pageId + bigSpan->_n - 1] = bigSpan;
return newSpan;
}
}
// 3. 无任何空闲 span,向系统申请 128 页(大页)
size_t allocPages = MAX_PAGE_BUCKETS - 1;
void* ptr = SystemAlloc(allocPages); // 封装 VirtualAlloc/mmap
span* newSpan = new span;
newSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
newSpan->_n = allocPages;
_spanlist[allocPages].(newSpan);
(kPages);
}
2.2 大页内存的释放逻辑
大页释放的核心是"归还 span 到 PageCache + 双向合并空闲 span",避免内存碎片化,核心代码:
// ConcurrentAlloc.h - 用户层大对象释放接口
void ConcurrentFree(void* ptr, size_t size) {
assert(ptr);
if (size > MAX_BYTES) {
// 1. 通过地址反查对应的 span(核心:_idSpanMap 映射)
PageCache::GetInstance()->_pageMutex.lock();
span* bigSpan = PageCache::GetInstance()->MapObjectToSpan(ptr);
// 2. 归还 span 到 PageCache 并合并
PageCache::GetInstance()->ReleaseSpanToPageCache(bigSpan);
PageCache::GetInstance()->_pageMutex.unlock();
return;
}
// 小对象走 ThreadCache...
}
// PageCache.cpp - span 合并核心
void PageCache::ReleaseSpanToPageCache(span* obj) {
// 1. 前向合并:合并前一页的空闲 span
while (true) {
PAGE_ID prevId = obj->_pageId - 1;
auto it = _idSpanMap.find(prevId);
if (it == _idSpanMap.end()) break;
span* prevSpan = it->second;
if (prevSpan->_isUse || prevSpan->_n + obj->_n > MAX_PAGE_BUCKETS - 1) break;
// 合并 prevSpan 到当前 span
_spanlist[prevSpan->_n].Erase(prevSpan);
obj->_pageId = prevSpan->_pageId;
obj->_n += prevSpan->_n;
delete prevSpan; // 此处后续用定长内存池优化
}
// 2. 后向合并:合并后一页的空闲 span(逻辑同上)
() {
PAGE_ID nextId = obj->_pageId + obj->_n;
it = _idSpanMap.(nextId);
(it == _idSpanMap.()) ;
span* nextSpan = it->second;
(nextSpan->_isUse || nextSpan->_n + obj->_n > MAX_PAGE_BUCKETS - ) ;
_spanlist[nextSpan->_n].(nextSpan);
obj->_n += nextSpan->_n;
nextSpan;
}
obj->_isUse = ;
_spanlist[obj->_n].(obj);
_idSpanMap[obj->_pageId] = obj;
_idSpanMap[obj->_pageId + obj->_n - ] = obj;
}
2.3 大页内存管理的核心优势
- 低碎片化:连续页块通过 span 合并,避免原生 malloc 的"内存洞";
- 少系统调用:向系统一次申请 128 页,拆分后复用,大幅减少 VirtualAlloc/mmap 调用;
- 高并发安全:全局页锁仅在申请/释放大页时持有,持有时间极短,不影响小对象并发;
- 地址映射高效:仅维护 span 首尾页的映射,而非全量页,哈希表开销降低 90%+。
三、核心 2:定长内存池(ObjectPool)脱离 new/delete
上述代码中,new span 和 delete span 会引入额外开销:new 需要调用构造函数 + 内存分配,delete 需要调用析构函数 + 内存释放,而 span 作为内存池的元对象,创建/销毁频率极高,必须优化。此外,核心目标是替换 malloc 和 new,若内部仍依赖 new/delete,则违背设计初衷。
3.1 定长内存池的设计思路
定长内存池(ObjectPool)的核心是"预分配一块连续内存,按固定大小切割,管理空闲链表",适用于:
- 频繁创建/销毁的定长对象(如 span、ThreadCache)
- 避免 new/delete 的系统调用和锁竞争
- 支持定位 new 和显式析构,兼容 C++ 对象生命周期
3.2 ObjectPool 核心实现
// Fixed-size_memory_pool.h
template<class T>
class ObjectPool {
public:
// 申请对象(脱离 new)
T* New() {
T* obj = nullptr;
// 1. 优先从空闲链表取
if (_freeList) {
obj = (T*)_freeList;
_freeList = *((void**)_freeList); // 头删
} else {
// 2. 空闲链表空,向系统申请大块内存
if (_leftBytes < sizeof(T)) {
_leftBytes = 128 * 1024; // 预分配 128KB
// 按页申请,避免内存碎片
size_t pageNum = _leftBytes / PAGE_SIZE;
_memory = (char*)SystemAlloc(pageNum);
if (_memory == nullptr) throw std::bad_alloc();
}
// 切割内存块
obj = (T*)_memory;
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += objSize;
_leftBytes -= objSize;
}
// 3. 定位 new:仅初始化对象,不分配内存(核心)
new(obj)T();
return obj;
}
// 释放对象(脱离 delete)
void Delete {
obj->~();
objSize = (T) < (*) ? (*) : (T);
*((**)obj) = _freeList;
_freeList = obj;
}
:
* _memory = ;
_leftBytes = ;
* _freeList = ;
};
3.3 替换 new/delete:以 span 和 ThreadCache 为例
(1)管理 span 对象
将 PageCache 中所有 new span/delete span 替换为 ObjectPool:
// PageCache.h
class PageCache {
private:
ObjectPool<span> _spanPool; // 定长内存池管理 span
public:
span* NewSpan(size_t kPages) {
// 替换 new span
span* newSpan = _spanPool.New();
// ... 其他逻辑
}
void ReleaseSpanToPageCache(span* obj) {
// ... 合并逻辑
// 替换 delete span
_spanPool.Delete(prevSpan);
_spanPool.Delete(nextSpan);
}
};
(2)管理 ThreadCache 对象
ThreadCache 是线程私有对象,创建频率高,用 ObjectPool 管理:
// ConcurrentAlloc.h
static __thread ThreadCache* pTLSThreadCache = nullptr;
void* ConcurrentAlloc(size_t size) {
if (size <= MAX_BYTES) {
if (pTLSThreadCache == nullptr) {
// 替换 new ThreadCache
static ObjectPool<ThreadCache> tcPool;
pTLSThreadCache = tcPool.New();
}
return pTLSThreadCache->Allocate(size);
}
// ... 大对象逻辑
}
3.4 定长内存池的核心收益
- 零开销创建/销毁:空闲对象直接从链表取,无需系统调用,速度比 new 快;
- 内存连续:预分配的大块内存减少 TLB 缺失,访问效率更高;
- 对象生命周期可控:定位 new + 显式析构,既兼容 C++ 对象,又脱离 new/delete 的束缚;
- 无锁(线程私有):每个 ThreadCache 的 ObjectPool 是线程私有,无并发竞争。
四、整体性能对比
| 场景 | 原生 malloc/free + new/delete | 高并发内存池(大页 + 定长池) |
|---|---|---|
| 大对象分配耗时 | 高(频繁系统调用) | 低(批量申请 + span 复用) |
| 元对象创建耗时 | 高(new/delete 锁竞争) | 极低(空闲链表无锁) |
| 内存碎片率 | 高(零散分配) | 低(span 合并 + 定长切割) |
| 多线程并发吞吐量 | 低(全局锁竞争) | 高(桶锁 + 线程私有) |
五、总结
高并发内存池的性能优化,本质是"场景适配 + 开销极致压缩":
- 大页内存管理:通过 span 抽象连续页块,批量申请、双向合并,解决大对象碎片化和系统调用频繁问题
- 定长内存池:针对高频创建的元对象,预分配连续内存、管理空闲链表,彻底脱离 new/delete,实现零开销对象管理
这两个设计的结合,让内存池既保证了大内存的高效管理,又解决了元数据的性能损耗,是 TCMalloc 等工业级内存池的核心精髓,也是高并发场景下内存管理的最优解之一。
附:核心注意事项
- 大页合并时仅维护首尾页映射,兼顾效率和正确
- 定长内存池的对象大小需对齐到 void*,避免越界访问
- 线程私有 ObjectPool 需配合 TLS 使用,避免并发竞争
- 向系统申请的大页内存,需在进程退出时主动释放(封装 SystemFree)

