4. 一些ODS定义
4.1. 特性(trait)
MLIR支持一个完全开放的生态系统,因为任意方言都可以定义适合特定抽象层次的属性(attribute)、操作(operation)以及类型(type)。特性(trait)是抽象出实现细节以及在许多不同属性/操作/类型等之间通用的属性(properties)的机制。特性可用于说明这个对象特殊的属性以及约束,包括一个操作是否有副作用,或者它的输出是否与输入有相同的类型。操作特性的一些例子有Commutative、SingleResult、Terminator等。
4.1.1. 特性AttrSizedOperandSegments
特性AttrSizedOperandSegments是这样定义的:
1777 def AttrSizedOperandSegments : NativeOpTrait<"AttrSizedOperandSegments">;
Mlir codegen看到这个特性会为Op对象生成一个名为operand_segment_sizes的字典属性项。这个属性说明每个ODS声明的操作数(变长或定长)对应多少个实际操作数。它用于具有多个变长操作数,但它们的大小关系静态未知。这个属性必须是与ODS声明操作数的元素个数相同的1维向量。这意味着即使某些操作数不是变长的,这个属性仍然需要有一个元素对应其大小,这将总是1。
4.1.2. 特性AttrSizedResultSegments
这个类与AttrSizedOperandSegments是类似的,只不过适用于Op的结果,对应于字典中"result_segment_sizes"一项。
4.2. Variadic
Variadic是这样定义的:
308 class Variadic
309 Type baseType = type;
310 }
这个定义用在Op的arguments列表中,比如(AffineOps.td):
69 let arguments = (ins AffineMapAttr:$map, Variadic
这里,Index也是一个定义好的ODS类型:
434 def Index : Type
435 BuildableType<"$_builder.getIndexType()">;
一个Op允许使用任意多具有Variadic特性的操作数,前提是这些操作数所包含的子操作数的个数是相同;而且这些Variadic特性操作数可与非Variadic特性操作数混合使用。
区别:AttrSizedXXX用作操作的属性,描述的是操作所面对的操作数的形式。Variadic在这里通常用于描述为操作生成的方法的变长参数。
另外,在这里及其他地方看到的变长数据类型(比如VariadicRegion)都只是占位符,它们告诉mlir-tlbgen这个占位符对应的MLIR-IR是变长的,要相应地生成解析代码。
4.3. 区域
4.3.1. 定义
区域(region)是MLIR块的有序列表。区域里的语义不是IR施加的。相反,包含它的区域的操作定义了区域的语义。MLIR当前定义了两种类型的区域:SSACFG区域,它描述了基本块之间的控制流;以及Graph区域,它不要求基本块之间的控制流。操作里区域类型由RegionKindInterface描述。
区域没有名字或地址,仅包含在区域里的基本块有。区域被包含在操作内,并且没有类型或属性(attributes)。区域里的第一个基本块是称为“入口块”的特殊基本块。入口块的参数也是区域的参数。入口块不能作为其他基本块的后继者。
函数体是区域的一个例子:它包含一个基本块的CFG,并具有其他类型区域可能没有的额外的语义约束。例如,在函数体中,块终结符必须是去往另一个块的分支;或者从一个函数返回,其中return参数的类型必须匹配函数签名的结果类型。类似地,函数参数必须匹配区域参数的类型与数量。一般来说,具有区域的操作可以任意定义这些对应性(correspondances)。
4.3.2. 值作用域
区域提供了程序的分层封装(hierarchical encapsulation):引用(即分支到)不在同一个区域、作为引用源头的基本块(即一个终结符操作)是不可能的(it is impossible to reference, i.e. branch to, a block which is not in the same region as the source of the reference, i.e. a terminator operation)。类似地,区域提供了值可见性的一个自然的辖域:定义在一个区域里的值不会逃逸到包含它的区域(如果有的话)。默认地,区域里的操作可以援引定义在外部区域的值,只要包含它操作的操作数援引这些值是合法的,但这局限于使用特性(trait),比如OpTrait::IsolatedFromAbove,或定制的验证器。
例子:
"any_op"(%a) ({ // if %a is in-scope in the containing region...
// then %a is in-scope here too.
%new_value = "another_op"(%a) : (i64) -> (i64)
}) : (i64) -> (i64)
MLIR定义了一个泛化的“分层支配”的概念,它跨层次工作,定义一个值是否“在作用域内”可以被特定的操作使用。一个值是否可以被同一个区域里的另一个操作使用,由区域类型定义。定义在一个区域里的值可以被在同一个区域里有一个父亲的操作使用,当且仅当这个父亲可以使用这个值。由区域参数定义的值总是可以被深度包含在这个区域里的任何操作使用。在一个区域里定义的值不能在这个区域外使用。
4.3.3. 控制流与SSACFG区域
在MLIR里,区域的控制流语义由RegionKind::SSACFG表示。非正式地,这些区域支持这样的语义:区域里的操作“顺序执行”。在执行一个操作前,它的操作数有定义良好的值。在操作执行后,操作数有相同的值且结果也有定义良好的值。在操作执行后,执行基本块里的下一个操作,直到基本块末尾的终结符操作,在这个情形下将执行其他某个操作。确定下一条执行指令的是“控制流的传递”。
一般来说,当控制流被传递到一个操作时,MLIR不限制控制流何时进入或离开包含这个区域的操作。不过,当控制流进入一个区域时,它总是在这个区域的第一个块开始,这个块称为入口块。结束每个基本块的终结符操作通过显明说明这个块的后继者基本块来表达控制流。控制流仅能传递给其中一个后继者基本块,比如在一个branch操作,或者在一个回到封装操作的return操作里。没有后继者的终结符操作仅能将控制传递回封装它的操作。在这些约束中,终结符操作的特定语义是由所涉及的特定方言操作确定的。没有在终结符操作的后继者列表中的(入口块以外的)基本块被定义为不可到达,可以被删除而不影响封装操作的语义。
虽然控制流总是通过入口块进入一个区域,控制流可能通过任何一个具有合适终结符的基本块退出区域。Standard方言利用这个功能来定义具有单入口多出口(Single-Entry-Multiple-Exit,SEME)的区域,在区域中可能流经不同的基本块,并从任一带有return操作的基本块退出。这个行为类似于大多数编程语言中函数体的行为。另外,控制流还可能不能到达基本块或区域的末尾,例如,如果一个函数调用不返回。
例子:
func @accelerator_compute(i64, i1) -> i64 { // An SSACFG region ^bb0(%a: i64, %cond: i1): // Code dominated by ^bb0 may refer to %a cond_br %cond, ^bb1, ^bb2 ^bb1: // This def for %value does not dominate ^bb2 %value = "op.convert"(%a) : (i64) -> i64 br ^bb3(%a: i64) // Branch passes %a as the argument ^bb2: accelerator.launch() { // An SSACFG region ^bb0: // Region of code nested under "accelerator.launch", it can reference %a but // not %value. %new_value = "accelerator.do_something"(%a) : (i64) -> () } // %new_value cannot be referenced outside of the region ^bb3: ... }