MLIR 24.0.0git
IntRangeOptimizations.cpp
Go to the documentation of this file.
1//===- IntRangeOptimizations.cpp - Optimizations based on integer ranges --===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
9#include <utility>
10
11#include "llvm/ADT/TypeSwitch.h"
12
17
22#include "mlir/IR/IRMapping.h"
23#include "mlir/IR/Matchers.h"
30
31namespace mlir::arith {
32#define GEN_PASS_DEF_ARITHINTRANGEOPTS
33#include "mlir/Dialect/Arith/Transforms/Passes.h.inc"
34
35#define GEN_PASS_DEF_ARITHINTRANGENARROWING
36#include "mlir/Dialect/Arith/Transforms/Passes.h.inc"
37} // namespace mlir::arith
38
39using namespace mlir;
40using namespace mlir::arith;
41using namespace mlir::dataflow;
42
43static std::optional<APInt> getMaybeConstantValue(DataFlowSolver &solver,
44 Value value) {
45 auto *maybeInferredRange =
47 if (!maybeInferredRange || maybeInferredRange->getValue().isUninitialized())
48 return std::nullopt;
49 const ConstantIntRanges &inferredRange =
50 maybeInferredRange->getValue().getValue();
51 return inferredRange.getConstantValue();
52}
53
54static void copyIntegerRange(DataFlowSolver &solver, Value oldVal,
55 Value newVal) {
56 auto *oldState = solver.lookupState<IntegerValueRangeLattice>(oldVal);
57 if (!oldState)
58 return;
60 *oldState);
61}
62
63namespace mlir::dataflow {
64/// Patterned after SCCP
66 RewriterBase &rewriter, Value value) {
67 if (value.use_empty())
68 return failure();
69 std::optional<APInt> maybeConstValue = getMaybeConstantValue(solver, value);
70 if (!maybeConstValue.has_value())
71 return failure();
72
73 Type type = value.getType();
74 // If the type or element type is non-integral, the attribute constructor
75 // will crash, so eagerly check for an integer type to avoid this.
76 if (!getElementTypeOrSelf(type).isIntOrIndex())
77 return failure();
78
79 // Bail out if the inferred APInt bitwidth does not match the storage width
80 // of the IR type; IntegerAttr::get would assert otherwise.
81 unsigned storageWidth = ConstantIntRanges::getStorageBitwidth(type);
82 if (storageWidth != 0 && maybeConstValue->getBitWidth() != storageWidth)
83 return failure();
84
85 Location loc = value.getLoc();
86 Operation *maybeDefiningOp = value.getDefiningOp();
87 Dialect *valueDialect =
88 maybeDefiningOp ? maybeDefiningOp->getDialect()
90
91 Attribute constAttr;
92 if (auto shaped = dyn_cast<ShapedType>(type)) {
93 constAttr = mlir::DenseIntElementsAttr::get(shaped, *maybeConstValue);
94 } else {
95 constAttr = rewriter.getIntegerAttr(type, *maybeConstValue);
96 }
97 Operation *constOp =
98 valueDialect->materializeConstant(rewriter, constAttr, type, loc);
99 // Fall back to arith.constant if the dialect materializer doesn't know what
100 // to do with an integer constant.
101 if (!constOp)
102 constOp = rewriter.getContext()
103 ->getLoadedDialect<ArithDialect>()
104 ->materializeConstant(rewriter, constAttr, type, loc);
105 if (!constOp)
106 return failure();
107
108 OpResult res = constOp->getResult(0);
110 solver.eraseState(res);
111 copyIntegerRange(solver, value, res);
112 rewriter.replaceAllUsesWith(value, res);
113 return success();
114}
115} // namespace mlir::dataflow
116
117namespace {
118class DataFlowListener : public RewriterBase::Listener {
119public:
120 DataFlowListener(DataFlowSolver &s) : s(s) {}
121
122protected:
123 void notifyOperationErased(Operation *op) override {
124 s.eraseState(s.getProgramPointAfter(op));
125 for (Value res : op->getResults())
126 s.eraseState(res);
127 }
128
129 DataFlowSolver &s;
130};
131
132/// Rewrite any results of `op` that were inferred to be constant integers to
133/// and replace their uses with that constant. Return success() if all results
134/// where thus replaced and the operation is erased. Also replace any block
135/// arguments with their constant values.
136struct MaterializeKnownConstantValues : public RewritePattern {
137 MaterializeKnownConstantValues(MLIRContext *context, DataFlowSolver &s)
138 : RewritePattern::RewritePattern(Pattern::MatchAnyOpTypeTag(),
139 /*benefit=*/1, context),
140 solver(s) {}
141
142 LogicalResult matchAndRewrite(Operation *op,
143 PatternRewriter &rewriter) const override {
144 if (matchPattern(op, m_Constant()))
145 return failure();
146
147 // We need to check isIntOrIndex() and APInt bitwidth compatibility here
148 // as well to avoid infinite loops in the greedy pattern rewriter. If we
149 // only check in maybeReplaceWithConstant, this lambda might still return
150 // true for values that cannot be materialized, causing the pattern to
151 // match and claim success without making any changes, leading to
152 // non-convergence.
153 auto needsReplacing = [&](Value v) {
154 if (!getElementTypeOrSelf(v.getType()).isIntOrIndex())
155 return false;
156 std::optional<APInt> maybeConstValue = getMaybeConstantValue(solver, v);
157 if (!maybeConstValue.has_value() || v.use_empty())
158 return false;
159 unsigned storageWidth =
161 return storageWidth == 0 ||
162 maybeConstValue->getBitWidth() == storageWidth;
163 };
164 bool hasConstantResults = llvm::any_of(op->getResults(), needsReplacing);
165 if (op->getNumRegions() == 0)
166 if (!hasConstantResults)
167 return failure();
168 bool hasConstantRegionArgs = false;
169 for (Region &region : op->getRegions()) {
170 for (Block &block : region.getBlocks()) {
171 hasConstantRegionArgs |=
172 llvm::any_of(block.getArguments(), needsReplacing);
173 }
174 }
175 if (!hasConstantResults && !hasConstantRegionArgs)
176 return failure();
177
178 bool replacedAll = (op->getNumResults() != 0);
179 for (Value v : op->getResults())
180 replacedAll &=
181 (succeeded(maybeReplaceWithConstant(solver, rewriter, v)) ||
182 v.use_empty());
183 if (replacedAll && isOpTriviallyDead(op)) {
184 rewriter.eraseOp(op);
185 return success();
186 }
187
188 PatternRewriter::InsertionGuard guard(rewriter);
189 for (Region &region : op->getRegions()) {
190 for (Block &block : region.getBlocks()) {
191 rewriter.setInsertionPointToStart(&block);
192 for (BlockArgument &arg : block.getArguments()) {
193 (void)maybeReplaceWithConstant(solver, rewriter, arg);
194 }
195 }
196 }
197
198 return success();
199 }
200
201private:
202 DataFlowSolver &solver;
203};
204
205template <typename RemOp>
206struct DeleteTrivialRem : public OpRewritePattern<RemOp> {
207 DeleteTrivialRem(MLIRContext *context, DataFlowSolver &s)
208 : OpRewritePattern<RemOp>(context), solver(s) {}
209
210 LogicalResult matchAndRewrite(RemOp op,
211 PatternRewriter &rewriter) const override {
212 Value lhs = op.getOperand(0);
213 Value rhs = op.getOperand(1);
214 auto maybeModulus = getConstantIntValue(rhs);
215 if (!maybeModulus.has_value())
216 return failure();
217 int64_t modulus = *maybeModulus;
218 if (modulus <= 0)
219 return failure();
220 auto *maybeLhsRange = solver.lookupState<IntegerValueRangeLattice>(lhs);
221 if (!maybeLhsRange || maybeLhsRange->getValue().isUninitialized())
222 return failure();
223 const ConstantIntRanges &lhsRange = maybeLhsRange->getValue().getValue();
224 const APInt &min = isa<RemUIOp>(op) ? lhsRange.umin() : lhsRange.smin();
225 const APInt &max = isa<RemUIOp>(op) ? lhsRange.umax() : lhsRange.smax();
226 // The minima and maxima here are given as closed ranges, we must be
227 // strictly less than the modulus.
228 if (min.isNegative() || min.uge(modulus))
229 return failure();
230 if (max.isNegative() || max.uge(modulus))
231 return failure();
232 if (!min.ule(max))
233 return failure();
234
235 // With all those conditions out of the way, we know thas this invocation of
236 // a remainder is a noop because the input is strictly within the range
237 // [0, modulus), so get rid of it.
238 rewriter.replaceOp(op, ValueRange{lhs});
239 return success();
240 }
241
242private:
243 DataFlowSolver &solver;
244};
245
246/// Gather ranges for all the values in `values`. Appends to the existing
247/// vector.
248static LogicalResult collectRanges(DataFlowSolver &solver, ValueRange values,
250 for (Value val : values) {
251 auto *maybeInferredRange =
253 if (!maybeInferredRange || maybeInferredRange->getValue().isUninitialized())
254 return failure();
255
256 const ConstantIntRanges &inferredRange =
257 maybeInferredRange->getValue().getValue();
258 ranges.push_back(inferredRange);
259 }
260 return success();
261}
262
263/// Return int type truncated to `targetBitwidth`. If `srcType` is shaped,
264/// return shaped type as well.
265static Type getTargetType(Type srcType, unsigned targetBitwidth) {
266 auto dstType = IntegerType::get(srcType.getContext(), targetBitwidth);
267 if (auto shaped = dyn_cast<ShapedType>(srcType))
268 return shaped.clone(dstType);
269
270 assert(srcType.isIntOrIndex() && "Invalid src type");
271 return dstType;
272}
273
274namespace {
275// Enum for tracking which type of truncation should be performed
276// to narrow an operation, if any.
277enum class CastKind : uint8_t { None, Signed, Unsigned, Both };
278} // namespace
279
280/// If the values within `range` can be represented using only `width` bits,
281/// return the kind of truncation needed to preserve that property.
282///
283/// This check relies on the fact that the signed and unsigned ranges are both
284/// always correct, but that one might be an approximation of the other,
285/// so we want to use the correct truncation operation.
286static CastKind checkTruncatability(const ConstantIntRanges &range,
287 unsigned targetWidth) {
288 unsigned srcWidth = range.smin().getBitWidth();
289 if (srcWidth <= targetWidth)
290 return CastKind::None;
291 unsigned removedWidth = srcWidth - targetWidth;
292 // The sign bits need to extend into the sign bit of the target width. For
293 // example, if we're truncating 64 bits to 32, we need 64 - 32 + 1 = 33 sign
294 // bits.
295 bool canTruncateSigned =
296 range.smin().getNumSignBits() >= (removedWidth + 1) &&
297 range.smax().getNumSignBits() >= (removedWidth + 1);
298 bool canTruncateUnsigned = range.umin().countLeadingZeros() >= removedWidth &&
299 range.umax().countLeadingZeros() >= removedWidth;
300 if (canTruncateSigned && canTruncateUnsigned)
301 return CastKind::Both;
302 if (canTruncateSigned)
303 return CastKind::Signed;
304 if (canTruncateUnsigned)
305 return CastKind::Unsigned;
306 return CastKind::None;
307}
308
309static CastKind mergeCastKinds(CastKind lhs, CastKind rhs) {
310 if (lhs == CastKind::None || rhs == CastKind::None)
311 return CastKind::None;
312 if (lhs == CastKind::Both)
313 return rhs;
314 if (rhs == CastKind::Both)
315 return lhs;
316 if (lhs == rhs)
317 return lhs;
318 return CastKind::None;
319}
320
321static Value doCast(OpBuilder &builder, Location loc, Value src, Type dstType,
322 CastKind castKind) {
323 Type srcType = src.getType();
324 assert(isa<VectorType>(srcType) == isa<VectorType>(dstType) &&
325 "Mixing vector and non-vector types");
326 assert(castKind != CastKind::None && "Can't cast when casting isn't allowed");
327 Type srcElemType = getElementTypeOrSelf(srcType);
328 Type dstElemType = getElementTypeOrSelf(dstType);
329 assert(srcElemType.isIntOrIndex() && "Invalid src type");
330 assert(dstElemType.isIntOrIndex() && "Invalid dst type");
331 if (srcType == dstType)
332 return src;
333
334 if (isa<IndexType>(srcElemType) || isa<IndexType>(dstElemType)) {
335 if (castKind == CastKind::Signed)
336 return arith::IndexCastOp::create(builder, loc, dstType, src);
337 return arith::IndexCastUIOp::create(builder, loc, dstType, src);
338 }
339
340 auto srcInt = cast<IntegerType>(srcElemType);
341 auto dstInt = cast<IntegerType>(dstElemType);
342 if (dstInt.getWidth() < srcInt.getWidth())
343 return arith::TruncIOp::create(builder, loc, dstType, src);
344
345 if (castKind == CastKind::Signed)
346 return arith::ExtSIOp::create(builder, loc, dstType, src);
347 return arith::ExtUIOp::create(builder, loc, dstType, src);
348}
349
350struct NarrowElementwise final : OpTraitRewritePattern<OpTrait::Elementwise> {
351 NarrowElementwise(MLIRContext *context, DataFlowSolver &s,
352 ArrayRef<unsigned> target)
353 : OpTraitRewritePattern(context), solver(s), targetBitwidths(target) {}
354
356 LogicalResult matchAndRewrite(Operation *op,
357 PatternRewriter &rewriter) const override {
358 if (op->getNumResults() == 0)
359 return rewriter.notifyMatchFailure(op, "can't narrow resultless op");
360
361 // Inline size chosen empirically based on compilation profiling.
362 // Profiled: 2.6M calls, avg=1.7+-1.3. N=4 covers >95% of cases inline.
363 SmallVector<ConstantIntRanges, 4> ranges;
364 if (failed(collectRanges(solver, op->getOperands(), ranges)))
365 return rewriter.notifyMatchFailure(op, "input without specified range");
366 if (failed(collectRanges(solver, op->getResults(), ranges)))
367 return rewriter.notifyMatchFailure(op, "output without specified range");
368
369 Type srcType = op->getResult(0).getType();
370 if (!llvm::all_equal(op->getResultTypes()))
371 return rewriter.notifyMatchFailure(op, "mismatched result types");
372 if (op->getNumOperands() == 0 ||
373 !llvm::all_of(op->getOperandTypes(),
374 [=](Type t) { return t == srcType; }))
375 return rewriter.notifyMatchFailure(
376 op, "no operands or operand types don't match result type");
377
378 for (unsigned targetBitwidth : targetBitwidths) {
379 CastKind castKind = CastKind::Both;
380 for (const ConstantIntRanges &range : ranges) {
381 castKind = mergeCastKinds(castKind,
382 checkTruncatability(range, targetBitwidth));
383 if (castKind == CastKind::None)
384 break;
385 }
386 // For operations that explicitly treat the values as signed, we should
387 // only do signed casts, if those are deemed possible as such based on the
388 // value range.
389 auto castKindForOp =
390 llvm::TypeSwitch<Operation *, CastKind>(op)
391 .Case<arith::DivSIOp, arith::CeilDivSIOp, arith::FloorDivSIOp,
392 arith::RemSIOp, arith::MaxSIOp, arith::MinSIOp,
393 arith::ShRSIOp>([](auto) { return CastKind::Signed; })
394 .Default(CastKind::Both);
395 castKind = mergeCastKinds(castKind, castKindForOp);
396 if (castKind == CastKind::None)
397 continue;
398 // A shift by an amount >= the bitwidth is poison, so only narrow shifts
399 // when the shift amount (second operand) stays below the target width.
400 if (isa<arith::ShLIOp, arith::ShRSIOp, arith::ShRUIOp>(op) &&
401 !ranges[1].umax().ult(targetBitwidth))
402 continue;
403 Type targetType = getTargetType(srcType, targetBitwidth);
404 if (targetType == srcType)
405 continue;
406
407 Location loc = op->getLoc();
408 IRMapping mapping;
409 for (auto [arg, argRange] : llvm::zip_first(op->getOperands(), ranges)) {
410 CastKind argCastKind = castKind;
411 // When dealing with `index` values, preserve non-negativity in the
412 // index_casts since we can't recover this in unsigned when equivalent.
413 if (argCastKind == CastKind::Signed && argRange.smin().isNonNegative())
414 argCastKind = CastKind::Both;
415 Value newArg = doCast(rewriter, loc, arg, targetType, argCastKind);
416 mapping.map(arg, newArg);
417 }
418
419 Operation *newOp = rewriter.clone(*op, mapping);
420 rewriter.modifyOpInPlace(newOp, [&]() {
421 for (OpResult res : newOp->getResults()) {
422 res.setType(targetType);
423 }
424 });
425 SmallVector<Value> newResults;
426 for (auto [newRes, oldRes] :
427 llvm::zip_equal(newOp->getResults(), op->getResults())) {
428 Value castBack = doCast(rewriter, loc, newRes, srcType, castKind);
429 copyIntegerRange(solver, oldRes, castBack);
430 newResults.push_back(castBack);
431 }
432
433 rewriter.replaceOp(op, newResults);
434 return success();
435 }
436 return failure();
437 }
438
439private:
440 DataFlowSolver &solver;
441 SmallVector<unsigned, 4> targetBitwidths;
442};
443
444struct NarrowCmpI final : OpRewritePattern<arith::CmpIOp> {
445 NarrowCmpI(MLIRContext *context, DataFlowSolver &s, ArrayRef<unsigned> target)
446 : OpRewritePattern(context), solver(s), targetBitwidths(target) {}
447
448 LogicalResult matchAndRewrite(arith::CmpIOp op,
449 PatternRewriter &rewriter) const override {
450 Value lhs = op.getLhs();
451 Value rhs = op.getRhs();
452
453 SmallVector<ConstantIntRanges> ranges;
454 if (failed(collectRanges(solver, op.getOperands(), ranges)))
455 return failure();
456 const ConstantIntRanges &lhsRange = ranges[0];
457 const ConstantIntRanges &rhsRange = ranges[1];
458
459 auto isSignedCmpPredicate = [](arith::CmpIPredicate pred) -> bool {
460 return pred == arith::CmpIPredicate::sge ||
461 pred == arith::CmpIPredicate::sgt ||
462 pred == arith::CmpIPredicate::sle ||
463 pred == arith::CmpIPredicate::slt;
464 };
465 // If we're to narrow the input values via a cast, we should preserve the
466 // sign.
467 CastKind predicateBasedCastRestriction =
468 isSignedCmpPredicate(op.getPredicate()) ? CastKind::Signed
469 : CastKind::Both;
470
471 Type srcType = lhs.getType();
472 for (unsigned targetBitwidth : targetBitwidths) {
473 CastKind lhsCastKind = checkTruncatability(lhsRange, targetBitwidth);
474 CastKind rhsCastKind = checkTruncatability(rhsRange, targetBitwidth);
475 CastKind castKind = mergeCastKinds(lhsCastKind, rhsCastKind);
476 castKind = mergeCastKinds(castKind, predicateBasedCastRestriction);
477 // Note: this includes target width > src width, as well as the unsigned
478 // truncatability & signed predicate scenario.
479 if (castKind == CastKind::None)
480 continue;
481
482 Type targetType = getTargetType(srcType, targetBitwidth);
483 if (targetType == srcType)
484 continue;
485
486 Location loc = op->getLoc();
487 IRMapping mapping;
488 Value lhsCast = doCast(rewriter, loc, lhs, targetType, lhsCastKind);
489 Value rhsCast = doCast(rewriter, loc, rhs, targetType, rhsCastKind);
490 mapping.map(lhs, lhsCast);
491 mapping.map(rhs, rhsCast);
492
493 Operation *newOp = rewriter.clone(*op, mapping);
494 copyIntegerRange(solver, op.getResult(), newOp->getResult(0));
495 rewriter.replaceOp(op, newOp->getResults());
496 return success();
497 }
498 return failure();
499 }
500
501private:
502 DataFlowSolver &solver;
503 SmallVector<unsigned, 4> targetBitwidths;
504};
505
506/// Fold index_cast(index_cast(%arg: i8, index), i8) -> %arg
507/// This pattern assumes all passed `targetBitwidths` are not wider than index
508/// type.
509template <typename CastOp>
510struct FoldIndexCastChain final : OpRewritePattern<CastOp> {
511 FoldIndexCastChain(MLIRContext *context, ArrayRef<unsigned> target)
512 : OpRewritePattern<CastOp>(context), targetBitwidths(target) {}
513
514 LogicalResult matchAndRewrite(CastOp op,
515 PatternRewriter &rewriter) const override {
516 auto srcOp = op.getIn().template getDefiningOp<CastOp>();
517 if (!srcOp)
518 return rewriter.notifyMatchFailure(op, "doesn't come from an index cast");
519
520 Value src = srcOp.getIn();
521 if (src.getType() != op.getType())
522 return rewriter.notifyMatchFailure(op, "outer types don't match");
523
524 if (!srcOp.getType().isIndex())
525 return rewriter.notifyMatchFailure(op, "intermediate type isn't index");
526
527 auto intType = dyn_cast<IntegerType>(op.getType());
528 if (!intType || !llvm::is_contained(targetBitwidths, intType.getWidth()))
529 return failure();
530
531 rewriter.replaceOp(op, src);
532 return success();
533 }
534
535private:
536 SmallVector<unsigned, 4> targetBitwidths;
537};
538
539struct NarrowLoopBounds final : OpInterfaceRewritePattern<LoopLikeOpInterface> {
540 NarrowLoopBounds(MLIRContext *context, DataFlowSolver &s,
541 ArrayRef<unsigned> target)
542 : OpInterfaceRewritePattern<LoopLikeOpInterface>(context), solver(s),
543 targetBitwidths(target),
544 boundsNarrowingFailedAttr(
545 StringAttr::get(context, "arith.bounds_narrowing_failed")) {}
546
547 LogicalResult matchAndRewrite(LoopLikeOpInterface loopLike,
548 PatternRewriter &rewriter) const override {
549 // Skip ops where bounds narrowing previously failed.
550 if (loopLike->hasDiscardableAttr(boundsNarrowingFailedAttr))
551 return rewriter.notifyMatchFailure(loopLike,
552 "bounds narrowing previously failed");
553
554 std::optional<SmallVector<Value>> inductionVars =
555 loopLike.getLoopInductionVars();
556 if (!inductionVars.has_value() || inductionVars->empty())
557 return rewriter.notifyMatchFailure(loopLike, "no induction variables");
558
559 std::optional<SmallVector<OpFoldResult>> lowerBounds =
560 loopLike.getLoopLowerBounds();
561 std::optional<SmallVector<OpFoldResult>> upperBounds =
562 loopLike.getLoopUpperBounds();
563 std::optional<SmallVector<OpFoldResult>> steps = loopLike.getLoopSteps();
564
565 if (!lowerBounds.has_value() || !upperBounds.has_value() ||
566 !steps.has_value())
567 return rewriter.notifyMatchFailure(loopLike, "no loop bounds or steps");
568
569 if (lowerBounds->size() != inductionVars->size() ||
570 upperBounds->size() != inductionVars->size() ||
571 steps->size() != inductionVars->size())
572 return rewriter.notifyMatchFailure(loopLike,
573 "mismatched bounds/steps count");
574
575 Location loc = loopLike->getLoc();
576 SmallVector<OpFoldResult> newLowerBounds(*lowerBounds);
577 SmallVector<OpFoldResult> newUpperBounds(*upperBounds);
578 SmallVector<OpFoldResult> newSteps(*steps);
579 SmallVector<std::tuple<size_t, Type, CastKind>> narrowings;
580
581 // Check each (indVar, lb, ub, step) tuple.
582 for (auto [idx, indVar, lbOFR, ubOFR, stepOFR] :
583 llvm::enumerate(*inductionVars, *lowerBounds, *upperBounds, *steps)) {
584
585 // Only process value operands, skip attributes.
586 auto maybeLb = dyn_cast<Value>(lbOFR);
587 auto maybeUb = dyn_cast<Value>(ubOFR);
588 auto maybeStep = dyn_cast<Value>(stepOFR);
589
590 if (!maybeLb || !maybeUb || !maybeStep)
591 continue;
592
593 // Collect ranges for (lb, ub, step, indVar).
594 SmallVector<ConstantIntRanges> ranges;
595 if (failed(collectRanges(
596 solver, ValueRange{maybeLb, maybeUb, maybeStep, indVar}, ranges)))
597 continue;
598
599 const ConstantIntRanges &stepRange = ranges[2];
600 const ConstantIntRanges &indVarRange = ranges[3];
601
602 Type srcType = maybeLb.getType();
603
604 // Try each target bitwidth.
605 for (unsigned targetBitwidth : targetBitwidths) {
606 Type targetType = getTargetType(srcType, targetBitwidth);
607 if (targetType == srcType)
608 continue;
609
610 // Check if the target type is valid for this loop's induction
611 // variables.
612 if (!loopLike.isValidInductionVarType(targetType))
613 continue;
614
615 // Check if all values in this tuple can be truncated.
616 CastKind castKind = CastKind::Both;
617 for (const ConstantIntRanges &range : ranges) {
618 castKind = mergeCastKinds(castKind,
619 checkTruncatability(range, targetBitwidth));
620 if (castKind == CastKind::None)
621 break;
622 }
623
624 if (castKind == CastKind::None)
625 continue;
626
627 // Check if indVar + step fits in the narrowed type.
628 // This is critical for loop correctness: the loop computes
629 // iv_next = iv_current + step in the narrowed type, then compares
630 // iv_next < ub. If iv_current + step overflows, the comparison may
631 // produce incorrect results and break loop termination.
632 // Both signed and unsigned interpretations must fit because loop
633 // semantics are unknown (integer types are signless).
634 ConstantIntRanges indVarPlusStepRange(
635 indVarRange.smin().sadd_sat(stepRange.smin()),
636 indVarRange.smax().sadd_sat(stepRange.smax()),
637 indVarRange.umin().uadd_sat(stepRange.umin()),
638 indVarRange.umax().uadd_sat(stepRange.umax()));
639
640 if (checkTruncatability(indVarPlusStepRange, targetBitwidth) !=
641 CastKind::Both)
642 continue;
643
644 // Narrow the bounds and step values.
645 Value newLb = doCast(rewriter, loc, maybeLb, targetType, castKind);
646 Value newUb = doCast(rewriter, loc, maybeUb, targetType, castKind);
647 Value newStep = doCast(rewriter, loc, maybeStep, targetType, castKind);
648
649 newLowerBounds[idx] = newLb;
650 newUpperBounds[idx] = newUb;
651 newSteps[idx] = newStep;
652 narrowings.push_back({idx, targetType, castKind});
653 break;
654 }
655 }
656
657 if (narrowings.empty())
658 return rewriter.notifyMatchFailure(loopLike, "no narrowings found");
659
660 // Save original types before modifying.
661 SmallVector<Type> origTypes;
662 for (auto [idx, targetType, castKind] : narrowings) {
663 Value indVar = (*inductionVars)[idx];
664 origTypes.push_back(indVar.getType());
665 }
666
667 // Attempt to update bounds and induction variable types.
668 // If this fails, mark the op so we don't try again.
669 bool updateFailed = false;
670 rewriter.modifyOpInPlace(loopLike, [&]() {
671 // Update the loop bounds and steps.
672 if (failed(loopLike.setLoopLowerBounds(newLowerBounds)) ||
673 failed(loopLike.setLoopUpperBounds(newUpperBounds)) ||
674 failed(loopLike.setLoopSteps(newSteps))) {
675 // Mark op to prevent future attempts. IR was modified (attribute
676 // added), so we must return success() from the pattern.
677 loopLike->setDiscardableAttr(boundsNarrowingFailedAttr,
678 rewriter.getUnitAttr());
679 updateFailed = true;
680 return;
681 }
682
683 // Update induction variable types.
684 for (auto [idx, targetType, castKind] : narrowings) {
685 Value indVar = (*inductionVars)[idx];
686 auto blockArg = cast<BlockArgument>(indVar);
687
688 // Change the block argument type.
689 blockArg.setType(targetType);
690 }
691 });
692
693 if (updateFailed)
694 return success();
695
696 // Insert casts back to original type for uses.
697 for (auto [narrowingIdx, narrowingInfo] : llvm::enumerate(narrowings)) {
698 auto [idx, targetType, castKind] = narrowingInfo;
699 Value indVar = (*inductionVars)[idx];
700 auto blockArg = cast<BlockArgument>(indVar);
701 Type origType = origTypes[narrowingIdx];
702
703 OpBuilder::InsertionGuard guard(rewriter);
704 rewriter.setInsertionPointToStart(blockArg.getOwner());
705 Value casted = doCast(rewriter, loc, blockArg, origType, castKind);
706 copyIntegerRange(solver, blockArg, casted);
707
708 // Replace all uses of the narrowed indVar with the casted value.
709 rewriter.replaceAllUsesExcept(blockArg, casted, casted.getDefiningOp());
710 }
711
712 return success();
713 }
714
715private:
716 DataFlowSolver &solver;
717 SmallVector<unsigned, 4> targetBitwidths;
718 StringAttr boundsNarrowingFailedAttr;
719};
720
721struct IntRangeOptimizationsPass final
722 : arith::impl::ArithIntRangeOptsBase<IntRangeOptimizationsPass> {
723
724 void runOnOperation() override {
725 Operation *op = getOperation();
726 MLIRContext *ctx = op->getContext();
727 DataFlowSolver solver;
728 loadBaselineAnalyses(solver);
729 solver.load<IntegerRangeAnalysis>();
730 if (failed(solver.initializeAndRun(op)))
731 return signalPassFailure();
732
733 DataFlowListener listener(solver);
734
735 RewritePatternSet patterns(ctx);
737
738 // Disable folding and region simplification to avoid breaking the solver
739 // state. Both can remove block arguments (folding via control-flow
740 // simplification, region simplification via dead-arg elimination), which
741 // frees their underlying storage. A subsequent allocation may reuse the
742 // same address for a different block argument, causing stale solver state
743 // to be associated with the new argument and producing incorrect constants.
744 if (failed(
745 applyPatternsGreedily(op, std::move(patterns),
746 GreedyRewriteConfig()
747 .enableFolding(false)
748 .setRegionSimplificationLevel(
749 GreedySimplifyRegionLevel::Disabled)
750 .setListener(&listener))))
751 signalPassFailure();
752 }
753};
754
755struct IntRangeNarrowingPass final
756 : arith::impl::ArithIntRangeNarrowingBase<IntRangeNarrowingPass> {
757 using ArithIntRangeNarrowingBase::ArithIntRangeNarrowingBase;
758
759 void runOnOperation() override {
760 Operation *op = getOperation();
761 MLIRContext *ctx = op->getContext();
762 DataFlowSolver solver;
763 loadBaselineAnalyses(solver);
764 solver.load<IntegerRangeAnalysis>();
765 if (failed(solver.initializeAndRun(op)))
766 return signalPassFailure();
767
768 DataFlowListener listener(solver);
769
770 RewritePatternSet patterns(ctx);
771 populateIntRangeNarrowingPatterns(patterns, solver, bitwidthsSupported);
773 bitwidthsSupported);
774
775 // We specifically need bottom-up traversal as cmpi pattern needs range
776 // data, attached to its original argument values.
778 op, std::move(patterns),
779 GreedyRewriteConfig().setUseTopDownTraversal(false).setListener(
780 &listener))))
781 signalPassFailure();
782 }
783};
784} // namespace
785
787 RewritePatternSet &patterns, DataFlowSolver &solver) {
788 patterns.add<MaterializeKnownConstantValues, DeleteTrivialRem<RemSIOp>,
789 DeleteTrivialRem<RemUIOp>>(patterns.getContext(), solver);
790}
791
793 RewritePatternSet &patterns, DataFlowSolver &solver,
794 ArrayRef<unsigned> bitwidthsSupported) {
795 patterns.add<NarrowElementwise, NarrowCmpI>(patterns.getContext(), solver,
796 bitwidthsSupported);
797 patterns.add<FoldIndexCastChain<arith::IndexCastUIOp>,
798 FoldIndexCastChain<arith::IndexCastOp>>(patterns.getContext(),
799 bitwidthsSupported);
800}
801
803 RewritePatternSet &patterns, DataFlowSolver &solver,
804 ArrayRef<unsigned> bitwidthsSupported) {
805 patterns.add<NarrowLoopBounds>(patterns.getContext(), solver,
806 bitwidthsSupported);
807}
808
810 return std::make_unique<IntRangeOptimizationsPass>();
811}
return success()
static Operation * materializeConstant(Dialect *dialect, OpBuilder &builder, Attribute value, Type type, Location loc)
A utility function used to materialize a constant for a given attribute and type.
Definition FoldUtils.cpp:51
lhs
static void copyIntegerRange(DataFlowSolver &solver, Value oldVal, Value newVal)
static std::optional< APInt > getMaybeConstantValue(DataFlowSolver &solver, Value value)
@ None
static Value max(ImplicitLocOpBuilder &builder, Value value, Value bound)
static Value min(ImplicitLocOpBuilder &builder, Value value, Value bound)
Attributes are known-constant values of operations.
Definition Attributes.h:25
UnitAttr getUnitAttr()
Definition Builders.cpp:106
IntegerAttr getIntegerAttr(Type type, int64_t value)
Definition Builders.cpp:237
MLIRContext * getContext() const
Definition Builders.h:56
A set of arbitrary-precision integers representing bounds on a given integer value.
const APInt & smax() const
The maximum value of an integer when it is interpreted as signed.
const APInt & smin() const
The minimum value of an integer when it is interpreted as signed.
static unsigned getStorageBitwidth(Type type)
Return the bitwidth that should be used for integer ranges describing type.
std::optional< APInt > getConstantValue() const
If either the signed or unsigned interpretations of the range indicate that the value it bounds is a ...
const APInt & umax() const
The maximum value of an integer when it is interpreted as unsigned.
const APInt & umin() const
The minimum value of an integer when it is interpreted as unsigned.
The general data-flow analysis solver.
LogicalResult initializeAndRun(Operation *top, llvm::function_ref< bool(DataFlowAnalysis &)> analysisFilter=nullptr)
Initialize analyses starting from the provided top-level operation and run the analysis until fixpoin...
void eraseState(AnchorT anchor)
Erase any analysis state associated with the given lattice anchor.
const StateT * lookupState(AnchorT anchor) const
Lookup an analysis state for the given lattice anchor.
StateT * getOrCreateState(AnchorT anchor)
Get the state associated with the given lattice anchor.
AnalysisT * load(Args &&...args)
Load an analysis into the solver. Return the analysis instance.
static DenseIntElementsAttr get(const ShapedType &type, Arg &&arg)
Get an instance of a DenseIntElementsAttr with the given arguments.
Dialects are groups of MLIR operations, types and attributes, as well as behavior associated with the...
Definition Dialect.h:38
virtual Operation * materializeConstant(OpBuilder &builder, Attribute value, Type type, Location loc)
Registered hook to materialize a single constant operation from a given attribute value with the desi...
Definition Dialect.h:83
void map(Value from, Value to)
Inserts a new mapping for 'from' to 'to'.
Definition IRMapping.h:30
This class defines the main interface for locations in MLIR and acts as a non-nullable wrapper around...
Definition Location.h:76
Dialect * getLoadedDialect(StringRef name)
Get a registered IR dialect with the given namespace.
This class helps build Operations.
Definition Builders.h:210
Operation * clone(Operation &op, IRMapping &mapper)
Creates a deep copy of the specified operation, remapping any operands that use values outside of the...
Definition Builders.cpp:581
void setInsertionPointToStart(Block *block)
Sets the insertion point to the start of the specified block.
Definition Builders.h:434
This is a value defined by a result of an operation.
Definition Value.h:454
OpTraitRewritePattern is a wrapper around RewritePattern that allows for matching and rewriting again...
OpTraitRewritePattern(MLIRContext *context, PatternBenefit benefit=1)
Operation is the basic unit of execution within MLIR.
Definition Operation.h:87
Dialect * getDialect()
Return the dialect this operation is associated with, or nullptr if the associated dialect is not loa...
Definition Operation.h:237
OpResult getResult(unsigned idx)
Get the 'idx'th result of this operation.
Definition Operation.h:432
unsigned getNumRegions()
Returns the number of regions held by this operation.
Definition Operation.h:726
Location getLoc()
The source location the operation was defined or derived from.
Definition Operation.h:240
unsigned getNumOperands()
Definition Operation.h:371
operand_type_range getOperandTypes()
Definition Operation.h:422
MutableArrayRef< Region > getRegions()
Returns the regions held by this operation.
Definition Operation.h:729
result_type_range getResultTypes()
Definition Operation.h:453
operand_range getOperands()
Returns an iterator on the underlying Value's.
Definition Operation.h:403
result_range getResults()
Definition Operation.h:440
MLIRContext * getContext()
Return the context this operation is associated with.
Definition Operation.h:233
unsigned getNumResults()
Return the number of results held by this operation.
Definition Operation.h:429
Operation * getParentOp()
Return the parent operation this region is attached to.
Definition Region.h:213
MLIRContext * getContext() const
RewritePatternSet & add(ConstructorArg &&arg, ConstructorArgs &&...args)
Add an instance of each of the pattern types 'Ts' to the pattern list with the given arguments.
RewritePattern is the common base class for all DAG to DAG replacements.
This class coordinates the application of a rewrite on a set of IR, providing a way for clients to tr...
virtual void replaceOp(Operation *op, ValueRange newValues)
Replace the results of the given (original) operation with the specified list of values (replacements...
virtual void eraseOp(Operation *op)
This method erases an operation that is known to have no uses.
void replaceAllUsesExcept(Value from, Value to, Operation *exceptedUser)
Find uses of from and replace them with to except if the user is exceptedUser.
std::enable_if_t<!std::is_convertible< CallbackT, Twine >::value, LogicalResult > notifyMatchFailure(Location loc, CallbackT &&reasonCallback)
Used to notify the listener that the IR failed to be rewritten because of a match failure,...
void modifyOpInPlace(Operation *root, CallableT &&callable)
This method is a utility wrapper around an in-place modification of an operation.
virtual void replaceAllUsesWith(Value from, Value to)
Find uses of from and replace them with to.
Instances of the Type class are uniqued, have an immutable identifier and an optional mutable compone...
Definition Types.h:74
MLIRContext * getContext() const
Return the MLIRContext in which this type was uniqued.
Definition Types.cpp:35
bool isIntOrIndex() const
Return true if this is an integer (of any signedness) or an index type.
Definition Types.cpp:114
This class provides an abstraction over the different types of ranges over Values.
Definition ValueRange.h:389
This class represents an instance of an SSA value in the MLIR system, representing a computable value...
Definition Value.h:96
bool use_empty() const
Returns true if this value has no uses.
Definition Value.h:208
void setType(Type newType)
Mutate the type of this Value to be of the specified type.
Definition Value.h:116
Type getType() const
Return the type of this value.
Definition Value.h:105
Location getLoc() const
Return the location of this value.
Definition Value.cpp:24
Operation * getDefiningOp() const
If this value is the result of an operation, return the operation that defines it.
Definition Value.cpp:18
Region * getParentRegion()
Return the Region in which this Value is defined.
Definition Value.cpp:39
This lattice element represents the integer value range of an SSA value.
ChangeResult join(const AbstractSparseLattice &rhs) override
Join the information contained in 'rhs' into this lattice.
std::unique_ptr< Pass > createIntRangeOptimizationsPass()
Create a pass which do optimizations based on integer range analysis.
void populateControlFlowValuesNarrowingPatterns(RewritePatternSet &patterns, DataFlowSolver &solver, ArrayRef< unsigned > bitwidthsSupported)
Add patterns for narrowing control flow values (loop bounds, steps, etc.) based on int range analysis...
void populateIntRangeOptimizationsPatterns(RewritePatternSet &patterns, DataFlowSolver &solver)
Add patterns for int range based optimizations.
void populateIntRangeNarrowingPatterns(RewritePatternSet &patterns, DataFlowSolver &solver, ArrayRef< unsigned > bitwidthsSupported)
Add patterns for int range based narrowing.
LogicalResult maybeReplaceWithConstant(DataFlowSolver &solver, RewriterBase &rewriter, Value value)
Patterned after SCCP.
void loadBaselineAnalyses(DataFlowSolver &solver)
Populates a DataFlowSolver with analyses that are required to ensure user-defined analyses are run pr...
Definition Utils.h:29
detail::InFlightRemark failed(Location loc, RemarkOpts opts)
Report an optimization remark that failed.
Definition Remarks.h:732
Include the generated interface declarations.
bool matchPattern(Value value, const Pattern &pattern)
Entry point for matching a pattern over a Value.
Definition Matchers.h:490
std::optional< int64_t > getConstantIntValue(OpFoldResult ofr)
If ofr is a constant integer or an IntegerAttr, return the integer.
LogicalResult applyPatternsGreedily(Region &region, const FrozenRewritePatternSet &patterns, GreedyRewriteConfig config=GreedyRewriteConfig(), bool *changed=nullptr)
Rewrite ops in the given region, which must be isolated from above, by repeatedly applying the highes...
Type getElementTypeOrSelf(Type type)
Return the element type or return the type itself.
bool isOpTriviallyDead(Operation *op)
Return true if the given operation is unused, and has no side effects on memory that prevent erasing.
auto get(MLIRContext *context, Ts &&...params)
Helper method that injects context only if needed, this helps unify some of the attribute constructio...
detail::constant_op_matcher m_Constant()
Matches a constant foldable operation.
Definition Matchers.h:369
OpInterfaceRewritePattern is a wrapper around RewritePattern that allows for matching and rewriting a...
OpRewritePattern is a wrapper around RewritePattern that allows for matching and rewriting against an...