CANN opbase 算子开发之 Object 基类:预留内存管理接口的定位、实现与调用链解析
2026/9/18 12:31:09 网站建设 项目流程

CANN opbase 算子开发之 Object 基类:预留内存管理接口的定位、实现与调用链解析

【免费下载链接】opbase本项目是CANN算子库的基础框架库,为算子提供公共依赖文件和基础调度能力。项目地址: https://gitcode.com/cann/opbase

本篇技术指南聚焦 CANN opbase 算子库基础框架中的op::Object类接口。Object是算子开发框架中一组预留接口,通过重载operator new/operator delete为派生对象提供统一的内存申请与释放入口。读完本文,你将掌握Object四个接口的语义与使用边界、其在 object.h 中的声明形式,以及背后经由 bridge_pool.cpp 的 BlockCache 与 HugeMemPool 双路径内存分配机制,并了解仓库 UT 对分配结果对齐性的验证方式。

接口定位:一组不建议开发者使用的预留接口

Object属于opdev命名空间下的基础能力之一,其文档位于 object.md。文档开篇即明确:

本章接口为预留接口,后续有可能变更或废弃,不建议开发者使用,开发者无需关注。

这是理解Object的关键前提:它不是面向算子开发者的稳定公开 API,而是框架内部为统一对象内存管理预留的基础设施。文档提供的接口列表如下:

接口定义功能说明
Object()Object 的构造函数。
new(size_t size)Object 的内存申请函数。
new(size_t size, [[maybe_unused]] const std::nothrow_t &tag)Object 的内存申请函数。
delete(void *addr)Object 的内存释放函数。

从使用约束上讲,开发者不应在自定义算子代码中直接依赖这些接口,未来版本可能对其签名、语义乃至存在性进行调整。

头文件中的接口形态:四组重载的声明细节

Object的完整声明位于 object.h,其形态比文档表格展示的更为完整。核心声明如下:

namespace op { class Object { public: Object() = default; virtual ~Object() = default; public: void* operator new(size_t size) throw(); void* operator new[](size_t size) throw(); void* operator new(size_t size, [[maybe_unused]] const std::nothrow_t& tag) throw(); void* operator new[](size_t size, [[maybe_unused]] const std::nothrow_t& tag) throw(); void operator delete(void* addr); void operator delete[](void* addr); }; } // namespace op

对照文档表格,可以从源码层面补充几点事实:

  • 构造与析构:构造函数与析构函数均以= default形式提供,其中析构函数为virtual,说明Object的设计意图是作为基类被派生类继承,通过虚析构保证派生对象释放时正确调用到完整的析构链。
  • 对象级与数组级重载:除文档表格列出的单对象形式外,头文件同时重载了operator new[]/operator delete[],覆盖new T[n]delete[]的数组分配场景,二者在实现上指向同一套底层逻辑。
  • nothrow形态:带const std::nothrow_t&参数的版本对应new (std::nothrow) T的调用方式,配合throw()动态异常说明,语义上保证分配过程不向外抛异常。
  • 头文件依赖:源码通过#include <new>引入std::nothrow_t,通过#include <cstddef>引入size_t(并以using std::size_t;注入当前作用域)。

实现要点:所有重载汇聚到同一对内部接口

Object的成员函数实现集中在 object.cpp,代码极其精简,全部重载最终汇聚到op::internal命名空间下的Allocate/DeAllocate两个内部函数:

void* Object::operator new(size_t size) throw() { return op::internal::Allocate(size); } void* Object::operator new[](size_t size) throw() { return op::internal::Allocate(size); } void* Object::operator new(size_t size, [[maybe_unused]] const std::nothrow_t& tag) throw() { return op::internal::Allocate(size); } void Object::operator delete(void* addr) { op::internal::DeAllocate(addr); } void Object::operator delete[](void* addr) { op::internal::DeAllocate(addr); }

从源码结构可以推断出几点设计意图:

  1. size参数在 nothrow 版本中被标记为[[maybe_unused]],且tag同样未参与逻辑,说明两种形态在分配行为上完全等价,nothrow 语义由throw()异常规格与内部实现共同保证,而非在重载内部做分支处理。
  2. 数组与单对象共享同一实现,框架不区分对象级与数组级分配的内存布局,简化了底层内存池的管理模型。
  3. 内部接口的声明位于 bridge_pool.h,与GetPoolIndexUpdateHugeMemIndexFreeHugeMemCheckDoubleFree等函数同属op::internal内存管理工具集,说明Object的内存语义与框架的线程本地内存池体系深度绑定。

底层调用链:BlockCache 与 HugeMemPool 双路径分配

Allocate/DeAllocate的实现位于 bridge_pool.cpp,其核心逻辑是依据线程本地上下文中的内存池索引做路径分派:

