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

mac部署openclaw+ollama(本地模型)

mac部署openclaw+ollama(本地模型)

前提条件: 1.本地安装好ollama  2.在下适合自身机器的模型 这在上一篇中有提到https://blog.ZEEKLOG.net/i_k_o_x_s/article/details/77042085 安装:Docker Desktop 官网:https://www.docker.com/products/docker-desktop/ 安装openclaw https://openclaw.ai/ 需要其他装一些东西 homebrew  1.brew install node@22 (当前安装的可能是23的版本,openclaw 对这个不友好,需要装22的) 2.brew install vips curl -fsSL https://openclaw.

手搓简易 Linux 进程池:从 0 到 1 实现基于管道的任务分发系统

手搓简易 Linux 进程池:从 0 到 1 实现基于管道的任务分发系统

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 一. 核心设计思路 * 二. 代码模块拆解 * 2.1 任务定义与随机任务生成 * 2.2 子进程任务处理逻辑 * 2.3 通道(Channel)类:封装父子进程通信 * 2.4 进程池(ProcesspPool)类:核心管理逻辑 * 2.5 主函数:进程池使用示例 * 三. 关键知识点解析 * 3.1 管道通信原理 * 3.2 轮询负载均衡 * 3.3 进程回收的坑

Flutter 组件 sse_stream 的适配 鸿蒙Harmony 实战 - 驾驭轻量级服务器发送事件流、实现鸿蒙端长连接实时通讯与断线重连方案

Flutter 组件 sse_stream 的适配 鸿蒙Harmony 实战 - 驾驭轻量级服务器发送事件流、实现鸿蒙端长连接实时通讯与断线重连方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 sse_stream 的适配 鸿蒙Harmony 实战 - 驾驭轻量级服务器发送事件流、实现鸿蒙端长连接实时通讯与断线重连方案 前言 在鸿蒙(OpenHarmony)生态的金融实时行情、在线社交协作以及物联网告警应用中,如何实现“数据从服务器到终端的实时推送”是一个核心命题。面对不需要双向通信(WebSocket 太重)且对功耗极其敏感的移动端场景,基于 HTTP 协议的轻量化长连接方案——SSE(Server-Sent Events)成为了事实上的行业标准。 然而,处理不稳定的移动网络波动、处理分块传输(Chunked Encoding)中的字节截断、以及在鸿蒙端实现优雅的断线重连逻辑,依然是开发者面临的技术瓶颈。 sse_stream 是一套专为解析该协议设计的高性能响应流解析引擎。它能将原始的二进制流瞬间转化为语义化的 Event 对象。适配到鸿蒙平台后,它不仅能支撑起一个毫秒级延迟的行情大盘,

从小项目到大型鸿蒙 App 的架构变化

从小项目到大型鸿蒙 App 的架构变化

子玥酱(掘金 / 知乎 / ZEEKLOG / 简书 同名) 大家好,我是子玥酱,一名长期深耕在一线的前端程序媛 👩‍💻。曾就职于多家知名互联网大厂,目前在某国企负责前端软件研发相关工作,主要聚焦于业务型系统的工程化建设与长期维护。 我持续输出和沉淀前端领域的实战经验,日常关注并分享的技术方向包括前端工程化、小程序、React / RN、Flutter、跨端方案, 在复杂业务落地、组件抽象、性能优化以及多端协作方面积累了大量真实项目经验。 技术方向:前端 / 跨端 / 小程序 / 移动端工程化 内容平台:掘金、知乎、ZEEKLOG、简书 创作特点:实战导向、源码拆解、少空谈多落地 文章状态:长期稳定更新,大量原创输出 我的内容主要围绕 前端技术实战、真实业务踩坑总结、框架与方案选型思考、行业趋势解读 展开。文章不会停留在“API 怎么用”,而是更关注为什么这么设计、在什么场景下容易踩坑、