[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