C++基本组件之内存池详解

发布时间:2026/10/7 9:39:27
C++基本组件之内存池详解 前言内存池memory pool要解决的问题很具体通用分配器general-purpose allocator也就是malloc/free或operator new/operator delete为了应付任意大小、任意生命周期的请求 内部必须维护元数据、分箱bin、合并空闲块coalescing并且为了线程安全通常要加锁。 如果你的程序是反复申请、释放大量同尺寸、短生命周期的小对象这些都是纯开销。一个常见误解是内存池就是自己写个operator new。不是。重载operator new只是换了入口 真正决定性能的是池的分配策略块大小如何取整、空闲块如何组织、何时向系统批量取内存。 另一个误解是内存池一定更快——单线程下它确实通常更快但多线程共享同一个池时 一把互斥量带来的竞争可能比malloc的线程缓存还糟。本文分三层讲先讲定长池fixed-size pool的核心结构侵入式空闲链表 再给出一个完整可编译的实现最后介绍 C17 标准库里已经提供的池设施std::pmr。一、核心思想侵入式空闲链表定长池只需要三样东西一整块从系统申请来的连续内存按固定块大小切成 N 份一条空闲块链表把所有未分配的块串起来一个下一个空闲块的头指针。关键技巧是侵入式intrusivenext指针直接存放在空闲块自身的字节里 而不是额外开一个数组。块被分配出去时那块内存里的next就被用户的对象覆盖 块被归还时再在原地重建next。这样每个块的开销是零——没有每块的元数据 也没有额外的链表节点。这套结构决定了池的复杂度都是 O(1)分配是从链表头摘一个释放是往链表头插一个 没有任何查找或合并。代价是块大小固定只能服务一种尺寸或一个尺寸档位的请求块大小的下限是sizeof(void*)否则装不下next指针池本身不保证线程安全多线程共享时必须自己加锁。对比项operator new/delete定长内存池单次分配复杂度实现相关需要在空闲链/分箱里查找O(1) 摘头每块元数据有大小、边界标记等实现定义无合并空闲块支持不需要块等大内存碎片会产生外部碎片池内的块等大无外部碎片线程安全标准要求可多线程使用默认不安全需自行加锁归还给系统的时机视实现而定通常等池析构/release()二、对齐与块尺寸内存池最容易写错的地方是对齐。placement newnew (p) T(...)要求p满足alignof(T) 如果块大小取成 9 字节这种不是对齐数倍数的值第 2 块开始的地址就不再是 8 的倍数 在上面构造double或指针成员就是未定义行为UB。正确的做法是把块大小向上取整到alignof(std::max_align_t)的倍数 std::max_align_t在cstddef中。alignof的结果一定是 2 的幂 所以可以用位运算做取整constexpr std::size_t align_up(std::size_t n, std::size_t align) noexcept { return (n align - 1) ~(align - 1); }C17 的::operator new(size)返回的内存至少满足alignof(std::max_align_t) 所以按这个对齐取整池内任意块都可以安全地承载基本对齐的类型。 如果类型是过度对齐over-aligned的比如带alignas(64) 就需要 C17 引入的对齐版本::operator new(size, std::align_val_t(align))和对应的::operator delete(ptr, std::align_val_t(align))都在new里。三、完整实现下面是一个可直接编译的定长池以及叠在它上面的类型安全包装。// 适用C17GCC 13 / Clang 17 / MSVC 19.3x 均可编译 // 编译g -stdc17 -Wall -Wextra -pedantic pool.cpp -o pool #include cstddef #include iostream #include new #include utility namespace { constexpr std::size_t align_up(std::size_t n, std::size_t align) noexcept { return (n align - 1) ~(align - 1); } // 空闲块就地存放的结构一个指针指向下一个空闲块 struct FreeNode { FreeNode* next; }; } // namespace class FixedPool { public: FixedPool(std::size_t block_size, std::size_t block_count) : block_size_(align_up(block_size sizeof(FreeNode) ? sizeof(FreeNode) : block_size, alignof(std::max_align_t))), block_count_(block_count), free_head_(nullptr), raw_(nullptr) { if (block_count_ 0) { return; } // 一次性向系统要一整块内存之后不再和分配器打交道 raw_ static_castunsigned char*(::operator new(block_size_ * block_count_)); for (std::size_t i 0; i block_count_; i) { void* block raw_ i * block_size_; ::new (block) FreeNode{free_head_}; // 在空闲块上建立 FreeNode 对象 free_head_ static_castFreeNode*(block); } } ~FixedPool() { ::operator delete(raw_); } // 传 nullptr 是合法的空操作 FixedPool(const FixedPool) delete; FixedPool operator(const FixedPool) delete; // 池耗尽时返回 nullptr而不是抛异常 void* allocate() noexcept { if (free_head_ nullptr) { return nullptr; } FreeNode* node free_head_; free_head_ node-next; return node; } void deallocate(void* p) noexcept { if (p nullptr) { return; } ::new (p) FreeNode{free_head_}; // 重新建立 FreeNode结束被归还对象的生命周期 free_head_ static_castFreeNode*(p); } std::size_t block_size() const noexcept { return block_size_; } std::size_t block_count() const noexcept { return block_count_; } std::size_t available() const noexcept { // O(n)仅供调试/统计 std::size_t n 0; for (const FreeNode* p free_head_; p ! nullptr; p p-next) { n; } return n; } private: std::size_t block_size_; // 已按 max_align_t 取整 std::size_t block_count_; FreeNode* free_head_; // 空闲链表头 unsigned char* raw_; // 整块原始内存 }; // 类型安全包装负责构造与析构FixedPool 只负责字节 template class T class ObjectPool { public: static_assert(alignof(T) alignof(std::max_align_t), 本示例只支持基本对齐的类型过度对齐类型请改用 C17 的 aligned operator new); explicit ObjectPool(std::size_t count) : pool_(sizeof(T), count) {} template class... Args T* create(Args... args) { void* p pool_.allocate(); if (p nullptr) { return nullptr; } try { return new (p) T(std::forwardArgs(args)...); } catch (...) { pool_.deallocate(p); // 构造函数抛异常时把块还回去否则这块内存永久泄露 throw; } } void destroy(T* obj) noexcept { if (obj nullptr) { return; } obj-~T(); // 显式调用析构函数 pool_.deallocate(static_castvoid*(obj)); } std::size_t available() const noexcept { return pool_.available(); } private: FixedPool pool_; }; struct Vec2 { double x; double y; Vec2(double x_, double y_) noexcept : x(x_), y(y_) {} }; int main() { ObjectPoolVec2 pool(4); std::cout 块大小 sizeof(Vec2) 初始可用 pool.available() \n; Vec2* a pool.create(1.0, 2.0); Vec2* b pool.create(3.0, 4.0); if (a ! nullptr b ! nullptr) { std::cout a-x b-y \n; // 1.0 4.0 } pool.destroy(a); pool.destroy(b); std::cout 释放后可用 pool.available() \n; return 0; }逐行核对几个点::new (block) FreeNode{free_head_}是布局 newplacement new它不分配内存只在给定地址上构造对象FreeNode是聚合类型用花括号初始化列表。block_size_ sizeof(FreeNode) ? sizeof(FreeNode) : block_size保证块至少装得下一个指针。四个成员按block_size_、block_count_、free_head_、raw_的顺序声明构造函数的初始化列表顺序与之一致避免-Wreorder警告成员初始化顺序永远按声明顺序不按列表顺序。ObjectPool::create的try/catch(...)里throw;是重新抛出当前异常类型不变。四、标准库里已有的池std::pmrC17 在memory_resource里提供了现成的池设施不必都自己写std::pmr::memory_resource抽象基类公开allocate/deallocate/is_equal子类实现do_allocate/do_deallocate/do_is_equal。std::pmr::unsynchronized_pool_resource单线程用无锁内部按尺寸档位管理多个小块池。std::pmr::synchronized_pool_resource多线程用需要同步开销。std::pmr::pool_options可选项有max_blocks_per_chunk和largest_required_pool_block两个std::size_t成员具体取值由实现决定。// 需要 C17 及以上 #include cstddef #include memory_resource #include vector int main() { std::pmr::unsynchronized_pool_resource pool; // 单线程别跨线程用 std::pmr::polymorphic_allocatorint alloc{pool}; std::pmr::vectorint v{alloc}; for (int i 0; i 1000; i) { v.push_back(i); // 扩容时的旧缓冲会归还给 pool由 pool 复用 } pool.release(); // 一次性把所有内存还给上游upstream return 0; }libstdc 里还有一个历史遗留的扩展__gnu_cxx::__pool_alloc头文件ext/pool_allocator.h 那是 SGI STL 时代的池式分配器标准库的std::allocator现在只是operator new的薄包装 本身不做池化——这一点常被搞混。常见坑点块大小没有对齐取整❌ 直接按用户给的sizeof(T)切块T 为 9 字节时第 2 块起地址不再对齐 在上面 placement new 是 UB。✅ 用align_up(size, alignof(std::max_align_t))把所有块对齐到max_align_t。只归还内存不调用析构函数❌pool.deallocate(p);直接丢掉一个持有std::string成员的对象它的堆内存永远不会释放。✅p-~T(); pool.deallocate(p);—— 先显式析构再还内存顺序不能反。同一块内存释放两次❌ 归还两次会把同一个块两次插入空闲链表链表形成环下一次allocate可能拿到重复地址。✅ 建立所有权约定destroy之后立刻把指针置空接口文档写清不得重复释放。 此外给池类显式 delete拷贝构造与拷贝赋值能从类型层面杜绝 两个池对象管同一块内存引发的双重释放。块大小小于sizeof(void*)❌ 用户申请 4 字节块并直接写FreeNode越界覆盖相邻块的next字段。✅ 用sizeof(FreeNode)做下限并在文档里说明块实际会被放大。构造抛异常时内存没归还❌void* p pool.allocate(); return new (p) T(args...);—— T 的构造抛了异常p就永久丢失池越用越少。✅ 用try/catch(...)捕获pool.deallocate(p)后重新throw。把池当成线程安全的❌ 多个线程直接共享一个FixedPoolfree_head_的读改写没有任何同步—— 这是数据竞争标准不保证任何行为实际表现是链表断裂、块丢失或崩溃。✅ 单线程用不加锁的池每个线程一个thread_local池必须共享时加std::mutex 或直接用std::pmr::synchronized_pool_resource。池对象先销毁对象指针还在用❌FixedPool析构后仍在访问之前allocate出来的块——那些内存已经还给系统 访问是 UB。✅ 池的生存期必须覆盖所有从它分配的对象把它做成成员变量声明在所有使用者之前。过度对齐类型直接塞进池❌ 对struct alignas(64) CacheLine { ... };用ObjectPool 即使块大小取整到 64 的倍数::operator new(size)也不保证 64 字节对齐。✅ 改用 C17 的::operator new(size, std::align_val_t(64)) 并在对应的::operator delete(ptr, std::align_val_t(64))里成对使用。总结关键点结论空闲链表用侵入式链表next直接存在空闲块里零额外开销块大小下限sizeof(void*)并向上取整到alignof(std::max_align_t)的倍数分配/释放都是 O(1)无查找、无合并对象生命周期placement new 构造显式p-~T()析构异常时要归还内存线程安全池本身不安全共享必须加锁或用std::pmr::synchronized_pool_resource现成设施C17 的std::pmr已提供池除非有特殊需求不必手写内存池的本质是用块尺寸固定这个约束换掉通用分配器的查找、合并和锁开销。 只要你的场景真的是同尺寸小对象高频分配这笔交易就划算一旦对象尺寸五花八门 就该退回通用分配器或改用std::pmr的分档池。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询