1 # Usage of 'const' in MLIR, for core IR types
3 aka, where'd `const` go?
5 The MLIR data structures that represent the IR itself (Instruction, Block, etc)
6 form a graph-based data structure, and the compiler analyses and passes
7 frequently walk this graph (e.g. traversing from defs to users). The early
8 design of MLIR adopted the `const` model of LLVM, which is familiar and well
9 understood (even though the LLVM implementation is flawed in many ways).
11 The design team since decided to change to a different model, which eschews
12 `const` entirely for the core IR types: you should never see a `const` method on
13 `Operation`, should never see the type `const Value`, and you shouldn't feel bad
14 about this. That said, you *should* use `const` for non-IR types, like
15 `SmallVector`'s and many other things.
17 The document below explains this design point from the viewpoint of "why make a
18 change", to explain the rationale and the tradeoffs involved that led us to this
19 potentially controversial design point.
21 Bjarke Roune summarized the situation like this:
23 > In my opinion `const` correctness is highly valuable, catching many bugs and
24 > making it clear in a code base where the mutations happen. In my opinion
25 > `const` correctness still isn't worth it in particular for IR elements because
26 > of the special uses and properties of IRs, in particular that it is common to
27 > transfer a pointer/reference to an instruction from an analysis to an
28 > optimization which will change the instruction. The analysis should be const,
29 > the optimization needs to get a non-`const` pointer. So all analyses either
30 > end up being templates (and if they never get instantiated in a const context,
31 > then the point of `const` correctness has been defeated), you need to somehow
32 > launder the const in a safe way or there will be `const_cast`s. These options
33 > are all bad, probably so bad as to out-weigh the benefits of const.
35 # Reconsidering `const` in MLIR
37 This document argues this design is introducing significant sub-optimalities
38 into the MLIR codebase, argues that the cost/benefit tradeoff of this design is
39 a poor tradeoff, and proposes switching to a much simpler approach - eliminating
40 the use of const of these IR types entirely.
42 **Note:** This document is only discussing things like `const Value` and
43 `const Operation*`. There is no proposed change for other types, e.g.
44 `SmallVector` references, the immutable types like `Attribute`, etc.
46 ## Background: The LLVM Const Model
48 The LLVM and MLIR data structures provide the IR data structures (like
49 `mlir::Operation`s and their users) as a structured cyclic graph data structure.
50 Clients of the IR typically walk up and down the graph, perform dynamic down
51 casting (of various sorts) to check for patterns, and use some high-abstraction
52 pattern matching and binding facilities to do their work.
54 The basic idea of LLVM's design is that these traversals of the IR should
55 preserve the const'ness of a pointer: if you have a const pointer to an
56 instruction and ask for its parent (or operand, users, etc), you should get a
57 const pointer to the block containing the instruction (or value defining the
58 operand, instruction using the instruction, etc). The instruction class looks
63 class Instruction : ... {
66 // A const instruction returns a const parent pointer.
67 inline const BasicBlock *getParent() const { return Parent; }
68 // A non-const instruction returns a non-const parent pointer.
69 inline BasicBlock *getParent() { return Parent; }
75 The rationale for this design is that it would be const-incorrect to return a
76 non-const pointer from getParent, because you could then walk the block to find
77 the instruction again and get non-const references to the same instruction - all
78 without a `const_cast`.
80 This `const` model is simple and the C++ type system generally supports it through
81 code duplication of methods. That said, LLVM is actually inconsistent and buggy
82 about this. Even the core classes have bugs: `llvm::Instruction::getOperand()`
83 isn't currently const correct! There are other subsystems (e.g. the
84 `llvm/IR/PatternMatch.h` APIs) where you can perform a pattern match on a const
85 IR object and bind a non-const IR object.
87 LLVM is a mature technology with hundreds of people working on it. The fact that
88 it still isn't correctly following the const model it set out for strongly hints
89 that one of: 1) The design is too complicated to be practical, 2) the benefits
90 of the model aren't worth the cost of the complexity, or 3) both 1 and 2,
91 together in some combination.
93 ## Advantages of Const-correctness in MLIR
95 Even though this doc argues for eliminating const from MLIR, it is important to
96 evaluate that as a tradeoff with the advantages the const model provides,
97 allowing us to do a cost/benefit tradeoff. These are the benefits we see:
99 The major advantage of allowing const on MLIR types is as a marker in APIs that
100 indicate that the function will not modify the specified values. For example,
101 the dominator APIs have a `dominates(const Block*, const Block*)` method, and
102 the consts provide a way of indicating that the call won't modify the blocks
103 passed in - similarly predicates like `Instruction::isTerminator() const` do not
104 modify the receiver object.
106 It is also an advantage that MLIR follows the generally prevailing pattern of
107 C++ code, which generally uses const. Consistency with the community norm is
110 ## Costs of Const-correctness in MLIR
112 As mentioned above, early work on MLIR adopted the same design as LLVM intended,
113 allowing const-correct traversals in the APIs. Here we discuss the various costs
114 of doing this by looking at some examples, listed in roughly increasing order of
117 ### Pervasively duplicated accessors
119 Just as the getParent() example above shows, achieving this const model requires
120 that all of the graph traversal accessors be duplicated into const and non-const
121 versions. This causes API bloat and slows compile time, but these are minor
124 The more significant issue is that this duplication can be so significant that
125 the signal disappears in the noise, for example `mlir::Operation` ends up with
126 things like this, which is twice as much API surface area just to try to satisfy
130 operand_iterator operand_begin();
131 operand_iterator operand_end();
133 /// Returns an iterator on the underlying Value's (Value ).
134 operand_range getOperands();
136 // Support const operand iteration.
137 using const_operand_iterator =
138 OperandIterator<const Operation, const Value>;
139 using const_operand_range = llvm::iterator_range<const_operand_iterator>;
141 const_operand_iterator operand_begin() const;
142 const_operand_iterator operand_end() const;
144 /// Returns a const iterator on the underlying Value's (Value ).
145 llvm::iterator_range<const_operand_iterator> getOperands() const;
147 ArrayRef<OpOperand> getOpOperands() const {
148 return getOperandStorage().getOperands();
150 MutableArrayRef<OpOperand> getOpOperands() {
151 return getOperandStorage().getOperands();
154 OpOperand &getOpOperand(unsigned idx) { return getOpOperands()[idx]; }
155 const OpOperand &getOpOperand(unsigned idx) const {
156 return getOpOperands()[idx];
161 ### Templated accessors
163 A related issue is that having to provide both const and non-const versions of
164 accessors leads to us having to turn more code into templates than would
165 otherwise be desirable. Things like `ResultIterator` and `ResultTypeIterator`
166 are templates *_only_* because they are generic over const and non-const
167 versions of types. This leads to them being defined inline in headers (instead
170 Thus, our const model is leading to more code in headers and more complexity in
173 ### Const incorrect in practice
175 For some things, const is more trouble than it is worth, so they never get
178 This means that certain API in practice don't provide a const variant, leading
179 to pervasive use of `const_cast` to drop the const qualifier. For example the
180 logic in `Matchers.h` doesn't support const pointers at all, even
181 though matching and binding values themselves makes perfect sense for both const
182 and non-const values. Actually fixing this would cause massive code bloat and
185 Other parts of the code are just outright incorrect. For example, the operation
186 cloning methods are defined on `Operation` like this:
189 Operation *clone(IRMapping &mapper, MLIRContext *context) const;
191 Operation *clone(MLIRContext *context) const;
194 While it makes sense for a clone method to be `const` conceptually (the original
195 operation isn't modified) this is a violation of the model, since the returned
196 operation must be mutable, and provides access to the full graph of operands as
197 the original operation, violating the graph based const model we were shooting
200 ### The `OpPointer` and `ConstOpPointer` Classes
202 The "typed operation" classes for registered operations (e.g. like `DimOp` for
203 the "memref.dim" operation in memref ops) contain a pointer to an operation and
204 provide typed APIs for processing it.
206 However, this is a problem for our current `const` design - `const DimOp` means
207 the pointer itself is immutable, not the pointee. The previous solution for this
208 was the `OpPointer<>` and `ConstOpPointer<>` classes, which existed solely to
209 provide const correctness when referring to a typed operation. Instead of
210 referring to `DimOp` directly, we used `OpPointer<DimOp>` and
211 `ConstOpPointer<DimOp>` to preserve this constness.
213 While `auto` hides many instances of these `OpPointer` classes, their presence
214 leads to extremely ugly APIs. It also obscures the fact that the user does not
215 have a direct `DimOp` object, creating easy pitfalls with subtly incorrect
219 // OpPointer encodes unnecessary and superfluous information into the API.
220 SmallVector<OpPointer<AffineForOp>, 8> stripmineSink(
221 OpPointer<AffineForOp> forOp, uint64_t factor,
222 ArrayRef<OpPointer<AffineForOp>> targets);
223 // Compared to the much cleaner and easier to read...
224 SmallVector<AffineForOp, 8> stripmineSink(AffineForOp forOp, uint64_t factor,
225 ArrayRef<AffineForOp> targets);
227 // OpPointer is easy to misuse.
228 if (auto *dimOp = inst->dyn_cast<DimOp>()) {
229 // This is actually undefined behavior because dyn_cast actually returns
230 // OpPointer<DimOp>. OpPointer<DimOp> happily implicitly converts to DimOp *
231 // creating undefined behavior that will execute correctly most of the time.
235 It is much better to eliminate them entirely, and just pass around `DimOp`
236 directly. For example, instead of:
239 LogicalResult mlir::getIndexSet(MutableArrayRef<OpPointer<AffineForOp>> forOps,
240 FlatAffineValueConstraints *domain) {
244 It is a lot nicer to just have:
247 LogicalResult mlir::getIndexSet(MutableArrayRef<AffineForOp> forOps,
248 FlatAffineValueConstraints *domain) {
251 Particularly since all of the `FooOp` classes are already semantically a smart
252 pointer to their underlying operation.
254 ## (Accepted) Proposal: Remove `const` from IR objects
256 As we can see above, there is very little benefit to our const design and
257 significant cost, and given that the primary purpose of an IR is to represent
258 transformations of code, const is providing very little benefit.
260 As such, we propose eliminating support for const references to IR objects in
261 MLIR. This implies the following changes to the codebase:
263 1. All of the const-duplicated accessors would be eliminated, e.g.
264 `Operation::getParent() const` would be removed. This is expected to remove
265 approximately ~130 lines of code from just Operation.h alone.
266 1. Const-only predicates would be changed to be non-const, e.g.
267 `Operation::isTerminator() const` would have the const removed.
268 1. Iterators and other types and functions that are templated to support
269 `const` can have those template arguments removed.
270 1. Types like `OpPointer` and `ConstOpPointer` that exist solely to propagate
271 const can be entirely removed from the codebase.
272 1. We can close bugs complaining about const incorrectness in the IR.