系列文章目录
章节1:什么是MLIR
章节2:MLIR的引入
章节3:MLIR完成一个端到端的流程
章节4:编写一个pass
章节5:MLIR相关语法
目录
系列文章目录
一、TableGen中的Trait
核心区别对比
1. 使用方式:混入 vs 实例化
2. 生命周期:编译期 vs 运行时
3. 存在形式:必须依附于 ConcreteType
4. 方法调用:框架调度 vs 显式调用
5. 设计目的:声明 + 注入 vs 封装
6. 组合性:可叠加
一个类比
一句话总结
二、TableGen中def的作用
1. def 的基本作用
2. 在 MLIR ODS 中,def 具体定义什么?
三、operation类和op类
核心区别:运行时容器 vs. 编译期包装
它们如何协同工作?
一个直观的例子
总结
总结
一、TableGen中的Trait
从 C++ 语法层面看,MLIR 的 Trait就是一个类模板,和普通类没有本质区别——它也能有成员变量、成员函数、静态函数、继承关系。但从设计意图和使用范式上看,它和普通类有几点关键区别。
📊 核心区别对比
| 维度 | 普通类 | MLIR Trait |
|---|---|---|
| 使用方式 | 实例化对象,通过对象调用 | 作为基类混入到 Op/Attribute/Type 中 |
| 生命周期 | 运行时存在对象实例 | 编译期生效,不独立实例化 |
| 存在形式 | 可以独立存在 | 必须依附于某个ConcreteType |
| 方法调用 | 显式调用 | 成员方法被宿主继承;静态钩子由框架自动调度 |
| 设计目的 | 封装数据和行为 | 声明约束 + 注入通用实现 |
| 组合性 | 单继承为主 | 一个 Op 可叠加多个 Trait |
1. 使用方式:混入 vs 实例化
普通类你会new一个对象或用栈对象:
cpp
MyClass obj; // 实例化 obj.doSomething(); // 通过对象调用
Trait从不被单独实例化。它的用法是作为模板参数“混入”到宿主类型里:
cpp
class MyOp : public Op<MyOp, MyTrait> { ... }; // 这里没有 MyTrait 的对象,只有 MyOp 的对象MyTrait的代码通过继承成为MyOp的一部分,但你在运行时拿不到一个“MyTrait实例”。
2. 生命周期:编译期 vs 运行时
普通类的方法在运行时通过对象调用,可能有虚函数动态分发。
Trait 的成员方法在编译期就通过继承成为宿主的一部分,调用是静态解析的,没有虚表开销。Trait 的静态钩子(verifyTrait、foldTrait)也是框架在编译期就知道该调用哪个,属于静态多态。
3. 存在形式:必须依附于 ConcreteType
普通类可以独立存在。Trait 的模板参数ConcreteType意味着它必须知道“自己附着在谁身上”。一个MyTrait不能脱离具体的 Op 类型而存在——它存在的意义就是“给某个 ConcreteType 添加能力或约束”。
这也是为什么 Trait 用CRTP:基类TraitBase需要知道派生 Trait 和宿主类型,才能在编译期完成静态调度。
4. 方法调用:框架调度 vs 显式调用
普通类的方法,你不调用它就不会执行。
Trait 的静态钩子是框架按约定自动调用的:
verifyTrait:MLIR 验证 IR 时自动调用foldTrait:常量折叠时作为回退自动调用printTrait/parseTrait:自定义打印/解析时自动调用
你只要把 Trait 挂上去,这些逻辑就在合适的时机自动生效,不需要使用者手动触发。这是普通类没有的“框架回调”机制。
5. 设计目的:声明 + 注入 vs 封装
普通类的核心目的是封装数据和操作。Trait 的核心目的是:
声明:告诉 MLIR 世界“这个 Op 具备什么属性”(如
Commutative、Terminator)注入:把通用逻辑(验证、折叠、打印)一次性写好,自动赋给所有使用者
它更像 C++ 里的mixin或 C++20 的concept,而不是传统意义上的“对象类”。
6. 组合性:可叠加
一个 Op 可以同时附加多个 Trait:
cpp
class MyOp : public Op<MyOp, TraitA, TraitB, TraitC> { ... };每个 Trait 各自注入自己的方法和钩子,互不冲突(同名方法需要宿主自己覆盖解决)。普通类单继承下很难做到这种灵活的横向能力组合。
一个类比
| 概念 | 类比 |
|---|---|
| 普通类 | 一个独立的产品,有完整的数据和行为 |
| Trait | 一张能力标签/插件,贴到产品上就赋予它额外能力和约束 |
Trait 不是“另一种类”,而是一种用类模板实现的、面向框架回调的、编译期混入机制。语法上它是类,语义上它是“给宿主类型打的标签 + 附赠的代码”。
一句话总结
普通类回答“我是什么、我能做什么”;Trait 回答“宿主具备什么属性、框架该在什么时候调用我注入的逻辑”。前者是独立对象,后者是依附于宿主的编译期能力模块。
二、TableGen中def的作用
ef是 TableGen 用来“定义一条具体记录(record)”的关键字。它本身不是 C++ 的def,也不是运行时执行的东西;它的作用是在.td文件里声明一个具体实体,然后由mlir-tblgen解析并生成对应的 C++ 代码。
1.def的基本作用
TableGen 里常见两种定义:
tablegen
class MyClass<...> { ... } // 定义“模板/抽象记录” def MyRecord : MyClass<...>; // 定义“具体记录”class:定义可复用的模板,通常带参数,不直接生成最终实体。def:基于某个 class 或基类,定义一个具体的、有名字的记录。
例如:
tablegen
def MyOp : Op<"my.op", [Pure]> { let summary = "my op"; let arguments = (ins I32:$input); let results = (outs I32:$output); }这里def MyOp就是在 ODS 中定义了一个名为MyOp的具体操作。
2. 在 MLIR ODS 中,def具体定义什么?
def可以定义 MLIR 中的多种实体:
def定义的对象 | 例子 | 作用 |
|---|---|---|
| Operation | def AddOp : Op<"arith.addi", ...> | 定义一个具体算子 |
| Attribute | def MyAttr : AttrDef<...> | 定义一个自定义属性 |
| Type | def MyType : TypeDef<...> | 定义一个自定义类型 |
| Pattern | def MyPattern : Pat<...> | 定义一个重写模式 |
| Interface | def MyInterface : OpInterface<...> | 定义一个操作接口 |
| Dialect | def MyDialect : Dialect | 定义一个方言 |
def在 MLIR/TableGen 中的作用是:定义一个具体的记录(Operation、Attribute、Type、Pattern 等),给它一个唯一名字,并描述它的结构;TableGen 后端再根据这些def生成 C++ 代码。它本身不执行任何逻辑,而是声明式地描述“有什么”,把重复的 C++ 样板代码交给工具生成。
三、operation类和op类
Operation 是一个类(class);Op 是一个类模板(class template),本身不能直接实例化,必须提供模板实参,它本身几乎不持有数据,只是一个包装 Operation* 的强类型外壳,具体的操作类,比如AddOp、ConstantOp,是通过继承Op<AddOp, ...>实例化出来的类。
在MLIR中,Operation和Op是两层不同的抽象,服务于不同的场景。简单说,Operation是运行时那个“具体的东西”,而Op是你在C++代码里用来方便、安全地操作它的“类型化包装”。
核心区别:运行时容器 vs. 编译期包装
| 维度 | mlir::Operation | mlir::Op<...>(及各派生类) |
|---|---|---|
| 角色 | 运行时通用容器,代表IR中一个实际存在的操作指令 | 编译期类型安全包装,是访问Operation的“智能指针” |
| 类型信息 | 类型擦除,不知道自己是“加法”还是“常量”,只持有操作名(字符串)等通用信息 | 强类型,AddOp、ConstantOp等具体类明确知道自己是什么操作 |
| 核心职责 | 存储所有操作共有的数据:操作数、结果、属性、位置、所属区块等 | 提供操作特定的、语义化的API,如getLhs()、getRhs()等便捷访问器 |
| 使用方式 | 在Pass中遍历时,通常以Operation*指针形式出现 | 按值传递和持有,内部仅包含一个Operation*指针,非常轻量 |
它们如何协同工作?
Operation是数据的“真身”,Op是指向它的“遥控器”。
Operation持有全部数据:所有操作数、结果、属性等实际数据,都存储在Operation对象内部。Op派生类自身通常不包含任何数据成员,它只是一个空壳,所有数据都委托给它所包装的那个Operation实例。Op是轻量包装:mlir::Op<ConcreteType, Traits...>是一个模板类,它在编译期通过ConcreteType(如AddOp)和Traits获得类型信息,从而提供类型安全的访问方法。它的本质就是一个包装了Operation*的强类型智能指针。
一个直观的例子
在MLIR的Toy教程中,ConstantOp代表一个无输入、单输出的常量操作。它本身不存储那个常量值,值存储在它所包装的Operation对象里。ConstantOp的职责是提供getValue()这类类型安全的方法,让你方便地取出那个值。
当你从通用的Operation*得到特定的Op时,用的是dyn_cast;反之,从Op取回底层的Operation*则用getOperation():
cpp
void process(mlir::Operation *operation) { // 从通用 Operation* 转换为类型安全的 ConstantOp ConstantOp op = llvm::dyn_cast<ConstantOp>(operation); if (!op) return; // 如果不是 ConstantOp,转换失败 // 从 Op 取回底层的 Operation* mlir::Operation *internal = op.getOperation(); // internal == operation 为 true }总结
Operation回答“IR里实际存在什么”,是通用的、类型擦除的运行时实体;Op回答“我该如何方便、安全地操作它”,是具体的、类型安全的编译期包装。在Pass中处理未知操作时用Operation*,需要调用特定操作的语义化方法时,就dyn_cast到对应的Op类