[llvm] [InstCombine] Fold umax/umin with NUW-shifted operand and constant (PR #193959)
via llvm-commits
llvm-commits at lists.llvm.org
Fri Apr 24 05:51:24 PDT 2026
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-llvm-transforms
Author: Amelia Jochna (ameliajochna)
<details>
<summary>Changes</summary>
Adds a new fold in InstCombineInst for patterns of the form:
umax(x nuw<< K, C) --> umax(x, C >> K) nuw<< K
umin(x nuw<< K, C) --> umin(x, C >> K) nuw<< K
The fold is valid when C has at least K trailing zeros (ensuring C >> K is exact) and the shift is marked NUW (preserving unsigned ordering). Signed variants (smax/smin) are explicitly excluded because arithmetic right-shift of a signed constant does not preserve the signed ordering.
---
Full diff: https://github.com/llvm/llvm-project/pull/193959.diff
2 Files Affected:
- (modified) llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp (+51)
- (added) llvm/test/Transforms/InstCombine/minmax-nuw-shl.ll (+154)
``````````diff
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp
index b414a7ed4fe66..5e2465a0f2b1f 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp
@@ -1775,6 +1775,54 @@ static bool rightDistributesOverLeft(Instruction::BinaryOps LOp, bool HasNUW,
}
}
+/// Folds a min/max intrinsic when one operand is a NUW shift-by-constant.
+///
+/// Handles patterns like:
+/// umax(x << K, C) --> umax(x, C >> K) << K
+static Value *foldMinMaxWithShiftedOperand(IntrinsicInst *MinMax,
+ InstCombiner::BuilderTy &Builder) {
+ Intrinsic::ID IID = MinMax->getIntrinsicID();
+ if (IID != Intrinsic::umax && IID != Intrinsic::umin)
+ return nullptr;
+
+ Value *LHS = MinMax->getOperand(0), *RHS = MinMax->getOperand(1);
+
+ const APInt *ConstOperand;
+ BinaryOperator *BinOp;
+
+ bool Matched =
+ (match(LHS, m_BinOp(BinOp)) && match(RHS, m_APInt(ConstOperand))) ||
+ (match(RHS, m_BinOp(BinOp)) && match(LHS, m_APInt(ConstOperand)));
+
+ if (!Matched)
+ return nullptr;
+
+ if (!BinOp->hasOneUse())
+ return nullptr;
+
+ if (BinOp->getOpcode() != Instruction::Shl || !BinOp->hasNoUnsignedWrap())
+ return nullptr;
+
+ Value *ShiftBase = BinOp->getOperand(0);
+ Value *ShiftAmount = BinOp->getOperand(1);
+
+ const APInt *ShiftAmountConst;
+ if (!match(ShiftAmount, m_APInt(ShiftAmountConst)))
+ return nullptr;
+
+ uint64_t K = ShiftAmountConst->getZExtValue();
+ if (ConstOperand->countTrailingZeros() < K)
+ return nullptr;
+
+ APInt HoistedConst = ConstOperand->lshr(K);
+ Value *NewConstant = ConstantInt::get(MinMax->getType(), HoistedConst);
+
+ Value *NarrowedMinMax =
+ Builder.CreateBinaryIntrinsic(IID, ShiftBase, NewConstant);
+
+ return Builder.CreateShl(NarrowedMinMax, ShiftAmount, "", /*NUW=*/true);
+}
+
// Attempts to factorise a common term
// in an instruction that has the form "(A op' B) op (C op' D)
// where op is an intrinsic and op' is a binop
@@ -2150,6 +2198,9 @@ Instruction *InstCombinerImpl::visitCallInst(CallInst &CI) {
return I;
}
+ if (Value *V = foldMinMaxWithShiftedOperand(II, Builder))
+ return replaceInstUsesWith(*II, V);
+
// If both operands of unsigned min/max are sign-extended, it is still ok
// to narrow the operation.
[[fallthrough]];
diff --git a/llvm/test/Transforms/InstCombine/minmax-nuw-shl.ll b/llvm/test/Transforms/InstCombine/minmax-nuw-shl.ll
new file mode 100644
index 0000000000000..1d931ae1a63f1
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/minmax-nuw-shl.ll
@@ -0,0 +1,154 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
+; RUN: opt -S -passes=instcombine < %s | FileCheck %s
+
+; umax(x nuw<< K, C) --> umax(x, C >> K) nuw<< K when C has >= K trailing zeros
+
+; Basic umax fold
+define i32 @umax_shl_const(i32 %x) {
+; CHECK-LABEL: @umax_shl_const(
+; CHECK-NEXT: [[X:%.*]] = call i32 @llvm.umax.i32(i32 [[X1:%.*]], i32 4)
+; CHECK-NEXT: [[SHL:%.*]] = shl nuw i32 [[X]], 2
+; CHECK-NEXT: ret i32 [[SHL]]
+;
+ %shl = shl nuw i32 %x, 2
+ %r = call i32 @llvm.umax.i32(i32 %shl, i32 16)
+ ret i32 %r
+}
+
+; Basic umin fold
+define i32 @umin_shl_const(i32 %x) {
+; CHECK-LABEL: @umin_shl_const(
+; CHECK-NEXT: [[TMP1:%.*]] = call i32 @llvm.umin.i32(i32 [[X:%.*]], i32 3)
+; CHECK-NEXT: [[R:%.*]] = shl nuw nsw i32 [[TMP1]], 3
+; CHECK-NEXT: ret i32 [[R]]
+;
+ %shl = shl nuw i32 %x, 3
+ %r = call i32 @llvm.umin.i32(i32 %shl, i32 24)
+ ret i32 %r
+}
+
+; Constant on the left (commuted)
+define i32 @umax_const_shl(i32 %x) {
+; CHECK-LABEL: @umax_const_shl(
+; CHECK-NEXT: [[X:%.*]] = call i32 @llvm.umax.i32(i32 [[X1:%.*]], i32 4)
+; CHECK-NEXT: [[SHL:%.*]] = shl nuw i32 [[X]], 2
+; CHECK-NEXT: ret i32 [[SHL]]
+;
+ %shl = shl nuw i32 %x, 2
+ %r = call i32 @llvm.umax.i32(i32 16, i32 %shl)
+ ret i32 %r
+}
+
+; i8 type
+define i8 @umax_shl_i8(i8 %x) {
+; CHECK-LABEL: @umax_shl_i8(
+; CHECK-NEXT: [[X:%.*]] = call i8 @llvm.umax.i8(i8 [[X1:%.*]], i8 2)
+; CHECK-NEXT: [[SHL:%.*]] = shl nuw i8 [[X]], 1
+; CHECK-NEXT: ret i8 [[SHL]]
+;
+ %shl = shl nuw i8 %x, 1
+ %r = call i8 @llvm.umax.i8(i8 %shl, i8 4)
+ ret i8 %r
+}
+
+; Negative: shift without nuw -- must not fold
+define i32 @umax_shl_no_nuw(i32 %x) {
+; CHECK-LABEL: @umax_shl_no_nuw(
+; CHECK-NEXT: [[SHL:%.*]] = shl i32 [[X:%.*]], 2
+; CHECK-NEXT: [[R:%.*]] = call i32 @llvm.umax.i32(i32 [[SHL]], i32 16)
+; CHECK-NEXT: ret i32 [[R]]
+;
+ %shl = shl i32 %x, 2
+ %r = call i32 @llvm.umax.i32(i32 %shl, i32 16)
+ ret i32 %r
+}
+
+; Negative: C does not have enough trailing zeros -- must not fold
+define i32 @umax_shl_insufficient_trailing_zeros(i32 %x) {
+; CHECK-LABEL: @umax_shl_insufficient_trailing_zeros(
+; CHECK-NEXT: [[SHL:%.*]] = shl nuw i32 [[X:%.*]], 3
+; CHECK-NEXT: [[R:%.*]] = call i32 @llvm.umax.i32(i32 [[SHL]], i32 12)
+; CHECK-NEXT: ret i32 [[R]]
+;
+ %shl = shl nuw i32 %x, 3
+ %r = call i32 @llvm.umax.i32(i32 %shl, i32 12)
+ ret i32 %r
+}
+
+; Negative: smax -- must not fold (signed comparison + unsigned shift unsound)
+define i32 @smax_shl_no_fold(i32 %x) {
+; CHECK-LABEL: @smax_shl_no_fold(
+; CHECK-NEXT: [[SHL:%.*]] = shl nuw i32 [[X:%.*]], 2
+; CHECK-NEXT: [[R:%.*]] = call i32 @llvm.smax.i32(i32 [[SHL]], i32 16)
+; CHECK-NEXT: ret i32 [[R]]
+;
+ %shl = shl nuw i32 %x, 2
+ %r = call i32 @llvm.smax.i32(i32 %shl, i32 16)
+ ret i32 %r
+}
+
+; Negative: smin -- must not fold
+define i32 @smin_shl_no_fold(i32 %x) {
+; CHECK-LABEL: @smin_shl_no_fold(
+; CHECK-NEXT: [[SHL:%.*]] = shl nuw i32 [[X:%.*]], 2
+; CHECK-NEXT: [[R:%.*]] = call i32 @llvm.smin.i32(i32 [[SHL]], i32 16)
+; CHECK-NEXT: ret i32 [[R]]
+;
+ %shl = shl nuw i32 %x, 2
+ %r = call i32 @llvm.smin.i32(i32 %shl, i32 16)
+ ret i32 %r
+}
+
+; Negative: non-constant shift amount -- must not fold
+define i32 @umax_shl_variable_shamt(i32 %x, i32 %k) {
+; CHECK-LABEL: @umax_shl_variable_shamt(
+; CHECK-NEXT: [[SHL:%.*]] = shl nuw i32 [[X:%.*]], [[K:%.*]]
+; CHECK-NEXT: [[R:%.*]] = call i32 @llvm.umax.i32(i32 [[SHL]], i32 16)
+; CHECK-NEXT: ret i32 [[R]]
+;
+ %shl = shl nuw i32 %x, %k
+ %r = call i32 @llvm.umax.i32(i32 %shl, i32 16)
+ ret i32 %r
+}
+
+; Negative: both operands are variables, no constant -- must not fold
+define i32 @umax_both_variable(i32 %x, i32 %y) {
+; CHECK-LABEL: @umax_both_variable(
+; CHECK-NEXT: [[R:%.*]] = call i32 @llvm.umax.i32(i32 [[X:%.*]], i32 [[Y:%.*]])
+; CHECK-NEXT: ret i32 [[R]]
+;
+ %r = call i32 @llvm.umax.i32(i32 %x, i32 %y)
+ ret i32 %r
+}
+
+; Negative: shift has multiple uses -- must not fold
+define i32 @umax_shl_multiuse(i32 %x, ptr %p) {
+; CHECK-LABEL: @umax_shl_multiuse(
+; CHECK-NEXT: [[SHL:%.*]] = shl nuw i32 [[X:%.*]], 2
+; CHECK-NEXT: store i32 [[SHL]], ptr [[P:%.*]], align 4
+; CHECK-NEXT: [[R:%.*]] = call i32 @llvm.umax.i32(i32 [[SHL]], i32 16)
+; CHECK-NEXT: ret i32 [[R]]
+;
+ %shl = shl nuw i32 %x, 2
+ store i32 %shl, ptr %p
+ %r = call i32 @llvm.umax.i32(i32 %shl, i32 16)
+ ret i32 %r
+}
+
+; Negative: BinOp is not Shl -- must not fold
+define i32 @umax_add_no_fold(i32 %x) {
+; CHECK-LABEL: @umax_add_no_fold(
+; CHECK-NEXT: [[TMP1:%.*]] = call i32 @llvm.umax.i32(i32 [[X:%.*]], i32 12)
+; CHECK-NEXT: [[R:%.*]] = add nuw i32 [[TMP1]], 4
+; CHECK-NEXT: ret i32 [[R]]
+;
+ %add = add nuw i32 %x, 4
+ %r = call i32 @llvm.umax.i32(i32 %add, i32 16)
+ ret i32 %r
+}
+
+declare i32 @llvm.umax.i32(i32, i32)
+declare i32 @llvm.umin.i32(i32, i32)
+declare i32 @llvm.smax.i32(i32, i32)
+declare i32 @llvm.smin.i32(i32, i32)
+declare i8 @llvm.umax.i8(i8, i8)
``````````
</details>
https://github.com/llvm/llvm-project/pull/193959
More information about the llvm-commits
mailing list