1.C++-⾼性能内存池 1.C±⾼性能内存池1. 项⽬介绍这个项⽬做的是什么当前项⽬是实现⼀个⾼并发的内存池他的原型是google的⼀个开源项⽬tcmalloctcmalloc全称 Thread-Caching Malloc即线程缓存的malloc实现了⾼效的多线程内存管理⽤于替代系统的内 存分配相关的函数malloc、free。这个项⽬是把tcmalloc最核⼼的框架简化后拿出来模拟实现出⼀个⾃⼰的⾼并发内存池⽬的 就是学习tcamlloc的精华这种⽅式有点类似我们之前学习STL容器的⽅式。但是相⽐STL容器部分 tcmalloc的代码量和复杂度上升了很多tcmalloc源码gitee.com/mirrors/tcmalloc2. 什么是内存池2.1 池化技术所谓“池化技术”就是程序先向系统申请过量的资源然后⾃⼰管理以备不时之需。之所以要申 请过量的资源是因为每次申请该资源都有较⼤的开销不如提前申请好了这样使⽤时就会变得⾮ 常快捷⼤⼤提⾼程序运⾏效率。在计算机中有很多使⽤“池”这种技术的地⽅除了内存池还有连接池、线程池、对象池等。以 服务器上的线程池为例它的主要思想是先启动若⼲数量的线程让它们处于睡眠状态当接收到 客⼾端的请求时唤醒池中某个睡眠的线程让它来处理客⼾端的请求当处理完这个请求线程⼜ 进⼊睡眠状态。2.2 内存池内存池是指程序预先从操作系统申请⼀块⾜够⼤内存此后当程序中需要申请内存的时候不是直 接向操作系统申请⽽是直接从内存池中获取同理当程序释放内存的时候并不真正将内存返回 给操作系统⽽是返回内存池。当程序退出(或者特定时间)时内存池才将之前申请的内存真正释放。2.3 内存池主要解决的问题内存池主要解决的当然是效率的问题其次如果作为系统的内存分配器的⻆度还需要解决⼀下内存 碎⽚的问题。那么什么是内存碎⽚呢内存碎⽚分为外碎⽚和内碎⽚普通 malloc/free 频繁申请、释放小块内存会产生内存碎片内存池就是用来缓解这个问题。1、什么是外部碎片外碎片举个生活化例子你手里有一整条长条座位【占】【空】【占】【空】【占】【空】每一块空位单独看都不大。现在来了一组人想要连续 3 个空位。你数一数所有空位加起来总量是够 3 个位置的但是空位互相隔开、不连在一起凑不出一整块连续空间。放到内存里系统有很多零散、不连续的空闲内存块。空闲内存总和充足但没有一大块连续的空闲内存。当程序申请一块较大连续内存时分配失败。这就是外部碎片。关键点内存本身是空闲的只是碎片化、不连续。2、什么是内部碎片内碎片继续举例电影院买票规定售票只能按2 个座位一组售卖。你一个人只需要 1 个座位但规则强制分给你 2 个相连座位。你只用 1 座剩下那 1 个座位被分给你了、别人不能坐一直闲置。放到内存场景硬件 / 内存池有内存对齐规则分配内存时必须分配固定粒度大小的块。程序只需要 50 字节但是内存分配单位最小是 64 字节。系统直接给你 64 字节。多出 14 字节已经划给你的内存块里别人拿不走你自己也用不上白白浪费。关键点这块内存已经被分配出去了包裹在已占用内存内部闲置浪费。3、两者核心区别总结外碎片空闲内存但是四分五裂不连续拼不出大块内碎片内存已经分给进程了块内部多出一小截空间永久闲置。相信各位大佬到这里可以很容易理解什么是内存池了普通动态分配频繁申请释放 → 产生大量外部碎片。内存池提前一次性申请一大块连续内存自己管理里面空间减少向操作系统频繁申请释放大幅降低外部碎片。但内存池为了对齐、按固定块划分很容易产生内部碎片要一小块强制给一整块剩余空间浪费这也是后续项目里能直观观察到的现象。2.4 mallocC/C中我们要动态申请内存都是通过malloc去申请内存但是我们要知道实际我们不是直接去堆 获取内存的 ⽽malloc就是⼀个内存池。malloc()相当于向操作系统“批发”了⼀块较⼤的内存空间然后“零 售”给程序⽤。当全部“售完”或程序有⼤量的内存需求时再根据实际需求向操作系统“进货”。malloc的实现⽅式有很多种⼀般不同编译器平台⽤的都是不同的。⽐如windows的vs系列⽤的微软 ⾃⼰写的⼀套linuxgcc⽤的glibc中的ptmalloc。下⾯有⼏篇关于这块的⽂章⼤概可以去简单看看 了解⼀下关于ptmalloc有兴趣⼤家可以去看看他的实现细节。一文了解Linux内存管理malloc、free 实现原理 - 知乎malloc的底层实现pcmalloc)https://blog.csdn.net/z_ryan/article/details/799507373. 设计一个定长内存池作为程序员(C/C)我们知道申请内存使⽤的是mallocmalloc其实就是⼀个通⽤的⼤众货什么场景 下都可以⽤但是什么场景下都可以⽤就意味着什么场景下都不会有很⾼的性能下⾯我们就先来设 计⼀个定⻓内存池做个开胃菜当然这个定⻓内存池在我们后⾯的⾼并发内存池中也是有价值的所 以学习他⽬的有两层先熟悉⼀下简单内存池是如何控制的第⼆他会作为我们后⾯内存池的⼀个基础组件。3.1 设计的基本流程图3.1 申请空间New()的实现申请 New ()优先查自由链表_freeList有闲置内存直接拿不用向系统申请速度最快没有闲置块检查预分配大块内存剩余空间空间不足malloc 一次性申请 128KB 大内存从大块内存切出一块裸地址统一执行定位 new在裸内存上调用构造函数生成合法对象返回代码如下// 功能从内存池里申请一个T类型的对象返回对象指针 T* New() { T* obj nullptr; // 最终要返回的对象指针先初始化为空 // ---------------- 第一步优先复用「别人用完还回来的」内存块 ---------------- // _freeList 叫自由链表串着所有回收回来的空闲内存块 if (_freeList) { // 取出链表第一个节点里存的「下一个节点的地址」 // 通俗理解每个空闲块的前几个字节我们都用来存下一个空闲块的地址像链条一样串起来 void* next *(void**)_freeList; obj (T*)_freeList; // 把链表第一个块拿出来给用户当对象用 _freeList next; // 链表头往后挪一位指向第二个节点 } // ---------------- 第二步没有回收块就从大块内存里切一块 ---------------- else { // 先检查剩下的大块内存够不够分一个对象出来 if (_remainBytes sizeof(T)) { // 不够的话就向系统一次性申请一大块内存128KB 128*1024字节 // 一次性申请大块比一次次申请小块快很多这是对象池提速的核心之一 _memory (char*)malloc(128 * 1024); if (_memory nullptr) { throw std::bad_alloc(); // 申请失败就抛异常 } _remainBytes 128 * 1024; // 更新剩余内存大小为刚申请的整块大小 } obj (T*)_memory; // 从大块内存的开头切出一块当对象 // 计算实际分配大小如果对象比指针还小就按指针大小分配 // 原因回收的时候每个块至少要能存下一个指针用来串链表不然放不下 size_t objSize sizeof(T) sizeof(void*) ? sizeof(void*) : sizeof(T); _memory objSize; // 大块内存指针往后挪相当于切掉了刚分出去的那块 _remainBytes - objSize; // 剩余内存减去分出去的大小 } // ---------------- 第三步在内存上构造对象 ---------------- // 定位new语法在已经申请好的内存地址上主动调用T的构造函数 // 为什么要这一步因为前面只拿到了裸内存还没初始化对象成员值都是乱的 new(obj)T; return obj; }小编中午刚睡醒一时忘记了定位new的语法格式顺便复习一下吧。定位new的语法格式new(已经分配好的内存地址) 类型(构造参数);① 无参构造new(obj) T;等价调用T()默认构造② 带参数构造如果类有带参构造函数struct A { A(int x){} }; char buf[100]; A* p (A*)buf; new(p) A(100); // 在p地址调用A(int)构造函数③ C11 及以上支持初始化列表new(p) A{100};顺便来回顾以下定位new和普通的new的区别普通new T()向操作系统堆申请内存在这块内存调用构造函数定位 newnew(ptr) T只做第 2 件事不申请内存内存你自己提前准备好。众所周知new和delete是一对天赐良缘的伴侣。好好好既然new都复习了那我顺手讲一下delete吧普通new当然可以配合普通delete成对使用用来防止内存泄漏但是定位 new分配出来的对象绝对不能用普通 delete 释放普通delete p会做两件事调用析构函数释放堆内存但内存池的内存是我们统一管理的不能交给系统释放所以要手动拆分比如我代码里的// 1.手动调用析构销毁对象对应定位new的反向操作 obj-~T(); // 2.内存不还给系统回收进自由链表后续重复使用生命周期配对new(ptr) T;--obj-~T();3.2 Delete()的使用哈哈哈哈上面好像把delete该讲的都讲了没事没事这次就当奖励了3.2.1delete执行流程如下用户用完调用Delete(obj)obj-~T()销毁对象对象内部资源释放这块内存被挂回_freeList空闲链表下一次调用New()优先再次取出这块内存重复循环3.2.2 重要细节如下1.obj-~T();手动调用析构函数普通delete obj会做两件事① 调用析构函数 ② 调用 free 把内存还给系统堆内存池不能执行②内存我们要重复利用。所以我们手动只完成第一步析构对象。举例子如果T内部有动态开辟的缓冲区析构函数里面会释放缓冲区防止内存泄漏注意只是清理对象内部资源承载这个对象本身的那块内存仍然保留2.*(void**)obj _freeList;重点难点obj原本是T*指向一块存放 T 对象的内存。现在对象已经析构完毕这块内存暂时空闲这块内存的数据已经没用了。于是我们直接复用这块内存最开头的 8 字节64 位系统指针大小用来保存链表指针。其实这里也是关键我们用这个可以自动识别当前程序在多少位系统下运行32位的指针大小是4字节。Google牛批(void**)obj把 T 对象的地址强制转换成「指针的指针」*(void**)obj读写这块内存起始位置写入旧链表头_freeList效果当前空闲块记住「下一个空闲块是谁」形成单向链表。3._freeList obj;更新链表头把当前回收的内存块变成自由链表第一个节点。也就是头插法O (1) 时间复杂度不需要遍历链表速度极快。4.为什么敢直接覆盖 obj 头部内存因为已经执行obj-~T()对象生命周期结束对象本身的数据不再需要这块裸内存可以随便使用用来存储链表指针完全安全。5.为什么 New 拿到链表中的内存后必须再次执行new(obj) T内存块只是一块空地。上次 Delete 的时候析构了对象内存里是失效的数据。必须依靠定位 new重新调用构造函数初始化出新的合法对象。6.和普通 delete 最大区别delete obj; // 析构 归还内存给操作系统 TNPool.Delete(obj); // 只析构内存缓存复用不归还系统7.类型强转*(void**)obj的前提我们在 New 的时候做了限制size_t objSize sizeof(T) sizeof(void*) ? sizeof(void*) : sizeof(T);保证每一块内存至少拥有存放一个指针的空间。防止 T 很小内存空间不足以存放链表指针导致越界。3.3 TestObjectPool ()3.3.1 函数目标模拟高频、大量小对象反复创建销毁场景对比「系统原生new/delete」与「自定义定长内存池」的执行耗时验证理论频繁申请释放大量小对象时内存池性能优于系统默认堆分配同时减少内存碎片。代码与注释如下// 性能测试函数对比 系统new/delete 和 自定义对象池 的速度差异 void TestObjectPool() { const size_t Rounds 3; // 测试轮数连续跑3轮减少单次测试偶然性结果更稳定 const size_t N 100000; // 每一轮申请、释放 10万个TreeNode节点 // 第一组实验C原生 new / delete size_t begin1 clock(); // clock()记录CPU开始滴答数用来统计代码运行耗时 std::vectorTreeNode* v1; // 存放所有new出来的对象指针后面统一释放 v1.reserve(N); // 提前预留vector存储空间 // 重点不加reservevector不断扩容、拷贝内存会干扰计时结果 // 循环多轮模拟持续不断的对象创建销毁压力 for (size_t j 0; j Rounds; j) { // 批量申请10万个节点 for (int i 0; i N; i) { v1.push_back(new TreeNode); } // 批量释放10万个节点 for (int i 0; i N; i) { delete v1[i]; } v1.clear(); // 清空容器准备下一轮测试 } size_t end1 clock(); // 记录本组结束时间 // 第二组实验我们实现的ObjectPool内存池 ObjectPoolTreeNode TNPool; // 创建TreeNode专用内存池 size_t begin2 clock(); std::vectorTreeNode* v2; v2.reserve(N); // 和上面保持一致保证两组测试环境公平 for (size_t j 0; j Rounds; j) { // 从内存池批量申请对象 for (int i 0; i N; i) { v2.push_back(TNPool.New()); } // 将对象归还内存池不是释放给操作系统 for (int i 0; i N; i) { TNPool.Delete(v2[i]); } v2.clear(); } size_t end2 clock(); // 打印两组消耗的时钟滴答数数值越小运行越快 cout new cost time: end1 - begin1 endl; cout object pool cost time: end2 - begin2 endl; }3.3.2为什么系统 new/delete 通常更慢通用堆分配器开销巨大new底层调用malloc需要维护整张堆空闲链表、合并空闲块、处理边界、全局锁每次分配释放都有复杂算法。频繁用户态 ↔ 内核态切换大量小块内存反复申请容易触发操作系统堆扩容系统调用成本很高。容易产生外部内存碎片小块内存交替分配释放堆上出现大量不连续小孔后续分配越来越慢。那我们的内存池又有怎样的优越性呢老大哈哈哈哈哈我这就解释话说回来而内存池首次少量调用malloc申请大块内存后续分配 / 回收只操作指针用户态、O (1) 链表头插 / 头取没有复杂堆算法。3.3.3 代码细节vector.reserve(N)的意义vector 默认空间不足时自动扩容扩容会分配新内存 拷贝所有指针 释放旧内存。如果不提前reserve扩容耗时会混入测试结果破坏对比公平性。两组测试统一加上消除容器带来的干扰。先申请全部对象、再统一释放模拟业务中短时批量创建一堆对象用完统一销毁的场景也可以改成「申请一个、立刻归还一个」的混合模式两种负载都可以拓展测试。clock()小知识点拓展点clock()统计程序占用 CPU 的时间不是墙上时钟Windows 下CLOCKS_PER_SEC1000差值就是毫秒级滴答数。缺点精度一般更高精度可以改用 C11std::chrono。3.3.4测试代码存在的缺陷老大你的代码好像存在不足。looking my eyes, why baby why?what can you say?曼巴out其实这是一个开胃小菜既然你都讲出来了那我就给你罗列出来吧测试顺序影响公平性先跑new/delete再跑内存池。操作系统堆经过第一轮大量 new/delete 后产生碎片可能让第一组天然变慢。✅ 优化方案多次运行、调换顺序先测内存池再测 new取平均值。当前内存池存在内存泄漏New()中malloc出来的 128KB 大块内存程序结束没有调用free。拓展优化增加内存池析构函数保存所有大块内存地址退出统一释放。只测试单线程场景​ 原生malloc​ 全局锁竞争在多线程下会急剧变慢内存池可以改造为无 锁 / 分段锁并发性能差距会更大。3.4 定长内存池源码ObjectPool.h#pragma once // 头文件保护防止这个头文件被重复包含导致重复定义报错 // 引入需要的标准库 #include iostream // 用于控制台打印输出 #include vector // 用数组存申请的对象指针方便批量释放 #include ctime // 用于计时对比两种分配方式的速度 #include new // 提供「定位new」语法和内存分配失败的异常类型 using namespace std; // 定长内存池模板类专门用来高效分配/释放 固定大小为T的对象 // 核心优势比系统自带的new/delete快很多还能减少内存碎片 templateclass T class ObjectPool { public: // 功能从内存池里申请一个T类型的对象返回对象指针 T* New() { T* obj nullptr; // 最终要返回的对象指针先初始化为空 // ---------------- 第一步优先复用「别人用完还回来的」内存块 ---------------- // _freeList 叫自由链表串着所有回收回来的空闲内存块 if (_freeList) { // 取出链表第一个节点里存的「下一个节点的地址」 // 通俗理解每个空闲块的前几个字节我们都用来存下一个空闲块的地址像链条一样串起来 void* next *(void**)_freeList; obj (T*)_freeList; // 把链表第一个块拿出来给用户当对象用 _freeList next; // 链表头往后挪一位指向第二个节点 } // ---------------- 第二步没有回收块就从大块内存里切一块 ---------------- else { // 先检查剩下的大块内存够不够分一个对象出来 if (_remainBytes sizeof(T)) { // 不够的话就向系统一次性申请一大块内存128KB 128*1024字节 // 一次性申请大块比一次次申请小块快很多这是对象池提速的核心之一 _memory (char*)malloc(128 * 1024); if (_memory nullptr) { throw std::bad_alloc(); // 申请失败就抛异常 } _remainBytes 128 * 1024; // 更新剩余内存大小为刚申请的整块大小 } obj (T*)_memory; // 从大块内存的开头切出一块当对象 // 计算实际分配大小如果对象比指针还小就按指针大小分配 // 原因回收的时候每个块至少要能存下一个指针用来串链表不然放不下 size_t objSize sizeof(T) sizeof(void*) ? sizeof(void*) : sizeof(T); _memory objSize; // 大块内存指针往后挪相当于切掉了刚分出去的那块 _remainBytes - objSize; // 剩余内存减去分出去的大小 } // ---------------- 第三步在内存上构造对象 ---------------- // 定位new语法在已经申请好的内存地址上主动调用T的构造函数 // 为什么要这一步因为前面只拿到了裸内存还没初始化对象成员值都是乱的 new(obj)T; return obj; } // 功能把对象还回内存池回收以后重复用 void Delete(T* obj) { // 第一步先主动调用析构函数清理对象里的资源 // 通俗理解对象不用了要先结束它的生命周期比如对象里有malloc的内存这里要释放 obj-~T(); // 第二步用头插法把这块内存插到自由链表的最前面 // 头插最快不用遍历链表这也是对象池快的原因 *(void**)obj _freeList; // 让当前块的前几个字节存上原来的链表头地址 _freeList obj; // 把链表头更新成当前块它就成了第一个节点 } private: char* _memory nullptr; // 指向大块内存的起始地址用char*是因为1字节好切分像尺子一样量字节 size_t _remainBytes 0; // 大块内存里还剩多少字节没分配出去 void* _freeList nullptr; // 自由链表的头指针串着所有回收回来的空闲内存块 }; // 测试用的树节点结构体用来当内存池的测试对象 struct TreeNode { int _val; // 节点存的值 TreeNode* _left; // 左孩子指针 TreeNode* _right; // 右孩子指针 // 构造函数创建节点时给成员赋初始值 TreeNode() :_val(0) , _left(nullptr) , _right(nullptr) { } }; // 性能测试函数对比 系统new/delete 和 自定义对象池 的速度差异 void TestObjectPool() { const size_t Rounds 3; // 测试轮数跑3轮取平均结果更准 const size_t N 100000; // 每轮申请、释放的对象数量10万个 // ---------------- 第一组用系统原生的 new / delete ---------------- size_t begin1 clock(); // 记录开始时间 std::vectorTreeNode* v1; // 存所有申请的对象指针方便批量释放 v1.reserve(N); // 提前预留空间避免vector扩容影响计时 // 循环Rounds轮每轮申请N个、再释放N个 for (size_t j 0; j Rounds; j) { // 批量申请 for (int i 0; i N; i) { v1.push_back(new TreeNode); } // 批量释放 for (int i 0; i N; i) { delete v1[i]; } v1.clear(); // 清空数组准备下一轮 } size_t end1 clock(); // 记录结束时间 // ---------------- 第二组用我们自己写的对象池 ---------------- ObjectPoolTreeNode TNPool; // 创建一个TreeNode类型的内存池 size_t begin2 clock(); std::vectorTreeNode* v2; v2.reserve(N); for (size_t j 0; j Rounds; j) { // 从对象池批量申请 for (int i 0; i N; i) { v2.push_back(TNPool.New()); } // 批量还回对象池 for (int i 0; i N; i) { TNPool.Delete(v2[i]); } v2.clear(); } size_t end2 clock(); // 打印两组耗时数值越小速度越快 cout new cost time: end1 - begin1 endl; cout object pool cost time: end2 - begin2 endl; }Test.cpp#include ObjectPool.h int main() { TestObjectPool(); return 0; }又是美好的一天各位观众姥爷们若有什么不懂的地方欢迎在评论区留言哦我会给出相应解释的。如有什么疏忽的细节请点出我会及时纠正。感谢观看。喵喵

