📏MLIR Operation内存布局

type
status
date
slug
summary
tags
category
icon
password

Operation Hierarchy

/// Operation is a basic unit of execution within MLIR. Operations can /// be nested within Regions held by other operations effectively forming a /// tree. Child operations are organized into operation blocks represented by a /// 'Block' class.
简单来讲就是一个op可以有多个region, 一个region可以有多个block,一个block里面有多个op。
具体region的数量一般根据op的语义来决定。block则和传统编译器的basic block的概念差不多。

Operation 内存布局

notion image
上图展示了Operation class的基本内存布局。左侧的数字是类成员的offset。右边展示了Operation类本身及其父类的各个成员。

作为链表的节点

Operation类继承了llvm::ilist_node_with_parent,是一个doublely-linked list。有两个成员分别 PrevAndSentinelNext指向前置节点和后继节点。和std::list不同的是,std::list的每个元素是单纯的数据,而ilist是侵入式的,通过继承llvm::ilist_node_with_parent,每个元素本身就具有向前/向后访问的能力,这样设计使得ilist拥有更好的局部性。

Operation类本身的成员

从offset16到offset56,这些成员存储了一些operation的基本信息,基本都比较好懂。其中 orderIndexisBeforeInBlock 里会用到。

Operation的regions和operands放在哪里的

Operation类本身的成员里,只记录了operand的数量,并没有operand本身,也没有对应的指针成员指向对应的operand区域。

llvm::TrailingObjects

Operation类继承了llvm::TrailingObjects<Operation, detail::OperandStorage, BlockOperand, Region, OpOperand>,可以看见这里的类模板参数里面就有和operand,region相关的类。但是llvm::TailingObjects这个类本身没有任何成员,不占空间,它只提供一些访问这些数据的方法。
先说结论:regions和operands都紧挨着Opertion对象,放在Operation对象的下方。一个有2个region, 2个block operand, 2个op operand的op,结构如下图:
notion image
注意下面这数据,理论上并不是Operation这个c++对象的,在实际创建operation对象时,是先malloc一块更大的buffer(上图则是224B),然后使用原地new,在这块buffer的前64字节创建operation对象,后续的部分则是通过TrailingObject提供的函数访问。TrailingObject会正确计算出相应的offset,并返回对应的类型。
这里的OperandStorage是用来访问opOperand的,虽然理论上可以直接通过TrailingObject直接得到OpOperand的offset然后访问,但是在一些情况下,OpOperand可能没有放在后续的这块内存区域,而需要通过OperandStorage获取实际operand存储的地址。比如:Operation在创建时,只申请了1个OpOperand的内存,但是后续又需要为op设置上更多的operand,此时就需要动态分配一块更大的内存,而不能使用原来的Trailing区域。
所以虽然从c++内存布局的角度上,从operandStorage开始后的这部分内存不属于operation对象,但是从设计和使用的逻辑上,可以把这块内存也看作时operation对象的一部分。和使用ilist一样,使用TrailingObject的目的之一想必也是为了提高局部性。

OpResult

OpResult也类似,但是是放在operation对象的前面。如,一个拥有的8个result的operation对象布局如下:
notion image
这8个result分成了6个inlineOpResult和2个OutOfLineOpResult。这也是一个实现上的trick,在high level的逻辑上,它们都是result,没有什么区别的。
一个ssa value对象包含:
  • 一个指向use list的指针:firstUse
  • 一个type对象
而result对象在ssa value的基础上,还有一个成员:index, 表明了该result是op的第几个result。
上图右半部分展示了这两种result的实现, 其中OutOfLineOpResult的3个成员和上面提到的一一匹配。而InlineOpResult则没有index这个对象,但是这里我们发现typeAndKind的类型是llvm::PointerIntPair。

llvm::PointerIntPair

/// PointerIntPair - This class implements a pair of a pointer and small /// integer. It is designed to represent this in the space required by one /// pointer by bitmangling the integer into the low part of the pointer. This /// can only be done for small integers: typically up to 3 bits, but it depends /// on the number of bits available according to PointerLikeTypeTraits for the /// type.
这个数据结构是一个pointer和int的pair,但是其大小和普通pointer的大小一致。在现代很多架构种,指针实际上不是每一个bit都用到了,这里正是用这些没有用到的bit存储一个较小的整数。
回到InlineOpResult种,mlir的type其实就是一个指针,因此typeAndKind里面里面实际是 typekind的pair。kind是一个枚举,先看下Kind的定义:
可以看见如果kind的值为[0,5]区间,说明它是inlineOpResult, 同时kind的值也代表了其index。这也是为什么一个有8个result的op,为什分成了6个InlineOpResult和两个OutOfLineOpResult

小结

从operation的设计可以看出,一个op只要创建后,其region, result,blockOperand的数量都无法改变,只有OpOperand的数量是可以改变的,其中OpOperand的存储和SmallVector的原理有一些相似。
使用TrailingObjectPointerIntPair以及ilist都是都对提高局部性做了贡献
并且虽然内部实现比较tricky,但是Operation暴露出来的接口并没有显现出太多复杂度,使用还是比简明的。
 
 
卷积优化规范化(Canonicalization)
Loading...
Catalog