C++ 高性能定长内存池的实现

前言

在 C++ 后端开发、游戏引擎、高频交易等场景中,频繁的 new/delete 操作会带来巨大的性能开销 —— 系统调用的上下文切换、内存碎片的产生、堆内存的锁竞争,都是性能瓶颈的元凶。

为了解决这个问题,内存池(Memory Pool) 应运而生。它的核心思想是:一次性向操作系统申请一大块连续内存,后续对象的创建和销毁都在这块内存上完成,避免频繁的系统调用,同时复用内存减少碎片

本文将带你从零实现一个定长内存池(针对固定大小对象优化),深入讲解核心原理,并通过性能测试对比,验证其相比原生 new/delete 的性能优势。

一、定长内存池核心原理

定长内存池专为固定大小的对象设计(比如树节点、消息结构体、自定义类实例等),核心逻辑可以概括为 3 步:

  1. 批量预分配:向操作系统一次性申请一大块内存(如 128KB),作为 “内存池”。
  2. 按需切分复用:创建对象时,从预分配的内存中切出一小块;销毁对象时,不释放给操作系统,而是将内存块加入 “空闲链表” 复用。
  3. 无锁高效:单线程场景下,全程无锁操作,分配和释放仅需指针操作,时间复杂度 O (1)。

关键技术点

  • 空闲链表(Free List):用链表记录可复用的内存块,分配时取表头,释放时头插,效率极高。
  • 定位 new(Placement New):在已分配的内存上调用构造函数,完成对象初始化。
  • 显式析构:释放对象时手动调用析构函数,清理资源,再将内存加入空闲链表。
  • 系统级内存申请:使用 VirtualAlloc(Windows)/mmap(Linux)直接向操作系统申请页对齐内存,减少内存碎片。

二、定长内存池完整实现

我们将代码分为两部分:内存池核心实现(ObjectPool.h)性能测试代码(Test.cpp),结构清晰,便于复用和测试。

1. 内存池核心代码(ObjectPool.h)

#pragma once #include <cstddef> #include <new> #include <stdexcept> #ifdef _WIN32 #include <windows.h> #else #include <sys/mman.h> #include <unistd.h> #endif inline static void* SystemAlloc(size_t kpage) { #ifdef _WIN32 void* ptr = VirtualAlloc( nullptr, kpage * 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE ); #else void* ptr = mmap( nullptr, kpage * 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0 ); if (ptr == MAP_FAILED) ptr = nullptr; #endif if (!ptr) throw std::bad_alloc(); return ptr; } template <class T> class ObjectPool { public: T* New() noexcept(false) { T* obj = nullptr; if (_freeList) { void* next = *static_cast<void**>(_freeList); obj = static_cast<T*>(_freeList); _freeList = next; } else { if (_remainBytes < sizeof(T)) { _remainBytes = 128 * 1024; _memory = static_cast<char*>(SystemAlloc(_remainBytes / 4096)); if (!_memory) throw std::bad_alloc(); } obj = static_cast<T*>(static_cast<void*>(_memory)); size_t objSize = (sizeof(T) < sizeof(void*)) ? sizeof(void*) : sizeof(T); _memory += objSize; _remainBytes -= objSize; } new (obj) T{}; return obj; } void Delete(T* obj) noexcept { if (!obj) return; obj->~T(); // 自由链表链接(内存池标准写法) *static_cast<void**>(static_cast<void*>(obj)) = _freeList; _freeList = obj; } private: char* _memory = nullptr; size_t _remainBytes = 0; void* _freeList = nullptr; };

2. 性能测试代码(Test.cpp)

#include <iostream> #include <vector> #include <ctime> #include <memory> #include "ObjectPool.h" using std::cout; using std::endl; struct TreeNode { int _val = 0; TreeNode* _left = nullptr; TreeNode* _right = nullptr; TreeNode() noexcept = default; ~TreeNode() noexcept = default; TreeNode(const TreeNode&) noexcept = default; TreeNode& operator=(const TreeNode&) noexcept = default; TreeNode(TreeNode&&) noexcept = default; TreeNode& operator=(TreeNode&&) noexcept = default; }; void TestObjectPool(); int main() { TestObjectPool(); return 0; } void TestObjectPool() { constexpr size_t Rounds = 5; constexpr size_t N = 100000; std::vector<TreeNode*> v1; v1.reserve(N); const clock_t begin1 = clock(); for (size_t j = 0; j < Rounds; ++j) { for (size_t i = 0; i < N; ++i) { v1.push_back(new TreeNode); } for (size_t i = 0; i < N; ++i) { delete v1[i]; } v1.clear(); } const clock_t end1 = clock(); std::vector<TreeNode*> v2; v2.reserve(N); ObjectPool<TreeNode> TNPool; const clock_t begin2 = clock(); for (size_t j = 0; j < Rounds; ++j) { for (size_t i = 0; i < N; ++i) { v2.push_back(TNPool.New()); } for (size_t i = 0; i < N; ++i) { TNPool.Delete(v2[i]); } v2.clear(); } const clock_t end2 = clock(); cout << "new cost time: " << end1 - begin1 << endl; cout << "object pool cost time: " << end2 - begin2 << endl; }