相关新闻

最新新闻

内网多人在线填表自动汇总系统搭建:飞书钉钉与开源Baserow、NocoDB方案

内网多人在线填表自动汇总系统搭建:飞书钉钉与开源Baserow、NocoDB方案

内网多人在线填表自动汇总系统搭建:飞书钉钉与开源Baserow、NocoDB方案 很多人都遇到过类似的需求,在此一次性说清。这件事看起来简单,实际操作时容易绕弯路,关键在于先把需求类型分清。 你想要的通常是:“在公司内网…

2026/7/31 10:37:40
售前技术支持的Agentic RAG架构:知识库检索与报价生成的工程实现

售前技术支持的Agentic RAG架构:知识库检索与报价生成的工程实现

前言语核科技技术团队在售前场景的AI工具落地过程中发现,知识库检索环节是决定报价生成质量与效率的关键瓶颈——传统RAG面对结构化程度低、字段专业的询价文件时,检索准确率和响应速度都难以满足生产环境要求。团队围绕这一问题,在Agentic R…

2026/7/31 10:37:40
AI认知基础设施是什么

AI认知基础设施是什么

摘要 很多人一提到 AI 增长,就会先想到投流、内容分发、爆款脚本,或者单点去做 SEO、做官网改版、做几篇文章。但如果底层没有一套能让 AI 正确认知、稳定读取、持续引用你的结构,前面的动作往往只是“信息投放”,而不是“认知建设…

