Operation Hierarchy
/// Operation is a basic unit of execution within MLIR. Operations can /// be nested withinRegion
s 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 内存布局
上图展示了Operation class的基本内存布局。左侧的数字是类成员的offset。右边展示了Operation类本身及其父类的各个成员。
作为链表的节点
Operation类继承了
llvm::ilist_node_with_parent
,是一个doublely-linked list。有两个成员分别 PrevAndSentinel
和 Next
指向前置节点和后继节点。和std::list
不同的是,std::list
的每个元素是单纯的数据,而ilist
是侵入式的,通过继承llvm::ilist_node_with_parent
,每个元素本身就具有向前/向后访问的能力,这样设计使得ilist
拥有更好的局部性。Operation类本身的成员
从offset16到offset56,这些成员存储了一些operation的基本信息,基本都比较好懂。其中
orderIndex
在 isBeforeInBlock
里会用到。/// Given an operation 'other' that is within the same parent block, return /// whether the current operation is before 'other' in the operation list /// of the parent block. /// Note: This function has an average complexity of O(1), but worst case may /// take O(N) where N is the number of operations within the parent block. bool Operation::isBeforeInBlock(Operation *other) { assert(block && "Operations without parent blocks have no order."); assert(other && other->block == block && "Expected other operation to have the same parent block."); // If the order of the block is already invalid, directly recompute the // parent. if (!block->isOpOrderValid()) { block->recomputeOpOrder(); } else { // Update the order either operation if necessary. updateOrderIfNecessary(); other->updateOrderIfNecessary(); } return orderIndex < other->orderIndex; }
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,结构如下图:
注意下面这数据,理论上并不是
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对象布局如下:
这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
里面里面实际是 type
和kind
的pair。kind
是一个枚举,先看下Kind
的定义:enum class Kind { /// The first N kinds are all inline operation results. An inline operation /// result means that the kind represents the result number. This removes /// the need to store an additional index value. The derived class here is /// an `OpResultImpl`. InlineOpResult = 0, /// The next kind represents a 'out-of-line' operation result. This is for /// results with numbers larger than we can represent inline. The derived /// class here is an `OpResultImpl`. OutOfLineOpResult = 6, /// The last kind represents a block argument. The derived class here is an /// `BlockArgumentImpl`. BlockArgument = 7 };
可以看见如果kind的值为[0,5]区间,说明它是
inlineOpResult
, 同时kind
的值也代表了其index。这也是为什么一个有8个result的op,为什分成了6个InlineOpResult
和两个OutOfLineOpResult
。小结
从operation的设计可以看出,一个op只要创建后,其region, result,blockOperand的数量都无法改变,只有
OpOperand
的数量是可以改变的,其中OpOperand
的存储和SmallVector
的原理有一些相似。使用
TrailingObject
,PointerIntPair
以及ilist
都是都对提高局部性做了贡献。并且虽然内部实现比较tricky,但是
Operation
暴露出来的接口并没有显现出太多复杂度,使用还是比简明的。