[RISCV] Attach VLMAX range attribute for vsetvli/vsetvlimax in InstCombine
Project: llvm/llvm-project | Language: C++ | Concepts: algorithmic-optimization, data-structures | Difficulty: senior
Link: https://github.com/llvm/llvm-project/pull/218652
The problem
LLVM's generic value analyses (like computeKnownBits, computeConstantRange, isKnownNonZero) could not reason precisely about the result of the RISC-V vector intrinsics vsetvli and vsetvlimax. These intrinsics return values (the "vector length", or vl) that are bounded by hardware-specific parameters (VLEN, LMUL, SEW), but this information was not being propagated as a range attribute on the IR. As a result, downstream optimizations could not take advantage of the known, target-specific bounds for these values.
Why it's non-trivial
A naive fix might try to always use the architectural maximums for VLEN, LMUL, and SEW, or assume that vl is always equal to the input AVL (application vector length). However, the actual range depends on the subtarget's real VLEN, and the result of vsetvli can be less than AVL in some cases (e.g., when AVL > VLMAX). The logic for computing the precise range is subtle, especially distinguishing between constant and non-constant AVLs, and ensuring that the lower and upper bounds are sound for all cases.
The approach
The core logic is implemented in RISCVTTIImpl::instCombineIntrinsic. The PR adds code to compute and attach an accurate range attribute to the result of vsetvli and vsetvlimax intrinsics, so that generic analyses can use this information.
Snippet 1: Recognizing the relevant intrinsics and extracting parameters
+ if (II.getIntrinsicID() == Intrinsic::riscv_vsetvli ||
+ II.getIntrinsicID() == Intrinsic::riscv_vsetvlimax) {
+ bool HasAVL = II.getIntrinsicID() == Intrinsic::riscv_vsetvli;
+ unsigned Offset = HasAVL ? 1 : 0;
+ unsigned Width = II.getType()->getScalarSizeInBits();
+ ConstantRange VLenRange(APInt(Width, ST->getRealMinVLen()),
+ APInt(Width, ST->getRealMaxVLen()) + 1);
- This checks if the intrinsic is one of the two RISC-V vector length intrinsics.
- It determines if the instruction has an AVL operand (
vsetvlidoes,vsetvlimaxdoes not), and computes the offset for the VSEW/VLMUL arguments. - It constructs a
ConstantRangefor the subtarget's actual VLEN range.
Snippet 2: Decoding VSEW/LMUL and computing VLMAX
+ uint64_t VSEW = cast<ConstantInt>(II.getArgOperand(Offset))->getZExtValue();
+ auto VLMUL = static_cast<RISCVVType::VLMUL>(
+ cast<ConstantInt>(II.getArgOperand(Offset + 1))->getZExtValue());
+ unsigned SEW = RISCVVType::decodeVSEW(VSEW);
+ unsigned Ratio = RISCVVType::getSEWLMULRatio(SEW, VLMUL);
+
+ // VLMAX = VLEN / (SEW / LMUL), clamped to >= 1 for any usable vtype.
+ ConstantRange VLMAXRange =
+ VLenRange.udiv(ConstantRange(APInt(Width, Ratio)))
+ .umax(ConstantRange(APInt(Width, 1)));
- Decodes the SEW (Standard Element Width) and LMUL (vector length multiplier) from the instruction's constant operands.
- Computes the ratio for VLMAX calculation.
- Calculates the range for VLMAX, ensuring it's at least 1.
Snippet 3: Computing the result range for vsetvli/vsetvlimax
+ // vsetvlimax returns exactly VLMAX; vsetvli returns vl with
+ // 0 <= vl <= min(AVL, VLMAX). vl == AVL only when AVL <= the smallest
+ // possible VLMAX; otherwise vl can shrink below VLMAX (to 0 at runtime), so
+ // only the VLMAX upper bound is sound.
+ ConstantRange VLRange = VLMAXRange;
+ if (HasAVL) {
+ APInt MaxVL = VLMAXRange.getUnsignedMax();
+ if (auto *AVL = dyn_cast<ConstantInt>(II.getArgOperand(0))) {
+ const APInt &C = AVL->getValue();
+ if (C.ule(VLMAXRange.getUnsignedMin()))
+ VLRange = ConstantRange(C);
+ else
+ VLRange = ConstantRange::getNonEmpty(APInt::getZero(Width),
+ (C.ult(MaxVL) ? C : MaxVL) + 1);
+ } else {
+ VLRange = ConstantRange::getNonEmpty(APInt::getZero(Width), MaxVL + 1);
+ }
+ }
- For
vsetvlimax, the result is exactly VLMAX. - For
vsetvli, if AVL is a constant ≤ the smallest possible VLMAX,vlis exactly AVL. - Otherwise,
vlcan be any value in [0, min(AVL, VLMAX)], so the upper bound is capped at VLMAX. - Handles both constant and non-constant AVL cases.
Snippet 4: Attaching the computed range attribute
+ ConstantRange OldRange =
+ II.getRange().value_or(ConstantRange::getFull(Width));
+ ConstantRange NewRange = VLRange.intersectWith(OldRange);
+ if (NewRange != OldRange) {
+ II.addRangeRetAttr(NewRange);
+ return &II;
+ }
+ return {};
- Intersects the new computed range with any existing range.
- If the new range is tighter, attaches it as a return attribute to the intrinsic.
Tests
Two new test files were added to verify the behavior for both intrinsics, with various AVL and VLEN settings:
llvm/test/Transforms/InstCombine/RISCV/riscv-vsetvli-range.llllvm/test/Transforms/InstCombine/RISCV/riscv-vsetvlimax-range.ll
These tests check that the correct range attributes are attached and that constant folding happens where possible.
From the review thread
No notable review discussion on this PR.
Takeaways
- When an intrinsic's result is bounded by hardware parameters, propagate those bounds as range metadata in the IR to enable more precise generic analysis and optimization.
- Always distinguish between constant and non-constant operands when computing result ranges; constant values may allow for tighter bounds or even constant folding.
- When computing value ranges, be careful to use the actual subtarget-specific hardware limits, not just architectural maximums, to avoid unsound assumptions.