mold 项目内嵌 oneTBB 的 fixed_pool 详解:基于固定缓冲区的可扩展内存池
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
fixed_pool是 oneTBB(Threading Building Blocks)提供的可扩展内存池类,它从一个由用户预先提供、大小固定的内存缓冲区中执行 malloc/free/realloc 语义的内存分配,且分配行为可随处理器数量扩展。本文以 mold 仓库内嵌的 oneTBB 源码为蓝本,完整讲解fixed_pool的启用方式、类接口、构造函数语义、成员函数行为,并结合 memory_pool.h 的底层实现剖析其工作原理,帮助读者在自己的并发程序中安全、高效地使用固定缓冲区内存池。
一、什么是 fixed_pool:固定缓冲区的可扩展分配
fixed_pool是一个类(class),用于从一块固定大小的缓冲区中进行可扩展的内存分配。它的核心特征包括:
- 所有可供分配的内存,在构造时通过构造参数一次性传入(即用户自备缓冲区,池本身不向系统申请额外内存);
- 分配与释放操作是线程安全的,并且扩展性随处理器数量提升(scales with the number of processors);
- 它满足 oneTBB 定义的 Memory Pool 命名要求,即提供
recycle()、malloc()、free()、realloc()这一组标准接口。
与同为内存池的memory_pool模板类不同,fixed_pool不依赖任何底层分配器——它管理的全部内存都来自构造时传入的那块缓冲区。这意味着它特别适合以下场景:
- 预分配一块大内存区域,之后所有小对象分配都从中取,避免对操作系统的反复调用;
- 对内存使用总量有硬性上限、不允许向系统超额申请的场景;
- 高并发下需要低开销、可扩展分配,且对象生命周期由池统一管理的场景。
注意:
fixed_pool属于 oneTBB 的预览(preview)特性,要使用它必须先定义宏TBB_PREVIEW_MEMORY_POOL并将其置为 1。
二、启用预览特性:TBB_PREVIEW_MEMORY_POOL 宏
oneTBB 对尚未定稿的新特性采用“预览宏”机制进行门控。fixed_pool、memory_pool、memory_pool_allocator均被TBB_PREVIEW_MEMORY_POOL宏保护。这一点在源码中有强制校验:
在 oneapi/tbb/memory_pool.h 头部:
#if !TBB_PREVIEW_MEMORY_POOL #error Set TBB_PREVIEW_MEMORY_POOL to include memory_pool.h #endif也就是说,如果在未定义该宏的情况下直接#include "oneapi/tbb/memory_pool.h",编译会直接报错。同样,聚合头文件 oneapi/tbb.h 也只在宏开启时才包含 memory_pool.h:
#if TBB_PREVIEW_MEMORY_POOL #include "oneapi/tbb/memory_pool.h" #endif正确的启用方式是:在任何包含头文件之前,先定义宏:
#define TBB_PREVIEW_MEMORY_POOL 1 #include "oneapi/tbb/memory_pool.h"官方示例 fixed_pool_example.cpp 也是采用这一顺序(第一行定义宏,第二行包含头文件)。如果希望在整个翻译单元统一启用,也可以在编译命令行中通过-DTBB_PREVIEW_MEMORY_POOL=1(GCC/Clang)或/DTBB_PREVIEW_MEMORY_POOL=1(MSVC)传入。
三、类接口总览:fixed_pool 的 Synopsis
fixed_pool位于命名空间oneapi::tbb中,头文件为oneapi/tbb/memory_pool.h。其完整类声明如下:
namespace oneapi { namespace tbb { class fixed_pool { public: fixed_pool(void *buffer, size_t size); fixed_pool(const fixed_pool& other) = delete; fixed_pool& operator=(const fixed_pool& other) = delete; ~fixed_pool(); void recycle(); void* malloc(size_t size); void free(void* ptr); void* realloc(void* ptr, size_t size); }; } // namespace tbb } // namespace oneapi几个值得注意的接口设计点:
- 不可拷贝:拷贝构造函数与拷贝赋值运算符被显式
delete。这是合理的——内存池内部维护着底层的池状态(rml::MemoryPool 指针)与缓冲区归属关系,不允许复制,因此也没有no_copy语义问题; recycle()一次性回收:与memory_pool一样,fixed_pool通过recycle()一次性释放池中所有已分配对象,而不是逐个 free;- 与 C 运行时同名函数对应:
malloc/free/realloc三个成员函数分别对应 C 标准库同名函数的内存分配语义,只是内存来源从系统堆换成了池管理的缓冲区。
四、构造函数:fixed_pool(void *buffer, size_t size)
构造函数是fixed_pool的唯一入口,其声明与语义如下:
fixed_pool(void *buffer, size_t size);Effects(文档语义):构造一个内存池,管理由buffer指向的、大小为size字节的内存区域。文档同时声明:如果库无法构造该类实例,将抛出bad_alloc异常。
结合源码可以更精确地了解实际行为。在 memory_pool.h 中,构造函数实现为:
inline fixed_pool::fixed_pool(void *buf, size_t size) : my_buffer(buf), my_size(size) { if (!buf || !size) // TODO: improve support for mode with exceptions disabled 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")); }从源码可以看到实际抛出的异常类型比文档描述更细致:
| 触发条件 | 异常类型 | 说明 |
|---|---|---|
buffer为空指针,或size为 0 | std::invalid_argument | 参数非法("Zero in parameter is invalid"),二者之一为 0 即拒绝构造 |
底层 rml 池创建失败(pool_create_v1返回非POOL_OK) | std::runtime_error | 抛出 "Can't create pool" |
通过throw_exception包装 | 取决于TBB_USE_EXCEPTIONS配置 | 禁用异常时行为有所不同 |
由此可见,传入的缓冲区既不能为空指针,大小也不能为 0,这是使用fixed_pool的第一条硬性约束。
从源码还可以看到底层池的创建参数:
rml::MemPoolPolicy args(allocate_request, nullptr, size, /*fixedPool=*/true);MemPoolPolicy的四个参数依次为:内存请求回调、释放回调(固定池为nullptr,即不回调)、池的大小上限(此处为传入的size)、以及fixedPool=true标志——这明确告知 rml(runtime memory library)这是一个固定池,所有内存均来自单一外部缓冲区。
五、成员函数详解:recycle / malloc / free / realloc
fixed_pool的四个核心成员函数语义如下:
| 成员函数 | 语义 |
|---|---|
void recycle() | 释放池中所有已分配的内存,将池重置为可重新使用的状态 |
void* malloc(size_t size) | 从内存池中分配size字节,返回指向该内存的指针 |
void free(void* ptr) | 释放由ptr指向的内存对象 |
void* realloc(void* ptr, size_t size) | 将ptr指向的内存对象重新分配为size字节(可能移动内存位置) |
这些接口并非fixed_pool独自实现,而是继承自公共基类pool_base。在 memory_pool.h 中,pool_base将这些操作全部委托给 oneTBB 底层的 rml 内存分配库:
class pool_base : no_copy { public: 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); } protected: void destroy() { rml::pool_destroy(my_pool); } rml::MemoryPool *my_pool; };pool_base被设计为no_copy(不可拷贝),内部仅持有一个rml::MemoryPool* my_pool句柄。四个成员函数分别对应 rml 的pool_reset、pool_malloc、pool_free、pool_realloc。realloc的存在还允许底层实现做一些低级优化(例如原地扩容,避免数据搬移)。析构时则通过destroy()调用rml::pool_destroy释放整个池。
六、Memory Pool 命名要求:所有内存池的公共契约
fixed_pool满足 oneTBB 定义的Memory Pool 命名要求(Named Requirement),这是理解它与其他内存池组件关系的钥匙。根据 scalable_memory_pools.rst 中的定义,假设P是一个内存池类实例,则必须提供:
| 伪签名 | 语义 |
|---|---|
~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字节 |
在 oneTBB 中,同时满足该命名要求的两个模型类型是:
memory_pool模板类:从底层分配器(如std::allocator)申请大块内存,再切分给用户;fixed_pool类:从固定大小的用户缓冲区中分配,本文的主角。
两者共享同一套接口,但内存来源完全不同:前者“可增长”(底层分配器按需供块),后者“固定”(缓冲区用尽即失败)。它们的公共基类正是上一节的pool_base——这从 memory_pool.h 的类继承关系可以印证:memory_pool与fixed_pool都public pool_base。
七、源码级原理:fixed_pool 如何“消费”一块缓冲区
理解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; }这段代码揭示了固定池的工作机制:
- rml 在需要为池补充内存时,会回调
allocate_request; - 该回调一次性把整个缓冲区交出:返回
my_buffer,同时把要交付的字节数bytes设为my_size,随后将my_size清零; my_size被清零起到“一次性”标记作用——断言0 != self.my_size表明缓冲区只能被取用一次,重复取用属于编程错误(在 debug 构建下会触发断言)。
也就是说,固定池的“固定”体现在:底层 rml 一次性获得整块用户缓冲区,之后所有 malloc/free/realloc 请求都在 rml 内部从这块缓冲区中切分与回收。正因如此:
recycle()(即rml::pool_reset)可以把这块缓冲区内的已分配对象全部回收、供后续重新分配;- 而缓冲区一旦被 rml 取用并耗尽,无法再向池补充新的内存——这是固定池与可增长的
memory_pool的本质区别。
八、完整可运行示例:从固定缓冲区分配与释放
官方示例 fixed_pool_example.cpp 给出了最简洁的用法:
#define TBB_PREVIEW_MEMORY_POOL 1 #include "oneapi/tbb/memory_pool.h" int main() { char buf[1024]; oneapi::tbb::fixed_pool my_pool(buf, 1024); void* my_ptr = my_pool.malloc(10); my_pool.free(my_ptr); }逐步解读:
char buf[1024];——栈上(或任意位置)预先准备一块 1024 字节的内存;fixed_pool my_pool(buf, 1024);——用该缓冲区构造固定池。注意这里传入的是裸void*与字节数,缓冲区只需大小足够,不需要特定对齐要求之外的特殊属性(对齐由 rml 内部根据请求大小处理);my_pool.malloc(10);——从池中分配 10 字节;my_pool.free(my_ptr);——释放该块内存,归还给池,可供后续分配复用。
在实际项目中,可以在构造阶段一次性malloc多块内存,或者配合recycle()在“一轮任务结束”时整体回收。由于分配来自用户自备的缓冲区,整个池的峰值内存占用是可预期、有上限的,这在实时性敏感或内存受限的环境中是显著优势。
九、与 memory_pool、memory_pool_allocator 的关系:三种组件的协作
fixed_pool不是孤立存在的。oneTBB 的 Scalable Memory Pools 体系由三个组件构成(对应 scalable_memory_pools.rst 的 toctree):
| 组件 | 类型 | 内存来源 | 定位 |
|---|---|---|---|
| memory_pool | 类模板<typename Alloc> | 底层分配器按需提供的大块内存 | 可增长的线程安全内存池 |
| fixed_pool(本文) | 普通类 | 构造时传入的固定缓冲区 | 固定容量、可扩展分配的内存池 |
| memory_pool_allocator | 类模板<typename T> | 绑定某个memory_pool或fixed_pool实例 | 把内存池包装成标准 C++ 分配器,供 STL 容器使用 |
其中memory_pool_allocator是让fixed_pool走进标准库容器的桥梁。其构造函数分别重载了绑定memory_pool&与fixed_pool&两个版本(见 memory_pool_allocator_cls.rst),底层实现则是在 memory_pool.h 中把allocate/deallocate转发给池的malloc/free,allocate失败时抛出std::bad_alloc。
一个典型的组合用法(参考官方示例 memory_pool_allocator_example.cpp 的变体):
#define TBB_PREVIEW_MEMORY_POOL 1 #include "oneapi/tbb/memory_pool.h" #include <list> int main() { char buf[65536]; oneapi::tbb::fixed_pool my_pool(buf, sizeof(buf)); using pool_allocator_t = oneapi::tbb::memory_pool_allocator<int>; std::list<int, pool_allocator_t> my_list(pool_allocator_t{my_pool}); my_list.emplace_back(1); // 节点内存来自固定池 }这里std::list的节点内存不再来自系统堆,而是来自 64KB 的固定缓冲区——容器的峰值内存因此变得完全可控。memory_pool_allocator同时满足 ISO C++ 标准 [allocator.requirements] 的要求,并提供rebind与operator==/operator!=(比较是否绑定同一池实例),可无缝用于std::vector、std::list、std::map等标准容器。
十、使用注意事项与限制
综合文档与源码,使用fixed_pool时需注意以下几点:
- 先定义宏再包含头文件:
TBB_PREVIEW_MEMORY_POOL必须置 1,否则编译报错(#error强制校验); - 缓冲区必须非空且大小非 0:否则构造函数抛出
std::invalid_argument; - 池容量固定,不可增长:缓冲区一经 rml 取用,池的可用内存总量即固定;分配超出剩余容量会失败(返回
nullptr),不会向系统申请更多内存; - 构造失败异常:文档声明失败时抛
bad_alloc,但从源码看,参数非法抛invalid_argument、底层池创建失败抛runtime_error,编程时应按实际实现处理; - 不可拷贝:
fixed_pool拷贝构造与赋值被删除,无法按值传递,应通过引用或指针共享; - 嵌套池的生命周期(来自 memory_pool_cls.rst 的 caution):如果底层分配器引用另一个可扩展内存池,内层池必须在外层池被销毁或 recycle 之前销毁;
- 缓冲区不得提前释放:池的生命周期内,其管理的缓冲区必须保持有效;池析构(
~fixed_pool→destroy()→rml::pool_destroy)之后,该缓冲区才可被安全地另行处置。
结语
fixed_pool以“用户自备一块固定缓冲区”的极简模型,换来了线程安全、可随处理器数量扩展、且峰值内存完全可控的内存分配能力。配合memory_pool_allocator,它能无缝融入 STL 容器体系,为高并发程序的中间数据结构提供确定性的内存行为。理解其“缓冲区一次性整块交给 rml、池内自由切分回收”的实现机制,有助于在真正需要时做出正确的容量规划与生命周期管理。本文对应的完整类声明与实现可直接在 oneapi/tbb/memory_pool.h 中查阅,官方示例位于 fixed_pool_example.cpp,更多内存池家族成员可参见 Scalable Memory Pools 参考页。
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考