在C++高性能服务开发中,内存管理一直是影响系统性能的关键因素。传统的内存分配器(如malloc/free)在面对高并发场景时往往表现不佳,频繁的内存申请和释放会导致锁竞争加剧、内存碎片增多等问题。这正是我们开发高并发内存池的初衷——通过精细化的内存管理策略,为多线程环境提供高效、稳定的内存分配服务。
这个实战项目的核心在于构建一个三级内存管理架构:ThreadCache(线程局部缓存)、CentralCache(中心缓存)和PageCache(页缓存)。其中PageCache作为整个内存池的基石,负责大块内存的分配与回收,以及不同规格内存块之间的平衡调度。今天我们要重点拆解的,就是PageCache模块的具体实现方案。
PageCache的核心思想源自操作系统的页式内存管理。我们将物理内存划分为固定大小的页(通常为4KB),所有的内存分配都以页为单位进行。这种设计带来几个显著优势:
在实现层面,我们需要维护一个页空闲链表(free list),记录当前可用的内存页。当ThreadCache或CentralCache需要内存时,PageCache从空闲链表中分配;当内存被释放时,再将其归还到空闲链表。
Span是PageCache中最重要的数据结构,它描述了一段连续的页内存。每个Span包含以下关键信息:
cpp复制struct Span {
PAGE_ID pageId_; // 起始页号
size_t n_; // 页数量
Span* next_; // 双向链表指针
Span* prev_;
void* freeList_; // 切分后的小块内存链表
size_t useCount_; // 被使用计数
size_t objSize_; // 切分后的对象大小
};
Span的管理有几个技术要点:
PageCache使用一个哈希桶来组织不同大小的Span,这是典型的分离适配(segregated fit)策略:
cpp复制class PageCache {
private:
SpanList spanLists_[NPAGES]; // 哈希桶数组
static PageCache* instance_; // 单例模式实例
std::mutex mutex_; // 全局锁
};
其中NPAGES定义了最大支持的页数(如128表示最大支持128*4KB=512KB的内存申请)。每个桶中存放的是大小相近的Span链表,例如:
当CentralCache申请内存时,PageCache的执行流程如下:
cpp复制Span* PageCache::AllocSpan(size_t n) {
assert(n < NPAGES);
// 先尝试从对应大小的桶中获取
if (!spanLists_[n].Empty()) {
return spanLists_[n].PopFront();
}
// 没有则找更大的Span拆分
for (size_t i = n + 1; i < NPAGES; ++i) {
if (!spanLists_[i].Empty()) {
Span* bigSpan = spanLists_[i].PopFront();
Span* newSpan = new Span;
// 拆分后半部分
newSpan->pageId_ = bigSpan->pageId_ + n;
newSpan->n_ = i - n;
// 剩余部分放回桶中
bigSpan->n_ = n;
spanLists_[newSpan->n_].PushFront(newSpan);
return bigSpan;
}
}
// 连大Span都没有,直接向系统申请
Span* span = new Span;
void* ptr = SystemAlloc(n);
span->pageId_ = (PAGE_ID)ptr >> PAGE_SHIFT;
span->n_ = n;
return span;
}
当CentralCache归还内存时,PageCache需要处理可能的Span合并:
cpp复制void PageCache::ReleaseSpan(Span* span) {
// 向前合并
PAGE_ID prevId = span->pageId_ - 1;
auto prevIt = idSpanMap_.find(prevId);
if (prevIt != idSpanMap_.end() && !prevIt->second->inUse_) {
Span* prev = prevIt->second;
spanLists_[prev->n_].Erase(prev);
prev->n_ += span->n_;
delete span;
span = prev;
}
// 向后合并
PAGE_ID nextId = span->pageId_ + span->n_;
auto nextIt = idSpanMap_.find(nextId);
if (nextIt != idSpanMap_.end() && !nextIt->second->inUse_) {
Span* next = nextIt->second;
spanLists_[next->n_].Erase(next);
span->n_ += next->n_;
delete next;
}
// 合并后的Span放回对应桶中
spanLists_[span->n_].PushFront(span);
span->inUse_ = false;
}
为了实现快速查找,我们需要维护一个页号到Span的映射表。这里使用基数树(Radix Tree)来优化查找性能:
cpp复制class PageCache {
private:
// 使用三层基数树存储映射
Span** idSpanMap_[PAGE_MAP_SIZE];
Span* MapObjectToSpan(void* obj) {
PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;
Span* ret = (Span*)(idSpanMap_[id / (1024*1024)]
[(id % (1024*1024)) / 1024]
[id % 1024]);
return ret;
}
};
这种设计在64位系统下只需要3次内存访问就能完成查找,比哈希表更高效且无冲突。
PageCache作为全局资源,必须考虑多线程竞争问题。我们采用两种策略:
cpp复制Span* PageCache::AllocMoreSpans(size_t size, size_t batch) {
std::unique_lock<std::mutex> lock(mutex_);
Span* head = nullptr;
Span* tail = nullptr;
size_t allocated = 0;
while (allocated < batch) {
Span* span = AllocSpan(size);
if (!head) head = span;
if (tail) tail->next_ = span;
tail = span;
allocated++;
}
return head;
}
为了避免频繁的系统调用,可以在初始化时预分配一定数量的Span:
cpp复制void PageCache::Init() {
for (size_t i = 1; i <= INIT_PAGES; ++i) {
Span* span = new Span;
void* ptr = SystemAlloc(i);
span->pageId_ = (PAGE_ID)ptr >> PAGE_SHIFT;
span->n_ = i;
spanLists_[i].PushFront(span);
}
}
实现定期回收机制,避免内存长期闲置:
cpp复制void PageCache::PeriodicRelease() {
static size_t releaseInterval = 0;
if (++releaseInterval < RELEASE_INTERVAL) return;
std::unique_lock<std::mutex> lock(mutex_);
for (size_t i = 1; i < NPAGES; ++i) {
if (spanLists_[i].Size() > MAX_CACHE_PAGES) {
Span* span = spanLists_[i].PopFront();
SystemFree(span);
}
}
releaseInterval = 0;
}
现象:长期运行后,系统内存充足但分配失败
解决方案:
现象:锁竞争导致性能下降
优化方案:
现象:申请超大内存(如1GB)时效率低
处理策略:
我们在不同场景下对比了自制内存池与glibc malloc的性能:
| 测试场景 | malloc耗时(ms) | 内存池耗时(ms) | 提升幅度 |
|---|---|---|---|
| 单线程小对象 | 152 | 48 | 316% |
| 多线程小对象 | 643 | 112 | 574% |
| 大对象随机分配 | 287 | 203 | 141% |
| 长期运行碎片测试 | 逐渐升高 | 保持稳定 | - |
关键发现:
现代服务器往往配备多种内存(如DDR4 + PMem),可以扩展PageCache以支持:
针对多CPU架构:
cpp复制class NumaPageCache {
private:
PageCache caches_[MAX_NUMA_NODES];
Span* AllocSpan(size_t n, int node) {
return caches_[node].AllocSpan(n);
}
};
对于低频访问的内存页:
实现这个高并发内存池的PageCache模块后,我们的内存分配性能得到了显著提升。特别是在Web服务器、游戏服务器等场景下,实测QPS提升可达30%以上。这充分证明了精细化的内存管理在现代C++开发中的重要性。