C++ 高性能定长内存池的实现
前言
在 C++ 后端开发、游戏引擎、高频交易等场景中,频繁的 new/delete 操作会带来巨大的性能开销 —— 系统调用的上下文切换、内存碎片的产生、堆内存的锁竞争,都是性能瓶颈的元凶。
为了解决这个问题,内存池(Memory Pool) 应运而生。它的核心思想是:一次性向操作系统申请一大块连续内存,后续对象的创建和销毁都在这块内存上完成,避免频繁的系统调用,同时复用内存减少碎片。
本文将带你从零实现一个定长内存池(针对固定大小对象优化),深入讲解核心原理,并通过性能测试对比,验证其相比原生 new/delete 的性能优势。
一、定长内存池核心原理
定长内存池专为固定大小的对象设计(比如树节点、消息结构体、自定义类实例等),核心逻辑可以概括为 3 步:
- 批量预分配:向操作系统一次性申请一大块内存(如 128KB),作为 “内存池”。
- 按需切分复用:创建对象时,从预分配的内存中切出一小块;销毁对象时,不释放给操作系统,而是将内存块加入 “空闲链表” 复用。
- 无锁高效:单线程场景下,全程无锁操作,分配和释放仅需指针操作,时间复杂度 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 对象。
- 逻辑:
- 先看有没有人退回来的空盒子(_freeList),有就直接给。
- 没有就从我们囤的大内存块里切一小块。
- 用定位 new调用构造函数,把这块内存变成一个真正的对象。
- 就像:顾客来买东西,先给二手空盒,没有就从新货里拆一个。
(3)Delete() 方法(收退货,把盒子收回来)
void Delete(T* obj) noexcept { if (!obj) return; obj->~T(); // 调用析构函数,清空盒子 // 把空盒子头插到链表,方便下次复用 *static_cast<void**>(static_cast<void*>(obj)) = _freeList; _freeList = obj; }- 作用:回收用户不用的对象。
- 逻辑:
- 调用析构函数,清理对象内容。
- 把这个内存块地址头插到 _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 倍!