toy.struct_constant和toy.struct_access

📅 2026/7/16 19:42:43 👁️ 阅读次数 📝 编程学习
toy.struct_constant和toy.struct_access

在有了类型存储层StructTypeStorage后,编译器中还必须有具体的操作(Operations,简称 Ops)来创建和访问这个结构体。

在 MLIR Toy Tutorial 中,针对!toy.struct自定义类型,精心设计了两个互补的核心算子:toy.struct_constanttoy.struct_access。它们在计算图中分别扮演着“生产者”与“消费者”的角色。


1.toy.struct_constant—— 结构体的“生产者”

在编译器中,当用户在代码里写下类似于var a = [[1, 2], [3, 4]](假设这是一个结构体字面量)时,编译器需要在一个算子中把这些常量值打包封装起来。toy.struct_constant就是干这个活的。

📌 IR 展现形式

它在 MLIR 文本文件中长这样:

%0 = "toy.struct_constant"() { value = [dense<[1.0, 2.0]> : tensor<2xf64>, dense<3.0> : tensor<1xf64>] } : () -> !toy.struct<tensor<2xf64>, tensor<1xf64>>
  • 输入:它不需要运行时输入参数(括号内为空())。
  • 属性(Attributes):它携带了一个编译器静态已知的属性value,里面是一个数组,包含两个具体的稠密张量常量(dense<...>)。
  • 输出类型:返回一个完整的、带有具体成员类型签名的!toy.struct

🛠️ TableGen (ODS) 定义

Ops.td中,它的定义重点在于约束它的value属性和返回值类型:

def StructConstantOp : Toy_Op<"struct_constant", [Pure]> { let summary = "结构体常量生成算子"; // 关键属性:存储具体数据的 ArrayAttr let arguments = (ins ArrayAttr:$value); // 关键输出:输出必须是一个 Toy 结构体类型 let results = (outs Toy_StructType:$output); }

2.toy.struct_access—— 结构体的“消费者”

有了结构体,程序总得去读里面的数据。toy.struct_access类似于 C 语言中的点运算符(如my_struct.member),专门用来根据索引(Index)提取结构体中的某个特定数组成员

📌 IR 展现形式

// 语义:从刚才生成的结构体 %0 中,提取索引为 0 的那个成员 %1 = "toy.struct_access"(%0) { index = 0 : i64 } : (!toy.struct<tensor<2xf64>, tensor<1xf64>>) -> tensor<2xf64>
  • 输入:接收一个已经生成的!toy.struct变量。
  • 属性(Attributes):携带一个静态的整数属性index,表明我们要拿第几个成员(从 0 开始计数)。
  • 输出类型:它的输出类型(这里是tensor<2xf64>)必须严格等于该结构体中对应索引位置的那个类型。如果类型对不上,MLIR 的验证器(Verifier)就会报错。

🛠️ TableGen (ODS) 定义

def StructAccessOp : Toy_Op<"struct_access", [Pure]> { let summary = "结构体成员访问算子"; // 输入:一个结构体对象 + 一个静态的索引值 let arguments = (ins Toy_StructType:$input, I64Attr:$index); // 输出:解包后的任意 MLIR 类型(通常是底层 tensor) let results = (outs AnyType:$output); // 必须声明自定义的验证逻辑 let hasVerifier = 1; }

3. 核心硬核点:struct_access的 C++ 验证器(Verifier)

由于 MLIR 强调强类型安全,你不能随意去 access 一个不存在的索引,或者宣称拿出来的类型与结构体内部存的类型不一致。因此,博客在介绍struct_access时,一定会重点贴出它的 C++verify()函数实现:

LogicalResultStructAccessOp::verify(){// 1. 获取输入结构体的类型指针autostructType=llvm::cast<StructType>(getInput().getType());// 2. 获取你想要访问的 index 值uint64_tindex=getIndex();// 3. 安全检查:索引不能越界if(index>=structType.getElementTypes().size())returnemitOpError()<<"访问越界!结构体只有 "<<structType.getElementTypes().size()<<" 个成员";// 4. 类型强一致性检查:算子声明的输出类型 必须等于 结构体该位置的真实类型Type expectedType=structType.getElementTypes()[index];if(getOutput().getType()!=expectedType)returnemitOpError()<<"类型不匹配!期望获得 "<<expectedType<<",但算子输出声明为了 "<<getOutput().getType();returnsuccess();}

4. 它们在 Lowering(阶段性降低)时去哪了?

当编译器把代码向下推进到LLVM Dialect(底层中间表示)准备对接libc或生成汇编时,这两个高层算子会发生巨大的蜕变:

  • toy.struct_constant:在高级抽象中它是一体化的。降低到低层时,它会被完全解构(Flatten)。编译器会分别为内部的每一个 Tensor 常量生成底层的llvm.mlir.global全局静态内存,并生成一个llvm.struct把这些内存描述符指针打包装进去。
  • toy.struct_access:会被直接映射并降低为 LLVM Dialect 的llvm.extractvalue指令。该指令在硬件物理层面非常高效,它直接通过相对结构体基地址的偏移量(Offset),一条指令把目标成员的内存指针给抠出来。

💡 总结

struct_constantstruct_access是高级语言复合类型在编译器中间表示(IR)里的具体体现。一个负责把离散的数据包起来,一个负责按图索骥把数据掏出来。有了它们,MLIR 才能在高级图层面上完成复杂的结构体数据流分析与优化,并在最后一刻安全地交付给底层的llvm.struct和硬件寄存器。