matxscript运行时系统之内存系统
仓库链接:bytedance/matxscript: A high-performance, extensible Python AOT compiler.
概述
matxscript运行时系统的内存管理机制负责高效地分配和回收运行时环境中所有对象的内存。该系统基于MemoryPoolAllocator分配器实现,结合对象模型中的引用计数机制,实现了自动垃圾回收。系统支持在不同设备上进行内存分配,并通过DeviceAPI与底层硬件交互。
内存管理系统的核心目标是在保证内存安全的前提下,最大化内存分配和回收的效率,为高性能计算提供基础支撑。
核心组件架构
内存管理系统主要由以下几个核心组件构成:
- MemoryPoolAllocator: 主要的内存分配器实现
- ObjAllocatorBase: 分配器基类模板,提供通用的make_object接口
- ObjectPtr: 智能指针类,负责对象引用计数管理
- 全局内存分配器实例: global_memory_allocator
内存管理系统采用分层设计,顶层通过make_object函数提供统一的对象创建接口,底层通过MemoryPoolAllocator实现具体的内存分配策略。ObjectPtr智能指针负责管理对象的生命周期和引用计数。
分配器设计与实现
ObjAllocatorBase模板类
ObjAllocatorBase是一个使用奇异递归模板模式(CRTP)实现的基类模板,为各种具体的分配器提供通用的make_object接口。
1 | template <typename Derived> |
这种设计允许派生类(如MemoryPoolAllocator)定义特定的Handler来处理不同类型对象的分配和释放。
这段代码就是一个典型的 CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)的例子。先解释这段代码本身,再结合 CRTP 的思想讲清楚“为什么要这么写”。
1 | template <typename Derived> |
这段代码具体干了什么?
ObjAllocatorBase<Derived> 是一个“分配器基类模板”。真正的分配器类会这样继承它:
1 | class SimpleObjAllocator : public ObjAllocatorBase<SimpleObjAllocator> { |
然后使用时:
1 | SimpleObjAllocator alloc; |
make_object 的步骤:
using Handler = typename Derived::template Handler<T>;
Derived在这个例子里就是SimpleObjAllocator。Derived::template Handler<T>表示:由具体分配器提供一个Handler<T>,里面定义这个类型T的创建和删除方式。- 这里用
template关键字是因为Handler自身是模板,语法上必须加。
static_assert(std::is_base_of<Object, T>::value, ...)
- 保证
T一定是继承自Object的类型。 - 因为后面要写
ptr->type_index_、ptr->deleter_,这些字段是在Object里定义的。
T* ptr = Handler::New(static_cast<Derived*>(this), std::forward<Args>(args)...);
- 调用
Handler<T>::New(...)来构造一个T对象。 static_cast<Derived*>(this)把当前ObjAllocatorBase<Derived>*转成真实的Derived*(例如SimpleObjAllocator*),传给New。Args&&...通过std::forward完美转发给T的构造函数。- 也就是:真正的分配逻辑由派生类
**Derived**决定。
ptr->type_index_ = T::RuntimeTypeIndex();
- 给对象
T填上它自己的运行时类型编号,用于 RTTI/类型擦除框架。
ptr->deleter_ = Handler::Deleter();
- 从
Handler<T>里拿到专门销毁T的 deleter(你前面贴过的Deleter_那类函数),存到对象里。 - 以后
Object被销毁时,就通过这个函数来正确析构/释放内存。
- 把
ptr封装进ObjectPtr<T>返回。
什么是 CRTP(奇异递归模板模式)?
CRTP 的典型形式是:
1 | template <typename Derived> |
特点:
- 模板参数
Derived是“派生类本身”;也就是“基类用派生类作为模板参数”——看起来像个递归,所以叫“奇异递归模板模式”。 - 这样,
Base里有机会写出依赖于Derived的代码,比如:
1 | static_cast<Derived*>(this)->某个成员函数(); |
换句话说:基类模板可以在编译期“知道”派生类的类型,并调用派生类提供的接口,而不需要虚函数和运行时多态。
在你这个例子里:
1 | template <typename Derived> |
Derived就是具体的分配器实现类,比如SimpleObjAllocator、PoolObjAllocator等。ObjAllocatorBase通过Derived::Handler<T>和static_cast<Derived*>(this)调用派生类提供的逻辑。- 所以:不同的 Derived,可以提供不同的 Handler
::New / Handler 。::Deleter,从而实现不同的分配策略(普通 new/delete、对象池、共享内存等)
为什么要用 CRTP,而不是虚函数?
如果不用 CRTP,你可能会考虑这样写:
1 | class ObjAllocatorBase { |
问题:
- 需要虚函数表,
ObjAllocatorBase对象有运行时开销。 - 每次调用分配/删除都是运行时多态(虚调用),有一点性能成本。
- 对于模板类型
T的行为细化(比如Handler<T>)不太好做:只能写非模板虚函数或受限模板。
而 CRTP 的特性:
-
所有决策在编译期完成:
-
Derived::Handler<T>是模板,针对每种T实例化不同的 Handler。 -
Handler::New(...)是静态函数,调用时可以内联优化。 -
不需要虚表,不发生虚调用,多态完全在编译期展开。
-
适合像对象分配/销毁这种高频、性能敏感的操作。
完整链路
整体结构:
1 | ObjAllocatorBase<Derived> |
CRTP 的作用在这里就是:
- 让基类
ObjAllocatorBase能“看到”派生类Derived的Handler<T>。 - 让
make_object成为通用模板逻辑,而具体分配/销毁方式由不同Derived实现。
一句话总结 CRTP 在这里干的事
ObjAllocatorBase<Derived>:提供统一的“API”(如make_object<T>),不管分配器是什么实现。Derived:实现具体的策略(普通分配、内存池、共享内存等),通过Handler<T>告诉基类:T应该怎么创建和销毁。- CRTP 让基类在编译期就能“知道”派生类的实现细节,从而写出“统一接口 + 策略可插拔 + 无虚函数开销”的代码。
如果你愿意,我可以写一个最小可运行的完整例子:一个 SimpleAllocator 和一个 PoolAllocator,都继承 ObjAllocatorBase,展示如何通过 CRTP 和 Handler<T> 共用这套 make_object。
MemoryPoolAllocator实现
MemoryPoolAllocator是运行时系统使用的默认内存分配器,它为不同类型和大小的对象提供了不同的内存管理策略:
MemoryPoolAllocator通过Handler模板机制为不同类型提供专门的分配和释放操作:
- 普通对象Handler: 处理一般对象的分配和释放
- 数组对象ArrayHandler: 处理数组类型的分配和释放
1 | // Simple allocator that uses new/delete. |
自定义对象构造函数
1 | using StorageType = typename std::aligned_storage<sizeof(ArrayType), alignof(ArrayType)>::type; |
这段代码是一个“自定义对象构造函数”,核心是用 std::aligned_storage 定义了一块“原始内存”,然后在这块内存上用 placement new 构造出一个 T 类型的对象,实现内存分配与对象构造分离,并返回指针。
逐行解析如下:
1 | using StorageType = typename std::aligned_storage<sizeof(T), alignof(T)>::type; |
- 对于某个类型
T,std::aligned_storage<sizeof(T), alignof(T)>::type是一块原始的、未初始化的内存类型,大小为sizeof(T),对齐为alignof(T)。 - 换句话说,
StorageType就是一块足够存放一个T对象且对齐正确的“字节缓冲区”。
1 | template <typename... Args> |
- 这是一个静态模板函数
New,返回类型为T*。 - 第一个参数类型是
MemoryPoolAllocator*,但从注释看,在SimpleObjAllocator的实现里暂时用不到,只是为将来需要“对象池”等复杂 allocator 接口预留。 Args&&... args是转发引用,用于完美转发构造参数给T的构造函数。
注释中说的:
- 为对象池预留:对象池可能需要在内存块内藏一些“指向分配器自身的指针”,以便在 delete 时能够回收到正确的 pool。
- NOTE2: 使用 placement new 来构造对象,避免一些删除带虚函数却无虚析构函数时的警告,以及为对象池提供合适的 storage 形式。
核心逻辑:
1 | StorageType* data = new StorageType(); |
- 这里用普通的
new为StorageType分配一块内存并默认构造一个StorageType对象。 StorageType通常是类似struct { unsigned char bytes[N]; }这样一个“纯字节”类型,它的默认构造一般是 trivial 的(什么都不做)。- 这一步本质上只是从全局/堆上分配了一块大小和对齐都符合
T要求的内存空间。
1 | new (data) T(std::forward<Args>(args)...); |
- 这是 placement new:在已经分配好的地址
data上“就地构造”一个T对象。 - 调用的是
T的构造函数,参数通过std::forward完美转发,保留左值/右值属性。 - 构造完成以后,在
data这块StorageType内存中,就真正存在了一个T类型的对象。
1 | return reinterpret_cast<T*>(data); |
- 因为现在
data这个地址处确实存放着一个T对象,所以可以把StorageType*转成T*返回。 - 后续使用时,就可以像普通的
T*一样访问它。
整体含义总结:
New函数以一种“底层自管理”的方式为T分配原始字节存储,并在这块存储上构造一个T。- 之所以使用
std::aligned_storage和 placement new,而不直接new T(...):
- 为复杂的自定义 allocator(如对象池)提供灵活性:可以把 allocator 的信息和对象放在同一块内存里,并在自定义 deleter 中把对象返回给 allocator。
- 可以避免某些关于“通过基类指针 delete 派生类对象但基类没有虚析构”的编译器警告(通过自定义的释放逻辑来管理对象生命周期,而不是直接
delete)。 aligned_storage给的是“原始内存类型”,特别适合在内存池、对象池中统一管理内存块,而不是直接使用 C++ 的对象类型。
自定义对象删除函数
1 | static void Deleter_(Object* objptr) noexcept { |
这段 Deleter_ 和你前面贴的 New 是配套的:New 用 StorageType+placement new 构造对象,这里用显式析构 + delete StorageType* 来销毁同一块内存。
逐行说明:
1 | static void Deleter_(Object* objptr) noexcept { |
- 静态函数,签名类似“通用 deleter”:接收一个基类指针
Object*。 noexcept表示这个删除函数不应该抛异常(析构/删除通常都应保证不抛)。
1 | // NOTE: this is important to cast back to T* |
- 真实对象类型是
T,但调用时传进来的是Object*(T继承自Object)。 - 需要把
Object*再转换回T*,才能调用T的析构函数,并正确计算对应的StorageType*。 - 为何强调“可能不一样”:有些类可能通过自定义 allocator 或对象池在更大的块里放对象,基类指针指向的地址不一定和真正管理的内存块结构一一对应,所以要小心转换。
1 | // It is important to do tptr->T::~T(), |
关键点:
tptr->T::~T();是显式调用**T**的非虚析构函数。- 如果写成
tptr->~T();,语义上有可能被理解为“调用(可能是虚的)析构函数”,这在某些上下文会涉及虚析构相关的机制。 - 但这里的设计是:基类
Object可以没有虚析构函数,通过自定义 deleter 来弥补多态删除问题。 - 因为我们本来就知道实际类型是
T,直接指定调用T的析构函数就行,不依赖虚函数表,也规避编译器关于“基类无虚析构通过基类指针 delete 派生对象”的警告。
总结:tptr->T::~T(); 语法是“指定类名的析构函数调用”,不走虚调用,确定就是这个类的析构。
1 | delete reinterpret_cast<StorageType*>(tptr); |
- 与
New中的分配逻辑对应:
1 | StorageType* data = new StorageType(); |
- 所以这里反向操作:
- 先在
tptr上调用T的析构(上面那行)。 - 再把
T*转回当初的StorageType*:
1 | reinterpret_cast<StorageType*>(tptr) |
delete这块StorageType内存。
- 这就完成了“placement new + 显式析构 + delete 原始存储”的闭环。
整体流程回顾:
- 创建时:
StorageType* data = new StorageType();new (data) T(args...);- 返回
reinterpret_cast<T*>(data);
- 销毁时:
- 得到
Object* objptr→ 转成T* tptr。 - 调用
tptr->T::~T();显式析构。 delete reinterpret_cast<StorageType*>(tptr);释放当初那块原始存储。
这么设计的目的:
- 避免要求基类
Object有虚析构函数(减少对象体积、虚表开销)。 - 通过“外部注册的 deleter 函数”来正确销毁真实类型
T。 - 与前面的
aligned_storage+ placement new 的内存分配方式对称,适合对象池等自定义分配器场景。
特殊析构函数
tptr->T::~T(); 这句语法本身有点“奇怪”,但它是标准 C++ 里的显式析构调用的一种形式。我们拆到最细来解释。
普通的析构调用长什么样?
最常见的写法是:
1 | tptr->~T(); |
含义:对指针 tptr 指向的对象,调用类型为 T 的析构函数。
如果 T 有虚析构,并且 tptr 是多态指针(如 Base* 指向 Derived 对象),这时根据上下文可能触发虚调用机制(但这里细节很多,与实际静态类型和动态类型相关)。
tptr->T::~T(); 语法结构是什么?
这行可以拆解为:
1 | (tptr) -> T :: ~T(); |
也就是:
tptr:指向某对象的指针->:通过指针访问对象T:::指定“类 T 的成员”(这里是析构函数)~T():这个成员就是T的析构函数
组合起来:“对 **tptr** 指向的对象,调用类 **T** 的析构函数 **T::~T()**”。
它和下面这句在语义上类似:
1 | tptr->~T(); // 普通写法 |
只是多加了一个限定名 T::,意思是“明确说:我就是要调用 T 这个类的析构函数,而不是靠多态、名字查找等机制来决定”。
为什么要写成 tptr->T::~T(); 而不是 tptr->~T();?
核心原因是“不要走虚析构或多态逻辑,而是明确调用一个具体类的析构”。
在这段代码的使用场景里:
- 真实对象类型为
T - 有一个基类
Object,但并不一定有虚析构函数 - 这个
Deleter_是用来“知道真实类型是 T 时,明确清理 T 对象”的
如果你写的是:
1 | tptr->~T(); |
- 有些人从语义上会理解为“可能是虚析构”(虽然实际是否虚要看
T的定义) - 有时工具或代码审查会拿这点做文章:你是不是想通过多态方式删除?
- 而这里的设计理念是:我们根本不想依赖虚析构,因为我们已经知道具体类型就是 T,由外部 deleter 管理多态删除问题。
写成:
1 | tptr->T::~T(); |
则非常明确地告诉编译器和读代码的人:
- 调用的就是
T这个类的析构函数 - 不通过基类类型、不走虚表、不参与多态决策
- 就是一个“指定类名的显式析构调用”
这在语义上更“硬”,更符合这套“手工管理生命周期”的设计。
显式调用析构函数在 C++ 里的常见形式
显式调用析构有几种常见写法:
1 | obj.~T(); // 对栈上对象 obj 显式调用 T 的析构 |
特别是最后一种:
1 | Base* p = new Derived; |
可见“对象表达式 -> 类名 :: ~类名()”这类写法,就是在显式指定要调用哪个类的析构函数。
结合你的代码场景再串一遍
你的整体代码流程是:
- 用
std::aligned_storage分配一块“原始内存”作为StorageType - 在这块内存上
new (data) T(args...)构造对象 - 返回
reinterpret_cast<T*>(data)给外部使用 - 删除时走
Deleter_(Object* objptr):
T* tptr = static_cast<T*>(objptr);tptr->T::~T();// 显式析构 Tdelete reinterpret_cast<StorageType*>(tptr);// 释放那块原始存储
这套模式明确地把:
- 构造(placement new)
- 析构(显式调用析构)
- 内存释放(delete StorageType*)
三个步骤拆开管理,不再依赖类型本身的 operator new/delete,也不依赖虚析构。
在第 2 步,为了不让人误以为你是通过虚析构删除多态对象,作者特意用了较“重”的语法:tptr->T::~T();,强行指定“调用 T 的析构函数本身”。
对象创建流程
对象创建是内存管理系统的核心功能,通过make_object函数实现。以下是完整的对象创建流程:
整个过程包括以下关键步骤:
- 应用程序调用make_object模板函数
- 函数转发调用到global_memory_allocator的make_object方法
- 根据对象类型选择相应的Handler进行内存分配
- 在分配的内存上构造对象实例
- 设置对象的类型信息和删除器
- 将对象包装在ObjectPtr智能指针中返回
内存区域管理策略
为了优化不同大小对象的内存分配效率,string_core采用了三种不同的存储策略
小型对象(Small Objects)
- 大小:最多23个字符(在64位系统上)
- 存储方式:直接内联在对象内部,无需额外分配
- 优势:零分配开销,最佳缓存局部性
中型对象(Medium Objects)
- 大小:24到254个字符
- 存储方式:通过malloc分配独立内存块
- 特点: eager复制语义,每次拷贝都会分配新内存
大型对象(Large Objects)
- 大小:超过254个字符
- 存储方式:引用计数的内存块,支持lazy复制
- 特点:写时复制(Copy-on-Write),提高大字符串操作效率
引用计数与内存回收
内存管理系统通过ObjectPtr智能指针实现自动引用计数管理:
1 | template <typename T> |
当对象的引用计数降到0时,会自动调用预设的删除器进行内存释放,从而实现自动垃圾回收。
总结
matxscript运行时的内存管理系统通过精心设计的分配器架构和智能指针机制,实现了高效的内存管理和自动垃圾回收。系统的主要特点包括:
- 灵活的分配器设计: 通过模板和CRTP模式实现可扩展的分配器架构
- 多级内存管理: 针对不同大小的对象采用最优的存储策略
- 自动引用计数: 通过ObjectPtr实现内存的自动管理
- 高性能实现: 使用in-place new、内存池等技术优化性能
这套内存管理系统为matxscript运行时提供了稳定、高效的内存基础,支撑着整个系统的高性能运作。
- 标题: matxscript运行时系统之内存系统
- 作者: 鱿鱼圈
- 创建于 : 2025-11-13 23:50:00
- 更新于 : 2026-06-12 17:14:40
- 链接: https://yuyanqi.com/2025/11/13/matxscript内存管理/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。