void* Allocate(size_t size) { int32_t id = op::internal::GetThreadLocalContext().poolIndex_; if (id != op::kInvalidHugeMemIndexId) { return GetAddr(id, size); } else { return op::internal::BlockCache::CacheAlloc(size); } } void DeAllocate(void* addr) { OP_CHECK(addr != nullptr, OP_LOGW("The address passed to DeAllocate is nullptr."), return); if (op::internal::BlockPool::InHugeMemRange(addr)) { // since huge mem pool use offset, so free just a dummy operation } else { op::internal::BlockCache::CacheFree(addr); } }

由此可以得到一条清晰的调用链事实:

  • 路径分派依据:分配走哪条路径由线程本地上下文poolIndex_决定。索引为op::kInvalidHugeMemIndexId时走 BlockCache(块缓存池),否则走 HugeMemPool(大页内存池)的GetAddr偏移寻址。
  • 释放的对称设计:释放时通过BlockPool::InHugeMemRange判断地址是否落在大页区间——大页池按偏移管理,释放是空操作;非大页区间则归还给BlockCache
  • 空指针防护DeAllocate入口对空指针做OP_CHECK校验并打印告警日志后直接返回,避免释放空指针导致未定义行为。
  • 双重释放防护bridge_pool.cpp中还实现了CheckDoubleFree,通过BlockStore::BlockHeadermagic_字段与cacheExt_状态位区分"活跃 block""已归还缓存链表""跨线程 free"等场景,为内存安全提供额外看护。

分配结果的对齐契约:UT 用例的实证

仓库在 test_alignment.cpp 中为Object::operator new的对齐行为编写了专门的单元测试,这为"对象地址满足STD_MAX_ALIGN对齐"这一实现事实提供了直接证据。测试注释明确说明该用例看护 Issue#318 的对齐修复——x86_64 + GCC 13 下movaps指令写 8 mod 16 地址触发 SIGSEGV 的崩溃现场。

测试覆盖了四条路径(test_alignment.cpp):

  1. BlockHeaderSizeIsAligned:编译期static_assert与运行期断言双重看护BlockHeader大小为STD_MAX_ALIGN(定义于 common_utils.h,即alignof(std::max_align_t))的整数倍、对齐度不小于STD_MAX_ALIGN
  2. MixedSequence_BlockCachePath:在aclCreateTensornew aclOpExecutornew aclTensor交织循环 20 轮的场景下,验证 BlockCache 路径返回地址 16 对齐且释放无异常。
  3. MixedSequence_HugeMemPath:通过InitHugeMemThreadLocal激活大页路径,额外断言InHugeMemRange为真,验证 offset 累加后地址仍保持对齐。
  4. PathSwitch_BlockCacheToHugeMem:模拟线程先从 BlockCache 路径运行、后切换到大页路径的真实 executor 场景。

该测试同时印证了Object的实际应用位置:aclTensoraclOpExecutor等框架对象经由Object::operator new获得内存,例如 kernel_tensor.h 中struct aclTensorExtend : public Object的继承关系。

使用建议与注意事项

综合以上文档与源码事实,在使用层面给出如下建议:

  • 遵循预留接口约束Object接口可能随版本变更或废弃,算子实现应避免直接依赖其签名,优先使用框架提供的高层 API(如aclCreateTensor)完成对象创建。
  • 理解而非绕过:尽管不建议直接调用,理解Object的分配语义有助于排查算子对象创建、释放相关的内存问题——例如地址对齐异常、跨线程释放、双重释放等,均可从 bridge_pool.cpp 与 block_pool.h 的实现中找到线索。
  • 继承场景注意虚析构:自定义类若继承Object,其虚析构会被自动纳入delete释放链路;释放顺序应遵循对象创建的反序,与 UT 中delete directTensor; delete exec; aclDestroyTensor(apiTensor);的模式一致。
  • 大页与缓存路径差异:两条分配路径在地址来源、释放语义上不同(大页池释放为空操作),若在算子代码中自行管理Object派生对象,需确保申请与释放经由同一套内部机制,避免内存池状态不一致。

总结

Object是 CANN opbase 基础框架中一组定位明确、实现精巧的预留接口:对外仅暴露四个文档化接口,对内则通过operator new/operator delete重载将全部对象级与数组级分配汇聚到统一的Allocate/DeAllocate,并依据线程本地上下文在 BlockCache 与 HugeMemPool 两条路径间分派,最终以 16 字节对齐契约支撑aclTensor等核心对象的稳定创建。对算子开发者而言,理解这条调用链的价值在于:即使不直接使用这些预留接口,也能在遇到对象内存相关异常时,迅速定位到 object.cpp、bridge_pool.cpp 与 test_alignment.cpp 等关键实现与验证点。

【免费下载链接】opbase本项目是CANN算子库的基础框架库,为算子提供公共依赖文件和基础调度能力。项目地址: https://gitcode.com/cann/opbase

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询