mold 内嵌 oneTBB 的 fixed_pool 类深度解析:在定长缓冲区上构建可扩展内存池
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
本文以 oneTBB(oneAPI Threading Building Blocks)官方规范文档中 fixed_pool Class 为核心,结合 mold 仓库内嵌的 memory_pool.h 源码与其在 test_scalable_allocator.cpp 中的测试用例,完整讲解fixed_pool的接口语义、Memory Pool 概念、底层实现原理及与memory_pool、memory_pool_allocator的协作方式。读完本文,你将掌握如何在固定大小的用户缓冲区上创建线程安全、随处理器数量扩展的内存池,并理解其与 STL 容器分配器组合的实战模式。
一、fixed_pool 是什么:面向固定缓冲区的可扩展内存池
fixed_pool是一个用于"从固定大小缓冲区进行可扩展内存分配"的模板类。与每次分配都直接向系统堆(或底层分配器)索要内存不同,它在构造时一次性接管调用者提供的整个缓冲区,之后所有malloc/free/realloc操作都在这个缓冲区内部完成,且操作开销会随处理器数量的增加而扩展(即多线程并发分配时不会退化为单点串行竞争)。
从规范文档的 Syntax 与 Header 一节可以看到,使用它需要两个前提:
#define TBB_PREVIEW_MEMORY_POOL 1 #include "oneapi/tbb/memory_pool.h"TBB_PREVIEW_MEMORY_POOL是一个预览特性开关。这一约束在仓库源码中是被强制执行的:在 memory_pool.h 的开头就有
#if !TBB_PREVIEW_MEMORY_POOL #error Set TBB_PREVIEW_MEMORY_POOL to include memory_pool.h #endif如果忘记定义该宏,编译会直接报错。另外注意:本仓库内嵌的头文件将fixed_pool声明在tbb命名空间(通过inline namespace v1导出detail::d1::fixed_pool,见 memory_pool.h),而规范文档示例中使用的是oneapi::tbb::fixed_pool,两者在各自的头文件布局下等价,使用时以实际引入的头文件为准。
二、Memory Pool 概念:fixed_pool 遵循的接口契约
fixed_pool是"Memory Pool 概念"(Memory Pool Concept)的一个模型。规范文档 scalable_memory_pools.rst 用一张表定义了所有内存池类必须满足的伪签名与语义,这是理解fixed_pool全部公开成员的基础:
| 伪签名 | 语义 |
|---|---|
~P() throw(); | 析构函数,释放所有已分配对象的内存 |
void P::recycle(); | 释放所有已分配对象的内存(不销毁池本身) |
void* P::malloc(size_t n); | 从内存池中分配n字节并返回指针 |
void P::free(void* ptr); | 释放由ptr指定的内存对象 |
void* P::realloc(void* ptr, size_t n); | 将ptr指向的内存对象重新分配为n字节 |
fixed_pool的完整成员声明(摘自 fixed_pool_cls.rst):
namespace oneapi { namespace tbb { class fixed_pool : no_copy { public: fixed_pool(void *buffer, size_t size) throw(std::bad_alloc); ~fixed_pool(); void recycle(); void *malloc(size_t size); void free(void* ptr); void *realloc(void* ptr, size_t size); }; } // namespace tbb } // namespace oneapi其中唯一需要单独说明的构造函数语义是:
fixed_pool(void *buffer, size_t size):构造一个管理buffer所指向内存(长度为size字节)的内存池。如果运行时构造失败,抛出std::bad_alloc异常。
no_copy基类意味着内存池持有内部状态(底层rml::MemoryPool*句柄),不允许拷贝或赋值,这与 pool_base 注释中"Pool interface is separate from standard allocator classes because it has to maintain internal state, no copy or assignment"的说明一致。
三、快速上手:在 1MB 缓冲区上建立内存池
规范文档 Example 给出了最小可运行示例:
#define TBB_PREVIEW_MEMORY_POOL 1 #include "oneapi/tbb/memory_pool.h" char buf[1024 * 1024]; oneapi::tbb::fixed_pool my_pool(buf, 1024 * 1024); void* my_ptr = my_pool.malloc(10); my_pool.free(my_ptr);要点拆解:
- 缓冲区生命周期:
buf必须在使用期间保持有效,池本身不拥有、不释放该缓冲区,只负责管理其中的内存; - 一次性接管:从源码实现看(详见下一节),第一次分配请求发生时整个缓冲区会被整体移交给底层池管理器,此后所有分配都在该区域内部完成;
- 调用方式:
malloc/free/realloc分别对应 C 运行时同名函数的语义,返回的指针可直接用于读写。
四、源码级原理:fixed_pool 在 tbbmalloc 中的落地
fixed_pool的实现位于 memory_pool.h,它直接继承自内部基类pool_base:
class fixed_pool : public pool_base { void *my_buffer; size_t my_size; inline static void *allocate_request(intptr_t pool_id, size_t & bytes); public: inline fixed_pool(void *buf, size_t size); ~fixed_pool() { destroy(); } };pool_base(memory_pool.h)把所有公开操作都转发给底层 C 风格rml接口:
void recycle() { rml::pool_reset(my_pool); } void *malloc(size_t size) { return rml::pool_malloc(my_pool, size); } void free(void* ptr) { rml::pool_free(my_pool, ptr); } void *realloc(void* ptr, size_t size) { return rml::pool_realloc(my_pool, ptr, size); }而rml层(Memory Pool 的底层运行时)接口定义在 scalable_allocator.h,包括pool_create_v1、pool_destroy、pool_reset、pool_malloc、pool_realloc、pool_free等。真正的线程安全与可扩展分配逻辑全部封装在这一层。
4.1 构造:策略参数与校验
构造函数(memory_pool.h)是理解fixed_pool行为的关键:
inline fixed_pool::fixed_pool(void *buf, size_t size) : my_buffer(buf), my_size(size) { if (!buf || !size) throw_exception(std::invalid_argument("Zero in parameter is invalid")); rml::MemPoolPolicy args(allocate_request, nullptr, size, /*fixedPool=*/true); rml::MemPoolError res = rml::pool_create_v1(intptr_t(this), &args, &my_pool); if (res != rml::POOL_OK) throw_exception(std::runtime_error("Can't create pool")); }从源码可以推断出三点:
- 参数校验:缓冲区为空指针或大小为 0 时抛出
std::invalid_argument(注意规范文档只写了bad_alloc,实际实现额外增加了这一校验); - 固定池策略:
MemPoolPolicy的第 4 个参数fixedPool=true,且pFree=nullptr——因为整个缓冲区一次性给出、无需逐个归还大块内存,这与 scalable_allocator.h 中对fixedPool位字段"all memory consumed at 1st pAlloc call and never returned, no more pAlloc calls after 1st"的注释完全对应; - 池创建失败:返回码非
POOL_OK时抛出std::runtime_error("Can't create pool"),典型场景是缓冲区太小,不足以容纳池自身的内部元数据。
4.2 分配:整个缓冲区一次交付
fixed_pool的allocate_request(memory_pool.h)展示了"定长池"与可扩展池的根本差异:
inline void *fixed_pool::allocate_request(intptr_t pool_id, size_t & bytes) { fixed_pool &self = *reinterpret_cast<fixed_pool*>(pool_id); __TBBMALLOC_ASSERT(0 != self.my_size, "The buffer must not be used twice."); bytes = self.my_size; self.my_size = 0; // remember that buffer has been used return self.my_buffer; }即:底层池管理器第一次需要大块内存时,fixed_pool把整个缓冲区作为单个大块交付出去,并置my_size = 0标记缓冲区已被使用。这解释了为何缓冲区需要足够大——它既要容纳用户分配请求,还要容纳池内部的管理结构(slab 等)。
五、与兄弟类配合:memory_pool 与 memory_pool_allocator
fixed_pool并非孤立存在,同目录的规范文档还定义了另外两个类,三者共同构成完整的可扩展内存池体系(见 scalable_memory_pools.rst):
5.1 memory_pool:可扩展池
与fixed_pool直接接管用户缓冲区不同,memory_pool<Alloc>以模板参数指定的底层分配器为内存来源,按需以大块方式向底层索要内存(memory_pool_cls.rst)。其构造示例:
#define TBB_PREVIEW_MEMORY_POOL 1 #include "oneapi/tbb/memory_pool.h" oneapi::tbb::memory_pool<std::allocator<char>> my_pool; void* my_ptr = my_pool.malloc(10); my_pool.free(my_ptr);从 memory_pool.h 的实现看,它把Alloc::allocate/Alloc::deallocate包装成rml::MemPoolPolicy的allocate_request/deallocate_request回调,供底层池按需取用。规范文档特别提醒:如果底层分配器本身又是另一个可扩展内存池,内层池必须在外层池被销毁或 recycle 之前销毁(见 memory_pool_cls.rst 的 caution 提示)。
5.2 memory_pool_allocator:把内存池接入 STL 容器
memory_pool_allocator<T>提供 C++ 标准分配器接口,专门用于让memory_pool/fixed_pool服务于 STL 容器(memory_pool_allocator_cls.rst)。它与标准分配器的区别是没有默认构造函数,必须显式绑定到某个池实例:
typedef oneapi::tbb::memory_pool_allocator<int> pool_allocator_t; std::list<int, pool_allocator_t> my_list(pool_allocator_t(my_pool));在源码 memory_pool.h 中,memory_pool_allocator的allocate直接调用所绑定池的malloc,失败时抛出std::bad_alloc;deallocate调用池的free。它还实现了rebind、max_size、operator==/!=(比较的是底层池指针是否相同)等标准分配器要求。
六、测试验证:边界行为与嵌套池场景
仓库测试 test_scalable_allocator.cpp 给出了大量可验证行为,值得逐条对照:
6.1 极小缓冲区与非法参数
TestSmallFixedSizePool(test_scalable_allocator.cpp)遍历从 3 字节到 64KB 的各种缓冲区大小:
- 池要么可用(至少能完成 16 字节或 9KB 的分配),要么创建失败抛出异常,不会出现"创建成功但完全不可用"的中间态;
- 大小为零的缓冲区会抛出
std::invalid_argument; fixed_pool pool(nullptr, 10*1024*1024)也会抛出std::invalid_argument,测试注释明确指出"无内存的无用分配器不能被创建"("Useless allocator with no memory must not be created")。- 测试注释还透露了一个容量经验值:16 字节的小分配走 16KB 的 slab,因此至少要约 16KB 的缓冲区才能支撑小对象分配。
6.2 realloc 与回收复用
在同一测试文件的主流程(test_scalable_allocator.cpp)中:
static char buf[1024*1024*4]; tbb::fixed_pool pool(buf, sizeof(buf)); char *p1 = (char*)pool.malloc(16); strcpy(p1, "this is a test"); char *p2 = (char*)pool.realloc(p1, 15); // realloc 保持内容 char *p3 = (char*)pool.realloc(p2, sizeof(buf)-128*1024); // 几乎占满整个池 // 循环:不断 malloc 翻倍大小并 recycle,验证池可反复复用 for (size_t sz = 10; sz < sizeof(buf); sz *= 2) { REQUIRE(pool.malloc(sz)); pool.recycle(); }它验证了realloc的内容保持性、大块再分配(内部整理/defragmentation)以及recycle()一次回收全部对象后池仍可继续使用的语义。
6.3 三层嵌套池
测试还构造了一个"定长池 → 分配器 → 可扩展池"的嵌套用例(test_scalable_allocator.cpp):
typedef tbb::memory_pool<tbb::memory_pool_allocator<char, tbb::fixed_pool>> NestedPool; static char buffer[8*1024*1024]; tbb::fixed_pool fixedPool(buffer, sizeof(buffer)); tbb::memory_pool_allocator<char, tbb::fixed_pool> fixedPoolAllocator(fixedPool); NestedPool nestedPool(fixedPoolAllocator);即顶层memory_pool的大块内存请求,最终由底层fixed_pool从一块 8MB 的静态缓冲区中满足——这正是规范文档 warning 所指的嵌套场景,销毁顺序必须内层先于外层。
七、使用注意事项总结
- 务必先定义
TBB_PREVIEW_MEMORY_POOL,否则头文件直接#error拒绝编译; - 缓冲区必须足够大:除用户分配需求外,池内部管理结构也占用空间;极小缓冲区会导致
pool_create失败并抛出std::runtime_error; - 缓冲区指针与大小均不能为 0:否则构造函数抛
std::invalid_argument; - 缓冲区生命周期由调用者负责:池在析构时只释放内部管理结构,不会
free用户缓冲区; recycle()一次性释放全部对象,比逐对象free更适合"阶段式"使用模式(如每轮迭代结束后整体重置);- 与 STL 容器搭配使用
memory_pool_allocator,并确保池实例的存活期覆盖容器使用期; - 嵌套池场景遵守"先销毁内层池"的顺序约束。
fixed_pool的价值在于:把"内存从哪里来"(一块确定的固定缓冲区)与"内存怎么高效分配"(线程安全、可扩展的池化管理)彻底解耦,非常适合嵌入式缓冲区、共享内存区、以及需要可预测内存足迹的并发场景。若需进一步了解其兄弟类,可继续阅读 memory_pool_cls.rst 与 memory_pool_allocator_cls.rst。
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考