2026/7/31 10:37:40
如何在5分钟内为任何Unity游戏开启自动翻译:XUnity.AutoTranslator完全指南

如何在5分钟内为任何Unity游戏开启自动翻译:XUnity.AutoTranslator完全指南

如何在5分钟内为任何Unity游戏开启自动翻译:XUnity.AutoTranslator完全指南 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator 你是否曾经因为语言障碍而错过精彩的游戏体验?面对外语…

2026/7/31 10:37:40
常见OFDR测量曲线结果分析

常见OFDR测量曲线结果分析

光学频域反射计(OFDR)凭借其超高空间分辨率和分布式测量能力,已成为光器件、光模块及光链路特性表征的重要手段。在实际测试中,OFDR测试得到的反射强度-距离曲线,包含了从设备内部到待测样品末端整个光链路反射光信号强…

2026/7/31 10:37:40
C++17变量与基本类型:从基础概念到现代编程实践

C++17变量与基本类型:从基础概念到现代编程实践

1. 项目概述:为什么从变量和基本类型开始? 如果你刚翻开《C17从入门到精通》这本书,或者刚开始接触C编程,可能会觉得第二章“变量和基本类型”的内容有些基础,甚至想快速跳过。但以我十多年的编程和教学经验来看&#…

2026/7/31 10:32:40

月新闻