7.高并发内存池大页内存申请释放以及使用定长内存池脱离new

在高并发内存池的设计中,“大页内存管理” 和 “元数据开销优化” 是两个核心痛点:原生malloc/free在大内存分配时频繁触发系统调用,而new/delete管理内存池元对象(如ThreadCachespan)会引入额外性能损耗。本文基于 TCMalloc 思想,拆解高并发内存池中大页内存的申请 / 释放逻辑,以及如何通过定长内存池(ObjectPool) 彻底脱离new/delete,实现元数据的零开销管理

一、背景:为什么要单独处理大页内存?

在高并发内存池的三级缓存架构(ThreadCache→CentralCache→PageCache)中,我们将内存分为 “小对象(≤256KB)” 和 “大对象(>256KB)”:

  • 小对象:走三级缓存,利用线程私有、桶锁、批量分配降低竞争
  • 大对象:若仍走缓存,会导致 “缓存污染”(大内存占满缓存,小对象无空间可用),且大内存分配频率低,缓存收益有限

因此,大对象直接走PageCache申请连续物理页(大页),跳过ThreadCacheCentralCache,核心目标是:

  1. 减少系统调用次数(批量申请连续页,而非单次小内存)
  2. 避免大内存碎片化(用span管理连续页块,空闲后自动合并)
  3. 保证高并发下的线程安全(全局页锁 + 细粒度控制)

二、核心 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; // 地址转页ID newSpan->_n = allocPages; _spanlist[allocPages].Push_front(newSpan); // 递归拆分(仅1层,无栈溢出风险) return 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(逻辑同上) while (true) { PAGE_ID nextId = obj->_pageId + obj->_n; auto it = _idSpanMap.find(nextId); if (it == _idSpanMap.end()) break; span* nextSpan = it->second; if (nextSpan->_isUse || nextSpan->_n + obj->_n > MAX_PAGE_BUCKETS - 1) break; _spanlist[nextSpan->_n].Erase(nextSpan); obj->_n += nextSpan->_n; delete nextSpan; // 此处后续用定长内存池优化 } // 3. 标记为空闲,更新首尾页映射(高效设计) obj->_isUse = false; _spanlist[obj->_n].Push_front(obj); _idSpanMap[obj->_pageId] = obj; _idSpanMap[obj->_pageId + obj->_n - 1] = obj; } 

2.3 大页内存管理的核心优势

  1. 低碎片化:连续页块通过 span 合并,避免原生 malloc 的 “内存洞”;
  2. 少系统调用:向系统一次申请 128 页,拆分后复用,大幅减少 VirtualAlloc/mmap 调用;
  3. 高并发安全:全局页锁仅在申请 / 释放大页时持有,持有时间极短,不影响小对象并发;
  4. 地址映射高效:仅维护 span 首尾页的映射,而非全量页,哈希表开销降低 90%+。

三、核心 2:定长内存池(ObjectPool)脱离 new/delete