三,代码详解

1、这两段代码是干什么的?


      简单说,这是一个 **“内存池”** 的程序。
      你可以把它想象成一个 **“小卖部”**:

  • 平时我们买东西,都是去大超市(系统 new/delete),排队、结账,很慢。
  • 现在我们开了一个小卖部(内存池 ObjectPool),自己囤了一堆货,需要的时候直接拿,不用排队,特别快。

       这个程序的目的就是:测试 “小卖部”(内存池)比 “大超市”(系统 new)快多少。


2、第一部分代码:ObjectPool.h(小卖部的仓库和规则)


这部分代码定义了一个类,名字叫 ObjectPool,它就是我们的内存池(小卖部)。

我们把它拆开来看:
1. SystemAlloc 函数(去大超市进货)

inline static void* SystemAlloc(size_t kpage) { #ifdef _WIN32 void* ptr = VirtualAlloc(...); // Windows系统的进货接口 #else void* ptr = mmap(...); // Linux系统的进货接口 #endif if (!ptr) throw std::bad_alloc(); // 没进到货就报错 return ptr; }
  • 作用:向操作系统(大超市)一次性申请一大块内存(比如 128KB)。
  • 就像:小卖部老板开车去大仓库拉了一卡车的货。

2. ObjectPool 类(小卖部本体)
这个类管理着我们拉回来的货。
(1)成员变量(仓库货架)

private: char* _memory = nullptr; // 指向我们囤的一大块货的起始位置 size_t _remainBytes = 0; // 货架上还剩多少货 void* _freeList = nullptr; // 一个链表,记录退回来的空盒子(可以复用的内存)

(2)New() 方法(卖货,给用户一个对象)

T* New() noexcept(false) { T* obj = nullptr; // 1. 优先看有没有退回来的空盒子(复用) if (_freeList) { void* next = *static_cast<void**>(_freeList); obj = static_cast<T*>(_freeList); _freeList = next; } // 2. 没有空盒子,就从新囤的货里切一块出来 else { // 如果货不够了,就去进新货 if (_remainBytes < sizeof(T)) { _remainBytes = 128 * 1024; _memory = static_cast<char*>(SystemAlloc(_remainBytes / 4096)); } // 从大货里切一小块给用户 obj = static_cast<T*>(static_cast<void*>(_memory)); size_t objSize = ...; _memory += objSize; // 货架指针往后挪 _remainBytes -= objSize; // 剩余货物减少 } // 3. 调用构造函数,把盒子变成一个可用的对象 new (obj) T{}; return obj; }
  • 作用:给用户分配一个 TreeNode 对象。
  • 逻辑
  1. 先看有没有人退回来的空盒子(_freeList),有就直接给。
  2. 没有就从我们囤的大内存块里切一小块。
  3. 用定位 new调用构造函数,把这块内存变成一个真正的对象。
  • 就像:顾客来买东西,先给二手空盒,没有就从新货里拆一个。

(3)Delete() 方法(收退货,把盒子收回来)

void Delete(T* obj) noexcept { if (!obj) return; obj->~T(); // 调用析构函数,清空盒子 // 把空盒子头插到链表,方便下次复用 *static_cast<void**>(static_cast<void*>(obj)) = _freeList; _freeList = obj; }
  • 作用:回收用户不用的对象。
  • 逻辑:
  1. 调用析构函数,清理对象内容。
  2. 把这个内存块地址头插到 _freeList 链表,下次 New 可以直接复用。
  • 就像:顾客用完了,把盒子还回来,我们收进回收箱,下次直接再用。

3、第二部分代码:Test.cpp(测试小卖部有多快)


这部分代码是测试程序,用来对比:

  • 用系统 new/delete(大超市)花多长时间。
  • 用我们的 ObjectPool(小卖部)花多长时间。

1. TreeNode 结构体(我们卖的商品)

struct TreeNode { int _val = 0; TreeNode* _left = nullptr; TreeNode* _right = nullptr; TreeNode() noexcept = default; ~TreeNode() noexcept = default; // ... 其他默认函数 ... };
  • 这就是我们要频繁创建和销毁的对象,比如树的节点。

2. main 函数(程序入口)

int main() { TestObjectPool(); // 调用测试函数 return 0; }
  • 程序从这里开始跑,调用测试函数。

3. TestObjectPool 函数(核心测试逻辑)
这个函数做了两件事:
(1)测试系统 new/delete 的速度

const clock_t begin1 = clock(); for (size_t j = 0; j < Rounds; ++j) { for (int i = 0; i < N; ++i) { v1.push_back(new TreeNode); // 用系统new申请 } for (int i = 0; i < N; ++i) { delete v1[i]; // 用系统delete释放 } v1.clear(); } const clock_t end1 = clock();
  • 循环 5 轮,每轮申请 / 释放 10 万个对象。
  • 记录开始和结束时间,算出总耗时。

(2)测试我们的 ObjectPool 的速度

ObjectPool<TreeNode> TNPool; const clock_t begin2 = clock(); 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(); } const clock_t end2 = clock();
  • 同样循环 5 轮,每轮申请 / 释放 10 万个对象。
  • 记录开始和结束时间,算出总耗时。

(3)打印结果

cout << "new cost time: " << end1 - begin1 << endl; cout << "object pool cost time: " << end2 - begin2 << endl;
  • 输出两种方式的耗时,你会发现内存池快很多(通常快 5~10 倍)。

4、总结一下

  • ObjectPool.h 是 “小卖部”:自己囤内存,自己管理,申请释放特别快。
  • Test.cpp 是 “测试员”:对比 “小卖部” 和 “大超市(系统 new)” 谁更快。
  • 核心原理:一次性申请一大块内存,反复切分和复用,减少系统调用,所以快。

5、运行结果大概是这样的

new cost time: 15625 object pool cost time: 1562
  • 系统 new 花了 15625 个时钟周期。
  • 我们的内存池只花了 1562 个时钟周期。
  • 快了 10 倍!

Read more

2019年信奥赛C++提高组csp-s初赛真题及答案解析(选择题6-10)

2019年信奥赛C++提高组csp-s初赛真题及答案解析(选择题6-10)

2019年信奥赛C++提高组csp-s初赛真题及答案解析(选择题6-10) 第 6 题 由数字 1,1,2,4,8,8 所组成的不同的 4位数的个数是()。 A. 104 B. 102 C. 98 D. 100 答案:B 解析:由数字 1,1,2,4,8,8 组成四位数,需考虑重复数字。分类讨论: * 四个数字各不相同:取 1,2,4,8,排列数 4!=24。 * 两对重复:取两个1和两个8,排列数 4 ! 2

By Ne0inhk
深入解剖STL set/multiset:接口使用与核心特性详解

深入解剖STL set/multiset:接口使用与核心特性详解

❤️@燃于AC之乐 来自重庆 计算机专业的一枚大学生 ✨专注 C/C++ Linux 数据结构 算法竞赛 AI 🏞️志同道合的人会看见同一片风景! 👇点击进入作者专栏: 《算法画解》 ✅ 《linux系统编程》✅ 《C++》 ✅ 🌟《算法画解》算法相关题目点击即可进入实操🌟 感兴趣的可以先收藏起来,请多多支持,还有大家有相关问题都可以给我留言咨询,希望希望共同交流心得,一起进步,你我陪伴,学习路上不孤单! 文章目录 * 前言(关联式容器概述) * 一、set类介绍 * 1.1 set的类模板声明 * 二、set的构造与迭代器 * 2.1 构造接口 * 2.2 迭代器接口 * 三、set的核心操作接口 * 3.1 插入操作 * 3.2 查找操作 * 3.3

By Ne0inhk
C++ string 部分功能详解:迭代器、初始化与常用函数

C++ string 部分功能详解:迭代器、初始化与常用函数

在 C++ 中,string是处理字符串的核心容器,它封装了丰富的接口来简化字符串操作。本文将围绕string的迭代器访问、初始化方式、容量调整(reserve)、反转(reverse) 四大核心功能展开,结合可直接运行的代码和结果验证建议,帮你快速掌握string的实用技巧。 一、迭代器与范围 for:遍历 string 的两种核心方式 string作为 STL 容器的一种,支持迭代器(类似指针的访问工具)和范围 for 两种遍历方式,所有的STL容器都可以用以上两种方式遍历,其中范围 for 的底层本质就是迭代器。下面通过代码详细演示两者的用法。 1.1 迭代器遍历:灵活控制访问过程 迭代器的核心作用是 “指向容器元素”,支持*解引用获取值、++移动到下一个元素,适用于所有 STL 容器(如vector、list等)。注意:原文代码存在语法错误(如#

By Ne0inhk