- 并发编程
- 高性能计算
【免费下载链接】oneTBB
oneAPI Threading Building Blocks (oneTBB)
fixed_pool是 oneAPI Threading Building Blocks(oneTBB)预览特性TBB_PREVIEW_MEMORY_POOL提供的核心组件之一,它让开发者能够在一块预先分配好的固定大小缓冲区上,执行线程安全、随处理器数量扩展的内存分配与释放操作。本文基于 doc/main/reference/source/uncategorized/scalable_memory_pools/fixed_pool_cls.rst 展开,并结合 include/oneapi/tbb/memory_pool.h 的源码实现与 test/tbbmalloc/test_scalable_allocator.cpp 的测试用例进行纵深解读。读完本文,你将掌握fixed_pool的完整成员接口、构造约束、底层分配原理,以及如何在真实代码中安全地使用它。
概览:什么是 fixed_pool
fixed_pool是 oneTBB 内存池体系中的"定长池":它不从系统或底层分配器按需申请内存,而是将构造函数传入的一块固定大小的缓冲区作为全部可用内存来源,在池内部完成对这块内存的切分、分配与回收。其设计目标是:
- 可扩展性(Scalable):分配与释放操作随处理器数量增长而保持高效,不会退化为全局锁串行化;
- 线程安全:多个线程可以同时从同一个池中分配内存,无需外部加锁;
- 确定性内存来源:所有可分配内存均由调用方预先指定,便于实现定制化的内存管理策略(例如共享内存、固定地址空间、低碎片场景)。
从源码结构看,fixed_pool直接继承自pool_base(见 include/oneapi/tbb/memory_pool.h),而pool_base是 oneTBB 所有内存池共用的接口基类,内部封装了对 scalable allocator 底层 RML(Resource Management Layer)池接口的调用。
开启预览特性与包含头文件
memory_pool.h属于 oneTBB 的预览特性(preview feature)。包含该头文件之前必须先定义宏TBB_PREVIEW_MEMORY_POOL,且值为 1,否则编译器会直接报错。这一点在源码中有强制校验:
#include "detail/_config.h" #if !__TBB_PREVIEW_MEMORY_POOL #error Set TBB_PREVIEW_MEMORY_POOL to include memory_pool.h #endif标准用法如下:
#define TBB_PREVIEW_MEMORY_POOL 1 #include "oneapi/tbb/memory_pool.h"另外,同族的memory_pool模板类与memory_pool_allocator模板类也共用同一个头文件,三者合称Scalable Memory Pools,详见 doc/main/reference/source/memory_allocation/scalable_memory_pools.rst。
类声明与语法
fixed_pool的声明形式非常简单:
class fixed_pool;其完整成员接口位于命名空间oneapi::tbb下:
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注意该类继承自no_copy,即不可拷贝、不可赋值,这是内存池类保持内部状态一致性的必要设计(源码注释也明确指出:"Pool interface is separate from standard allocator classes because it has to maintain internal state, no copy or assignment")。
遵循的 Memory Pool 命名要求
fixed_pool建模(models)oneTBB 参考手册中定义的Memory Pool 概念(Memory Pool Concept)。该概念以伪签名形式规定了所有内存池必须满足的语义(见 doc/main/reference/source/memory_allocation/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字节。 |
该概念的另一建模者是 memory_pool 模板类:memory_pool从底层分配器按需申请大块内存,而fixed_pool的所有内存都来自构造函数传入的固定缓冲区,这是两者最本质的区别。
构造函数:绑定固定缓冲区
fixed_pool的构造函数是使用该类的关键入口:
fixed_pool(void *buffer, size_t size) throw(std::bad_alloc);- buffer:指向由调用方提供的内存缓冲区起始地址;
- size:该缓冲区的字节大小;
- 异常:若运行时无法成功构造池实例,抛出
std::bad_alloc。
参考手册对构造函数的说明是:"构造一个内存池,用于管理 buffer 指向的、大小为 size 的内存。"
从源码实现看,构造函数远比表面更严格(include/oneapi/tbb/memory_pool.h):
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 均非法:
buffer == nullptr或size == 0会抛出std::invalid_argument。这一约束在测试中得到了验证——test/tbbmalloc/test_scalable_allocator.cpp 的TestSmallFixedSizePool用例专门断言"零大小池仅抛出std::invalid_argument",并验证了fixed_pool pool(nullptr, 10*1024*1024)必然抛出该异常。 - 底层通过 RML 池接口创建:构造函数向
rml::pool_create_v1提交一个MemPoolPolicy,其中granularity被设为整个缓冲区大小size,fixedPool标志置为true。MemPoolPolicy与MemPoolError的定义位于 include/oneapi/tbb/scalable_allocator.h,后者包含POOL_OK、INVALID_POLICY、UNSUPPORTED_POLICY、NO_MEMORY、NO_EFFECT五类错误码。 - 内存申请回调只触发一次:
allocate_request回调在首次需要内存时把整块缓冲区交给池,并将内部my_size置 0 标记"缓冲区已被使用"(include/oneapi/tbb/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; }这意味着同一块缓冲区在池的生命周期内只提交一次给底层分配器,池内部负责此后所有小块内存的切分与复用。
缓冲区大小的实际下限
测试代码给出了一条非常实用的经验值:TestSmallFixedSizePool以 0、3、9、27……递增的缓冲区大小反复构造池,并断言"16 字节分配能放进 16KB 的 slab,因此池至少需要约 16KB 空间才能工作"(test/tbbmalloc/test_scalable_allocator.cpp)。也就是说,缓冲区过小会导致池无法创建或无法完成任何分配——不是构造失败抛出异常,就是malloc返回nullptr。测试同时验证了"最大缓冲区大小必然足以创建可用的 fixed_pool"这一结论。
成员函数详解
void recycle();
释放池中所有已分配的内存对象,使池恢复到可继续分配的状态。源码实现为:
void recycle() { rml::pool_reset(my_pool); }recycle()对应rml::pool_reset,语义是"一次性释放池内全部对象"(见 include/oneapi/tbb/scalable_allocator.h)。与析构函数相比,recycle()不会销毁池本身,因此适合"分配一批对象 → 处理 → 整批回收"的高频复用场景,能显著减少重复建池的开销。
void *malloc(size_t size);
从池中分配size字节,返回指向该内存块的指针;分配失败时返回nullptr。实现直接转发给rml::pool_malloc:
void *malloc(size_t size) { return rml::pool_malloc(my_pool, size); }注意这与标准operator new的语义不同:malloc失败不抛出异常,而是返回空指针,调用方需要显式检查返回值。
void free(void* ptr);
释放此前由malloc/realloc返回的内存对象:
void free(void* ptr) { rml::pool_free(my_pool, ptr); }void *realloc(void* ptr, size_t size);
将ptr指向的内存对象重新分配为size字节,是pool_malloc的互补接口,源码注释指出它"允许一些底层优化可能性":
void *realloc(void* ptr, size_t size) { return rml::pool_realloc(my_pool, ptr, size); }测试用例专门验证了realloc不会破坏既有内容:先在池中分配 16 字节写入字符串"this is a test",再realloc为 15 字节,随后断言指针非空且字符串内容完整(test/tbbmalloc/test_scalable_allocator.cpp)。
析构函数
~fixed_pool() { destroy(); }析构时调用pool_base::destroy(),最终执行rml::pool_destroy(my_pool)(include/oneapi/tbb/memory_pool.h),释放池内部所有簿记结构与分配出的内存对象。
最小可用示例
参考手册给出的官方示例完整如下:
#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);这段代码演示了从固定池分配的最小闭环:构造 1MB 缓冲区上的池 → 分配 10 字节 → 释放。更贴近实战的写法建议显式检查返回值并包含异常处理:
#define TBB_PREVIEW_MEMORY_POOL 1 #include "oneapi/tbb/memory_pool.h" #include <cstdio> int main() { static char buf[1024 * 1024]; // 固定 1MB 缓冲区 try { oneapi::tbb::fixed_pool pool(buf, sizeof(buf)); void* p = pool.malloc(10); // 分配 10 字节 if (!p) { std::fprintf(stderr, "allocation failed\n"); return 1; } // ... 使用 p ... pool.free(p); // 归还给池 pool.recycle(); // 可选:一次性回收全部对象 } catch (const std::invalid_argument&) { std::fprintf(stderr, "invalid buffer/size\n"); } catch (const std::bad_alloc&) { std::fprintf(stderr, "pool creation failed\n"); } return 0; }将 fixed_pool 接入 STL 容器
fixed_pool自身并不满足 C++ 标准分配器(Allocator)要求,但 oneTBB 提供了配套的 memory_pool_allocator 模板类,它"为内存池提供 C++ 分配器接口",主要目的就是"让内存池可用于 STL 容器"。
其构造函数接受memory_pool &或fixed_pool &两种池实例,因此可以把fixed_pool无缝接入容器:
#define TBB_PREVIEW_MEMORY_POOL 1 #include "oneapi/tbb/memory_pool.h" #include <list> ... typedef oneapi::tbb::memory_pool_allocator<int> pool_allocator_t; std::list<int, pool_allocator_t> my_list(pool_allocator_t(my_pool));上述用法中my_pool既可以是oneapi::tbb::memory_pool<Alloc>的实例,也可以是oneapi::tbb::fixed_pool的实例。从源码看,memory_pool_allocator<T, P = pool_base>保存指向池的指针pool_type *my_pool,其allocate调用my_pool->malloc,deallocate调用my_pool->free,两个不同元素类型的分配器相等性(operator==)也以"是否指向同一个池"为判据(include/oneapi/tbb/memory_pool.h)。
注意一个重要的生命周期约束(参考手册对memory_pool的 caution 同样适用于嵌套场景):若底层分配器引用的是另一个可扩展内存池,则内层池必须在外层池被销毁或 recycle 之前销毁,否则会留下悬空引用。
使用注意事项与边界
综合参考文档、源码与测试,使用fixed_pool时应牢记以下边界:
- 缓冲区生命周期:池持有的是调用方缓冲区的指针而非所有权,缓冲区必须比池存活得更久,且在整个池生命周期内地址保持有效;池析构后缓冲区方可复用。
- 缓冲区只会被底层"消费"一次:
allocate_request内部通过将my_size置 0 保证"缓冲区不得使用两次"(源码断言信息原文:"The buffer must not be used twice.")。 - 零值与空指针非法:
size == 0或buffer == nullptr构造必然抛出std::invalid_argument。 - 过小缓冲区不可用:从测试看,池需要有足够的内部空间(约 16KB 级别)才能完成分配,否则
malloc返回nullptr。 - 不可拷贝:
fixed_pool继承自no_copy,既不能拷贝也不能赋值,只能通过引用传递。 - 分配失败语义:
malloc失败返回nullptr而不抛异常,务必检查返回值。
总结
fixed_pool是 oneTBB Scalable Memory Pools 中面向"固定缓冲区"场景的内存池实现。它以一块由调用方提供的定长内存为唯一来源,通过底层 RML 池接口(pool_create_v1/pool_malloc/pool_free/pool_realloc/pool_reset)提供线程安全、可扩展的malloc/free/realloc/recycle操作,并可通过memory_pool_allocator接入 STL 容器。对需要精确控制内存来源、追求低碎片或复用整块缓冲区的并行程序而言,它提供了一个开箱即用的标准答案;其约束(不可拷贝、缓冲区大小下限、一次性消费语义)则保证了实现的简单与确定性。
- 并发编程
- 高性能计算
【免费下载链接】oneTBB
oneAPI Threading Building Blocks (oneTBB)
相关推荐
mold 内嵌 oneTBB 的 fixed_pool 类深度解析:在定长缓冲区上构建可扩展内存池
mold 内嵌 oneTBB 的 fixed_pool 类深度解析:在定长缓冲区上构建可扩展内存池 本文以 oneTBB(oneAPI Threading Bu
开发工具构建工具系统编程eLabFTW终极指南:如何5步搭建免费开源的电子实验记录本系统
eLabFTW终极指南:如何5步搭建免费开源的电子实验记录本系统 还在为实验室数据管理混乱而烦恼吗?每天花大量时间整理实验笔记、追踪试剂库存、协调团队工作?让我
科研后端F´ 框架 Svc::BufferManager 组件深度解析:基于 Bin 池的固定大小内存缓冲管理
F´ 框架 Svc::BufferManager 组件深度解析:基于 Bin 池的固定大小内存缓冲管理 Svc::BufferManager 是 F´(F Pr
嵌入式系统编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考