上述代码中,new spandelete span会引入额外开销:new需要调用构造函数 + 内存分配,delete需要调用析构函数 + 内存释放,而span作为内存池的元对象,创建 / 销毁频率极高,必须优化,而且最重要的是,我们写的这个不就是要替换malloc和new吗,要是代码中还使用的话,不就相当于左脚踩右脚了吗!!!

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(T* obj) { // 1. 显式析构:仅清理对象,不释放内存 obj->~T(); // 2. 头插到空闲链表 size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T); *((void**)obj) = _freeList; _freeList = obj; } private: char* _memory = nullptr; // 预分配的连续内存块 int _leftBytes = 0; // 剩余可用字节数 void* _freeList = nullptr; // 空闲对象链表 }; 

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 定长内存池的核心收益

  1. 零开销创建 / 销毁:空闲对象直接从链表取,无需系统调用,速度比 new
  2. 内存连续:预分配的大块内存减少 TLB 缺失,访问效率更高
  3. 对象生命周期可控:定位 new + 显式析构,既兼容 C++ 对象,又脱离 new/delete 的束缚;
  4. 无锁(线程私有):每个 ThreadCache 的 ObjectPool 是线程私有,无并发竞争

四、整体性能对比

场景原生 malloc/free + new/delete高并发内存池(大页 + 定长池)
大对象分配耗时高(频繁系统调用)低(批量申请 + span 复用)
元对象创建耗时高(new/delete 锁竞争)极低(空闲链表无锁)
内存碎片率高(零散分配)低(span 合并 + 定长切割)
多线程并发吞吐量低(全局锁竞争)高(桶锁 + 线程私有)

五、总结

高并发内存池的性能优化,本质是 “场景适配 + 开销极致压缩”:

  1. 大页内存管理:通过 span 抽象连续页块,批量申请、双向合并,解决大对象碎片化和系统调用频繁问题
  2. 定长内存池:针对高频创建的元对象,预分配连续内存、管理空闲链表,彻底脱离 new/delete,实现零开销对象管理

这两个设计的结合,让内存池既保证了大内存的高效管理,又解决了元数据的性能损耗,是 TCMalloc 等工业级内存池的核心精髓,也是高并发场景下内存管理的最优解之一

附:核心注意事项

  1. 大页合并时仅维护首尾页映射,兼顾效率和正确
  2. 定长内存池的对象大小需对齐到 void*,避免越界访问
  3. 线程私有 ObjectPool 需配合 TLS 使用,避免并发竞争
  4. 向系统申请的大页内存,需在进程退出时主动释放(封装 SystemFree)

Read more

【Trae】如何使用Trae编译C++(附带MinGW)

【Trae】如何使用Trae编译C++(附带MinGW)

结果 先看结果 这是一道回文串题目,F5编译,控制台输入输出。 下载 * 下载Trae编译器,直接去官网下:https://www.trae.cn/ * 下载VSCode编译器,这个相信用Trae的各位都不陌生。https://code.visualstudio.com/ * 查看你的C盘,C:\Users\Administrator\.vscode\extensions路径下是否有extensions.json文件,如果没有,请将VSCode卸载后重装。 安装插件 在VSCode里安装“C/C++”插件。 这里需要在VSCode里安装的原因是C/C++插件在Trae里是搜不到的。 再次查看C:\Users\Administrator\.vscode\extensions路径,看看插件是否安装成功,且extensions.json文件里有这个插件。 关闭VSCode,打开Trae。 点击右上角头像-IDE设置 选择“从VSCode导入”,等待它导入插件,导入完成后点击查看一下,

By Ne0inhk
初学二叉搜索树踩坑多?C++ 从原理到代码,搞定增删查全流程

初学二叉搜索树踩坑多?C++ 从原理到代码,搞定增删查全流程

🎬 个人主页:Vect个人主页 🎬 GitHub:Vect的代码仓库 🔥 个人专栏: 《数据结构与算法》《C++学习之旅》《计算机基础》 ⛺️Per aspera ad astra. 文章目录 * 1. 二叉搜索树相关概念 * 2. 二叉搜索树的操作 * 2.1. 查找节点 * 2.2. 插入节点 * 2.3. 删除节点 * 3. 二叉搜索树的实现 * 4. 二叉搜索树的应用 * 4.1. K模型 * 4.2. KV模型 1. 二叉搜索树相关概念 如下图所示,二叉搜索树(binary search tree)满足下列条件: 1. 对于根节点,左子树中所有节点的值<根节点的值&

By Ne0inhk
【C++】二叉搜索树深拷贝的致命陷阱:如何用前序遍历解决90%程序员的内存崩溃难题

【C++】二叉搜索树深拷贝的致命陷阱:如何用前序遍历解决90%程序员的内存崩溃难题

【【C++】二叉搜索树深拷贝的致命陷阱:如何用前序遍历解决90%程序员的内存崩溃难题 * 摘要 * 目录 * 一、key结构的默认成员函数 * 1. 拷贝构造函数 * 2. 赋值运算符重载函数 * 3. 析构函数 * 二、二叉搜索树key结构和key/val结构使用场景 * 三、key/val结构的模拟实现以及和key结构的对比 * 总结 摘要 本文以 “Key 结构→KeyValue 结构” 为演进主线,完整实现了两种结构的非递归与递归操作(插入、查找、删除),并针对默认成员函数(拷贝构造、赋值运算符重载、析构)的深拷贝需求,设计了基于前序遍历的拷贝逻辑、“拷贝 - 交换” 的赋值技法及后序遍历的销毁逻辑,同时结合 “小区车库车牌验证”“单词拼写检查”“中英互译字典” 等实际场景,清晰区分两种结构的适用范围,为 BST

By Ne0inhk
【C++修炼之路】类与对象实战:实现一个日期类

【C++修炼之路】类与对象实战:实现一个日期类

🏝️专栏: 【C++修炼之路】 🌅主页: f狐o狸x “于高山之巅,方见大河奔涌;于群峰之上,更觉长风浩荡”  目录 一、日期类的核心功能  二、日期类的定义 三、实现日期类比较大小 四、日期类加减 五、输入输出日期 六、附带功能         经过前面两篇文章的学习,相信聪明的你应该已经初步了解类与对象了,现在我们将一起实现一个日期类,进一步加深我面对类的理解。         在软件开发中,日期和时间的处理无处不在,从日程管理到金融计算,从数据分析到天气预报,日期类的设计都是开发者必须面对的挑战。在本文中,我们将从零开始,一步步实现一个功能完备的日期类。无论你是C++新手,还是想巩固面向对象编程基础,这个项目都会让你收获满满。 一、日期类的核心功能         想象一下:如果你要实现你手机里的日历这个app,它应该有些什么功能呢?         我认为主要功能如下:  日期合法性校验 日期加减(支持天数、月数、年数) 日期差计算

By Ne0inhk