Skip to content

[VectorCombine] Fold vector sign-bit checks#175194

Merged
RKSimon merged 4 commits into
llvm:mainfrom
SavchenkoValeriy:feat/vectorcombine/fold-sign-bit-check
Jan 23, 2026
Merged

[VectorCombine] Fold vector sign-bit checks#175194
RKSimon merged 4 commits into
llvm:mainfrom
SavchenkoValeriy:feat/vectorcombine/fold-sign-bit-check

Conversation

@SavchenkoValeriy

@SavchenkoValeriy SavchenkoValeriy commented Jan 9, 2026

Copy link
Copy Markdown
Member

Fold patterns that extract sign bits, reduce them, and compare against boundary values into direct sign checks on the reduced vector.

icmp pred (reduce.{add,or,and,umax,umin}(lshr X, BitWidth-1)), C
    ->  icmp slt/sgt (reduce.{or,umax,and,umin}(X)), 0/-1

When the comparison is against 0 or MAX (1 for boolean reductions, NumElts for add), the pattern reduces to one of four quantified predicates:

  • ∀x: x < 0 (AllNeg)
  • ∀x: x ≥ 0 (AllNonNeg)
  • ∃x: x < 0 (AnyNeg)
  • ∃x: x ≥ 0 (AnyNonNeg)

The transform eliminates the shift and selects between reduce.or/reduce.umax or reduce.and/reduce.umin based on cost modeling.

The matrix of Alive2 proofs for every pair of {reduction, comparison}:

Reduction == 0 != 0 == MAX != MAX
or proof proof proof proof
umax proof proof proof proof
and proof proof proof proof
umin proof proof proof proof
add proof proof proof proof

Other test cases

Test Proof
or_slt_1 (slt 1 ≡ eq 0) proof
umax_sgt_0 (sgt 0 ≡ ne 0) proof
and_slt_max (slt 1 ≡ ne 1) proof
umin_sgt_max_minus_1 (sgt 0 ≡ eq 1) proof
add_ult_max (ult 4 ≡ ne 4) proof
add_ugt_max_minus_1 (ugt 3 ≡ eq 4) proof
ashr_add_eq_0 (ashr instead of lshr) proof

or/umax and and/umin equivalence

Check Equivalence Proof
AnyNeg or slt 0 ≡ umax slt 0 proof
AllNonNeg or sgt -1 ≡ umax sgt -1 proof
AllNeg and slt 0 ≡ umin slt 0 proof
AnyNonNeg and sgt -1 ≡ umin sgt -1 proof

@llvmbot

llvmbot commented Jan 9, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-llvm-transforms

@llvm/pr-subscribers-vectorizers

Author: Valeriy Savchenko (SavchenkoValeriy)

Changes

Fold patterns that extract sign bits, reduce them, and compare against boundary values into direct sign checks on the reduced vector.

icmp pred (reduce.{add,or,and,umax,umin}(lshr X, BitWidth-1)), C
    -&gt;  icmp slt/sgt (reduce.{or,umax,and,umin}(X)), 0/-1

When the comparison is against 0 or MAX (1 for boolean reductions, NumElts for add), the pattern reduces to one of four quantified predicates:

  • ∀x: x < 0 (AllNeg)
  • ∀x: x ≥ 0 (AllNonNeg)
  • ∃x: x < 0 (AnyNeg)
  • ∃x: x ≥ 0 (AnyNonNeg)

The transform eliminates the shift and selects between reduce.or/reduce.umax or reduce.and/reduce.umin based on cost modeling.

The matrix of Alive2 proofs for every pair of {reduction, comparison}:

Reduction == 0 != 0 == MAX != MAX
or proof proof proof proof
umax proof proof proof proof
and proof proof proof proof
umin proof proof proof proof
add proof proof proof proof

Other test cases

Test Proof
or_slt_1 (slt 1 ≡ eq 0) proof

or/umax and and/umin equivalence

Check Equivalence Proof
AnyNeg or slt 0 <=> umax slt 0 proof

Patch is 33.45 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/175194.diff

2 Files Affected:

  • (modified) llvm/lib/Transforms/Vectorize/VectorCombine.cpp (+214)
  • (added) llvm/test/Transforms/VectorCombine/fold-signbit-reduction-cmp.ll (+635)
diff --git a/llvm/lib/Transforms/Vectorize/VectorCombine.cpp b/llvm/lib/Transforms/Vectorize/VectorCombine.cpp
index 243f685cf25e2..c1412f7d12f95 100644
--- a/llvm/lib/Transforms/Vectorize/VectorCombine.cpp
+++ b/llvm/lib/Transforms/Vectorize/VectorCombine.cpp
@@ -145,6 +145,7 @@ class VectorCombine {
   bool foldShuffleFromReductions(Instruction &I);
   bool foldShuffleChainsToReduce(Instruction &I);
   bool foldCastFromReductions(Instruction &I);
+  bool foldSignBitReductionCmp(Instruction &I);
   bool foldSelectShuffle(Instruction &I, bool FromReduction = false);
   bool foldInterleaveIntrinsics(Instruction &I);
   bool shrinkType(Instruction &I);
@@ -3806,6 +3807,216 @@ bool VectorCombine::foldCastFromReductions(Instruction &I) {
   return true;
 }
 
+/// Fold:
+///   icmp pred (reduce.{add,or,and,umax,umin}(signbit_extract(x))), C
+/// into:
+///   icmp sgt/slt (reduce.{or,umax,and,umin}(x)), -1/0
+///
+/// Sign-bit reductions produce values with known semantics:
+///   - reduce.{or,umax}: 0 if no element is negative, 1 if any is
+///   - reduce.{and,umin}: 1 if all elements are negative, 0 if any isn't
+///   - reduce.add: count of negative elements (0 to NumElts)
+///
+/// We transform to a direct sign check on reduce.{or,umax} or
+/// reduce.{and,umin} without explicit sign-bit extraction.
+///
+/// In spirit, it's similar to foldSignBitCheck in InstCombine.
+bool VectorCombine::foldSignBitReductionCmp(Instruction &I) {
+  CmpPredicate Pred;
+  Value *ReduceOp;
+  const APInt *CmpVal;
+  if (!match(&I, m_ICmp(Pred, m_Value(ReduceOp), m_APInt(CmpVal))))
+    return false;
+
+  auto *II = dyn_cast<IntrinsicInst>(ReduceOp);
+  if (!II || !II->hasOneUse())
+    return false;
+
+  Intrinsic::ID OrigIID = II->getIntrinsicID();
+  switch (OrigIID) {
+  case Intrinsic::vector_reduce_or:
+  case Intrinsic::vector_reduce_umax:
+  case Intrinsic::vector_reduce_and:
+  case Intrinsic::vector_reduce_umin:
+  case Intrinsic::vector_reduce_add:
+    break;
+  default:
+    return false;
+  }
+
+  Value *ReductionSrc = II->getArgOperand(0);
+  if (!ReductionSrc->hasOneUse())
+    return false;
+
+  auto *VecTy = dyn_cast<FixedVectorType>(ReductionSrc->getType());
+  if (!VecTy)
+    return false;
+
+  unsigned BitWidth = VecTy->getScalarSizeInBits();
+  unsigned NumElts = VecTy->getNumElements();
+
+  // Match sign-bit extraction: shr X, (bitwidth-1)
+  Value *X;
+  Constant *C;
+  if (!match(ReductionSrc, m_Shr(m_Value(X), m_Constant(C))) ||
+      !match(C, m_SpecificInt_ICMP(ICmpInst::ICMP_EQ,
+                                   APInt(BitWidth, BitWidth - 1))))
+    return false;
+
+  // MaxVal: 1 for or/and/umax/umin, NumElts for add
+  unsigned MaxVal = OrigIID == Intrinsic::vector_reduce_add ? NumElts : 1;
+
+  // In addition to direct comparisons EQ 0, NE 0, EQ 1, NE 1, etc. we support
+  // inequalities that can be interpreted as either EQ or NE considering a
+  // rather narrow range of possible value of sign-bit reductions.
+  bool IsEq;
+  uint64_t NormalizedCmpVal;
+  if (Pred == ICmpInst::ICMP_EQ) {
+    IsEq = true;
+    NormalizedCmpVal = CmpVal->getZExtValue();
+  } else if (Pred == ICmpInst::ICMP_NE) {
+    IsEq = false;
+    NormalizedCmpVal = CmpVal->getZExtValue();
+  } else if (Pred == ICmpInst::ICMP_SLT && CmpVal->isOne()) {
+    IsEq = true;
+    NormalizedCmpVal = 0; // slt 1 → eq 0
+  } else if (Pred == ICmpInst::ICMP_SGT && CmpVal->isZero()) {
+    IsEq = false;
+    NormalizedCmpVal = 0; // sgt 0 → ne 0
+  } else if ((Pred == ICmpInst::ICMP_SLT || Pred == ICmpInst::ICMP_ULT) &&
+             *CmpVal == MaxVal) {
+    IsEq = false;
+    NormalizedCmpVal = MaxVal; // s/ult MaxVal → ne MaxVal
+  } else if ((Pred == ICmpInst::ICMP_SGT || Pred == ICmpInst::ICMP_UGT) &&
+             *CmpVal == MaxVal - 1) {
+    IsEq = true;
+    NormalizedCmpVal = MaxVal; // s/ugt MaxVal-1 → eq MaxVal
+  } else {
+    return false;
+  }
+
+  bool TestsHigh = NormalizedCmpVal == MaxVal;
+  if (NormalizedCmpVal != 0 && !TestsHigh)
+    return false;
+
+  // For this fold we support for types of checks:
+  //
+  //   1. All lanes are negative - AllNeg
+  //   2. All lanes are non-negative - AllNonNeg
+  //   3. At least one negative lane - AnyNeg
+  //   4. At least one non-negative lane - AnyNonNeg
+  //
+  // For each case, we can generate the following code:
+  //
+  //   1. AllNeg    - reduce.and/umin(X) < 0
+  //   2. AllNonNeg -  reduce.or/umax(X) > -1
+  //   3. AnyNeg    -  reduce.or/umax(X) < 0
+  //   4. AnyNonNeg - reduce.and/umin(X) > -1
+  //
+  // The table below shows the aggregation of all supported cases
+  // using these four cases.
+  //
+  //   Reduction   | == 0      | != 0      | == MAX    | != MAX
+  //   ------------+-----------+-----------+-----------+-----------
+  //   or/umax     | AllNonNeg | AnyNeg    | AnyNeg    | AllNonNeg
+  //   and/umin    | AnyNonNeg | AllNeg    | AllNeg    | AnyNonNeg
+  //   add         | AllNonNeg | AnyNeg    | AllNeg    | AnyNonNeg
+  //
+  // NOTE: MAX = 1 for or/and/umax/umin, and the vector size N for add
+  //
+  // For easier codegen and check inversion, we use the following encoding:
+  //
+  //   1. Bit-3 === requires or/umax (1) or and/umin (0) check
+  //   2. Bit-2 === checks < 0 (1) or > -1 (0)
+  //   3. Bit-1 === universal (1) or existential (0) check
+  //
+  //   AnyNeg    = 0b110: uses or/umax,  checks negative, any-check
+  //   AllNonNeg = 0b101: uses or/umax,  checks non-neg,  all-check
+  //   AnyNonNeg = 0b000: uses and/umin, checks non-neg,  any-check
+  //   AllNeg    = 0b011: uses and/umin, checks negative, all-check
+  //
+  // XOR with 0b011 inverts the check (swaps all/any and neg/non-neg).
+  //
+  enum CheckKind : unsigned {
+    AnyNonNeg = 0b000,
+    AllNeg = 0b011,
+    AllNonNeg = 0b101,
+    AnyNeg = 0b110,
+  };
+  // Return true if we fold this check into or/umax and false for and/umin
+  auto RequiresOr = [](CheckKind C) -> bool { return C & 0b100; };
+  // Return true if we should check if result is negative and false otherwise
+  auto IsNegativeCheck = [](CheckKind C) -> bool { return C & 0b010; };
+  // Logically invert the check
+  auto Invert = [](CheckKind C) { return CheckKind(C ^ 0b011); };
+
+  CheckKind Base;
+  switch (OrigIID) {
+  case Intrinsic::vector_reduce_or:
+  case Intrinsic::vector_reduce_umax:
+    Base = TestsHigh ? AnyNeg : AllNonNeg;
+    break;
+  case Intrinsic::vector_reduce_and:
+  case Intrinsic::vector_reduce_umin:
+    Base = TestsHigh ? AllNeg : AnyNonNeg;
+    break;
+  default: // vector_reduce_add
+    Base = TestsHigh ? AllNeg : AllNonNeg;
+    break;
+  }
+
+  CheckKind Check = IsEq ? Base : Invert(Base);
+
+  // Calculate old cost: shift + reduction
+  unsigned ShiftOpcode = cast<Instruction>(ReductionSrc)->getOpcode();
+  InstructionCost OldCost =
+      TTI.getArithmeticInstrCost(ShiftOpcode, VecTy, CostKind);
+  unsigned OrigReductionOpc = getArithmeticReductionInstruction(OrigIID);
+  if (OrigReductionOpc != Instruction::ICmp)
+    OldCost += TTI.getArithmeticReductionCost(OrigReductionOpc, VecTy,
+                                              std::nullopt, CostKind);
+  else
+    OldCost +=
+        TTI.getMinMaxReductionCost(OrigIID, VecTy, FastMathFlags(), CostKind);
+
+  auto PickCheaper = [&](Intrinsic::ID Arith, Intrinsic::ID MinMax) {
+    InstructionCost ArithCost =
+        TTI.getArithmeticReductionCost(getArithmeticReductionInstruction(Arith),
+                                       VecTy, std::nullopt, CostKind);
+    InstructionCost MinMaxCost =
+        TTI.getMinMaxReductionCost(MinMax, VecTy, FastMathFlags(), CostKind);
+    return ArithCost <= MinMaxCost ? std::make_pair(Arith, ArithCost)
+                                   : std::make_pair(MinMax, MinMaxCost);
+  };
+
+  // Choose output reduction based on encoding's MSB
+  auto [NewIID, NewCost] = RequiresOr(Check)
+                               ? PickCheaper(Intrinsic::vector_reduce_or,
+                                             Intrinsic::vector_reduce_umax)
+                               : PickCheaper(Intrinsic::vector_reduce_and,
+                                             Intrinsic::vector_reduce_umin);
+
+  LLVM_DEBUG(dbgs() << "Found sign-bit reduction cmp: " << I << "\n  OldCost: "
+                    << OldCost << " vs NewCost: " << NewCost << "\n");
+
+  if (NewCost > OldCost)
+    return false;
+
+  // Generate comparison based on encoding's neg bit: slt 0 for neg, sgt -1 for
+  // non-neg
+  Builder.SetInsertPoint(&I);
+  Type *ScalarTy = VecTy->getScalarType();
+  Value *NewReduce = Builder.CreateIntrinsic(ScalarTy, NewIID, {X});
+  Value *NewCmp = IsNegativeCheck(Check)
+                      ? Builder.CreateICmpSLT(
+                            NewReduce, ConstantInt::getNullValue(ScalarTy))
+                      : Builder.CreateICmpSGT(
+                            NewReduce, ConstantInt::getAllOnesValue(ScalarTy));
+
+  replaceValue(I, *NewCmp);
+  return true;
+}
+
 /// Returns true if this ShuffleVectorInst eventually feeds into a
 /// vector reduction intrinsic (e.g., vector_reduce_add) by only following
 /// chains of shuffles and binary operators (in any combination/order).
@@ -4844,6 +5055,9 @@ bool VectorCombine::run() {
           return true;
         break;
       case Instruction::ICmp:
+        if (foldSignBitReductionCmp(I))
+          return true;
+        [[fallthrough]];
       case Instruction::FCmp:
         if (foldExtractExtract(I))
           return true;
diff --git a/llvm/test/Transforms/VectorCombine/fold-signbit-reduction-cmp.ll b/llvm/test/Transforms/VectorCombine/fold-signbit-reduction-cmp.ll
new file mode 100644
index 0000000000000..a598fa7bd22f9
--- /dev/null
+++ b/llvm/test/Transforms/VectorCombine/fold-signbit-reduction-cmp.ll
@@ -0,0 +1,635 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
+; RUN: opt -S -passes=vector-combine -mtriple=aarch64 < %s | FileCheck %s --check-prefixes=CHECK,AARCH64
+; RUN: opt -S -passes=vector-combine -mtriple=x86_64-- < %s | FileCheck %s --check-prefixes=CHECK,X86
+
+define i1 @or_eq_0(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @or_eq_0(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umax.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[TMP1]], -1
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @or_eq_0(
+; X86-SAME: <4 x i32> [[X:%.*]]) {
+; X86-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.or.v4i32(<4 x i32> [[X]])
+; X86-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[TMP1]], -1
+; X86-NEXT:    ret i1 [[CMP]]
+;
+  %shr = lshr <4 x i32> %x, splat (i32 31)
+  %red = call i32 @llvm.vector.reduce.or.v4i32(<4 x i32> %shr)
+  %cmp = icmp eq i32 %red, 0
+  ret i1 %cmp
+}
+
+define i1 @or_ne_0(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @or_ne_0(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umax.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @or_ne_0(
+; X86-SAME: <4 x i32> [[X:%.*]]) {
+; X86-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.or.v4i32(<4 x i32> [[X]])
+; X86-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; X86-NEXT:    ret i1 [[CMP]]
+;
+  %shr = lshr <4 x i32> %x, splat (i32 31)
+  %red = call i32 @llvm.vector.reduce.or.v4i32(<4 x i32> %shr)
+  %cmp = icmp ne i32 %red, 0
+  ret i1 %cmp
+}
+
+define i1 @or_eq_max(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @or_eq_max(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umax.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @or_eq_max(
+; X86-SAME: <4 x i32> [[X:%.*]]) {
+; X86-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.or.v4i32(<4 x i32> [[X]])
+; X86-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; X86-NEXT:    ret i1 [[CMP]]
+;
+  %shr = lshr <4 x i32> %x, splat (i32 31)
+  %red = call i32 @llvm.vector.reduce.or.v4i32(<4 x i32> %shr)
+  %cmp = icmp eq i32 %red, 1
+  ret i1 %cmp
+}
+
+define i1 @or_ne_max(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @or_ne_max(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umax.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[TMP1]], -1
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @or_ne_max(
+; X86-SAME: <4 x i32> [[X:%.*]]) {
+; X86-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.or.v4i32(<4 x i32> [[X]])
+; X86-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[TMP1]], -1
+; X86-NEXT:    ret i1 [[CMP]]
+;
+  %shr = lshr <4 x i32> %x, splat (i32 31)
+  %red = call i32 @llvm.vector.reduce.or.v4i32(<4 x i32> %shr)
+  %cmp = icmp ne i32 %red, 1
+  ret i1 %cmp
+}
+
+define i1 @umax_eq_0(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @umax_eq_0(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umax.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[TMP1]], -1
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @umax_eq_0(
+; X86-SAME: <4 x i32> [[X:%.*]]) {
+; X86-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.or.v4i32(<4 x i32> [[X]])
+; X86-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[TMP1]], -1
+; X86-NEXT:    ret i1 [[CMP]]
+;
+  %shr = lshr <4 x i32> %x, splat (i32 31)
+  %red = call i32 @llvm.vector.reduce.umax.v4i32(<4 x i32> %shr)
+  %cmp = icmp eq i32 %red, 0
+  ret i1 %cmp
+}
+
+define i1 @umax_ne_0(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @umax_ne_0(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umax.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @umax_ne_0(
+; X86-SAME: <4 x i32> [[X:%.*]]) {
+; X86-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.or.v4i32(<4 x i32> [[X]])
+; X86-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; X86-NEXT:    ret i1 [[CMP]]
+;
+  %shr = lshr <4 x i32> %x, splat (i32 31)
+  %red = call i32 @llvm.vector.reduce.umax.v4i32(<4 x i32> %shr)
+  %cmp = icmp ne i32 %red, 0
+  ret i1 %cmp
+}
+
+define i1 @umax_eq_max(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @umax_eq_max(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umax.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @umax_eq_max(
+; X86-SAME: <4 x i32> [[X:%.*]]) {
+; X86-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.or.v4i32(<4 x i32> [[X]])
+; X86-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; X86-NEXT:    ret i1 [[CMP]]
+;
+  %shr = lshr <4 x i32> %x, splat (i32 31)
+  %red = call i32 @llvm.vector.reduce.umax.v4i32(<4 x i32> %shr)
+  %cmp = icmp eq i32 %red, 1
+  ret i1 %cmp
+}
+
+define i1 @umax_ne_max(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @umax_ne_max(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umax.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[TMP1]], -1
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @umax_ne_max(
+; X86-SAME: <4 x i32> [[X:%.*]]) {
+; X86-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.or.v4i32(<4 x i32> [[X]])
+; X86-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[TMP1]], -1
+; X86-NEXT:    ret i1 [[CMP]]
+;
+  %shr = lshr <4 x i32> %x, splat (i32 31)
+  %red = call i32 @llvm.vector.reduce.umax.v4i32(<4 x i32> %shr)
+  %cmp = icmp ne i32 %red, 1
+  ret i1 %cmp
+}
+
+define i1 @and_eq_0(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @and_eq_0(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umin.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[TMP1]], -1
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @and_eq_0(
+; X86-SAME: <4 x i32> [[X:%.*]]) {
+; X86-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.and.v4i32(<4 x i32> [[X]])
+; X86-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[TMP1]], -1
+; X86-NEXT:    ret i1 [[CMP]]
+;
+  %shr = lshr <4 x i32> %x, splat (i32 31)
+  %red = call i32 @llvm.vector.reduce.and.v4i32(<4 x i32> %shr)
+  %cmp = icmp eq i32 %red, 0
+  ret i1 %cmp
+}
+
+define i1 @and_ne_0(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @and_ne_0(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umin.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @and_ne_0(
+; X86-SAME: <4 x i32> [[X:%.*]]) {
+; X86-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.and.v4i32(<4 x i32> [[X]])
+; X86-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; X86-NEXT:    ret i1 [[CMP]]
+;
+  %shr = lshr <4 x i32> %x, splat (i32 31)
+  %red = call i32 @llvm.vector.reduce.and.v4i32(<4 x i32> %shr)
+  %cmp = icmp ne i32 %red, 0
+  ret i1 %cmp
+}
+
+define i1 @and_eq_max(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @and_eq_max(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umin.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @and_eq_max(
+; X86-SAME: <4 x i32> [[X:%.*]]) {
+; X86-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.and.v4i32(<4 x i32> [[X]])
+; X86-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; X86-NEXT:    ret i1 [[CMP]]
+;
+  %shr = lshr <4 x i32> %x, splat (i32 31)
+  %red = call i32 @llvm.vector.reduce.and.v4i32(<4 x i32> %shr)
+  %cmp = icmp eq i32 %red, 1
+  ret i1 %cmp
+}
+
+define i1 @and_ne_max(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @and_ne_max(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umin.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[TMP1]], -1
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @and_ne_max(
+; X86-SAME: <4 x i32> [[X:%.*]]) {
+; X86-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.and.v4i32(<4 x i32> [[X]])
+; X86-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[TMP1]], -1
+; X86-NEXT:    ret i1 [[CMP]]
+;
+  %shr = lshr <4 x i32> %x, splat (i32 31)
+  %red = call i32 @llvm.vector.reduce.and.v4i32(<4 x i32> %shr)
+  %cmp = icmp ne i32 %red, 1
+  ret i1 %cmp
+}
+
+define i1 @umin_eq_0(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @umin_eq_0(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umin.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[TMP1]], -1
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @umin_eq_0(
+; X86-SAME: <4 x i32> [[X:%.*]]) {
+; X86-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.and.v4i32(<4 x i32> [[X]])
+; X86-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[TMP1]], -1
+; X86-NEXT:    ret i1 [[CMP]]
+;
+  %shr = lshr <4 x i32> %x, splat (i32 31)
+  %red = call i32 @llvm.vector.reduce.umin.v4i32(<4 x i32> %shr)
+  %cmp = icmp eq i32 %red, 0
+  ret i1 %cmp
+}
+
+define i1 @umin_ne_0(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @umin_ne_0(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umin.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @umin_ne_0(
+; X86-SAME: <4 x i32> [[X:%.*]]) {
+; X86-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.and.v4i32(<4 x i32> [[X]])
+; X86-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; X86-NEXT:    ret i1 [[CMP]]
+;
+  %shr = lshr <4 x i32> %x, splat (i32 31)
+  %red = call i32 @llvm.vector.reduce.umin.v4i32(<4 x i32> %shr)
+  %cmp = icmp ne i32 %red, 0
+  ret i1 %cmp
+}
+
+define i1 @umin_eq_max(<4 x i32> %x) {
+; AARCH64-LABEL: define i1 @umin_eq_max(
+; AARCH64-SAME: <4 x i32> [[X:%.*]]) {
+; AARCH64-NEXT:    [[TMP1:%.*]] = call i32 @llvm.vector.reduce.umin.v4i32(<4 x i32> [[X]])
+; AARCH64-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], 0
+; AARCH64-NEXT:    ret i1 [[CMP]]
+;
+; X86-LABEL: define i1 @umin_eq_max(
+; X86-SAME: <4 x i32> [[X:%.*]])...
[truncated]

@SavchenkoValeriy
SavchenkoValeriy force-pushed the feat/vectorcombine/fold-sign-bit-check branch from 19ab77e to c500cd8 Compare January 9, 2026 16:20
Comment thread llvm/lib/Transforms/Vectorize/VectorCombine.cpp Outdated
/// Sign-bit reductions produce values with known semantics:
/// - reduce.{or,umax}: 0 if no element is negative, 1 if any is
/// - reduce.{and,umin}: 1 if all elements are negative, 0 if any isn't
/// - reduce.add: count of negative elements (0 to NumElts)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a bit weird to use reduce.add for sign bit checking. Can you share some real-world cases? Is it generated by auto-vectorization?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While agree with this in general, it's quite common in practice because platforms like AArch64 don't have horizontal or and thus horizontal add becomes a viable alternative.

As I mentioned earlier in my other patch: #173069 (comment) that pattern of implementing _mm_movemask_ps for NEON. Then that implementation that might look something like this:

    uint32x4_t input = vreinterpretq_u32_m128(a);
    static const int32x4_t shift = {0, 1, 2, 3};
    uint32x4_t tmp = vshrq_n_u32(input, 31);
    return vaddvq_u32(vshlq_u32(tmp, shift));

gets inlined in various places producing quite inefficient signedness check in computationally heavy applications.

Combined with #173069 this change optimizes it away. And it is exactly the add version that gives us a substantial boost on the internal benchmarks.

@SavchenkoValeriy
SavchenkoValeriy force-pushed the feat/vectorcombine/fold-sign-bit-check branch from c500cd8 to b6666ce Compare January 11, 2026 13:53
Comment thread llvm/lib/Transforms/Vectorize/VectorCombine.cpp Outdated
Comment thread llvm/lib/Transforms/Vectorize/VectorCombine.cpp Outdated
Comment thread llvm/lib/Transforms/Vectorize/VectorCombine.cpp Outdated
@SavchenkoValeriy
SavchenkoValeriy force-pushed the feat/vectorcombine/fold-sign-bit-check branch from b6666ce to f236f8d Compare January 11, 2026 16:32
Comment thread llvm/test/Transforms/VectorCombine/fold-signbit-reduction-cmp.ll Outdated
Comment thread llvm/lib/Transforms/Vectorize/VectorCombine.cpp
@SavchenkoValeriy
SavchenkoValeriy force-pushed the feat/vectorcombine/fold-sign-bit-check branch from f236f8d to 24882ef Compare January 12, 2026 17:25
@SavchenkoValeriy

Copy link
Copy Markdown
Member Author

@RKSimon do you mind taking another look here?

@RKSimon

RKSimon commented Jan 14, 2026

Copy link
Copy Markdown
Contributor

@RKSimon do you mind taking another look here?

don't forget to use the "re-request-review" icon by the reviewer, otherwise I don't usually see it (and I often miss CC mentions)

@RKSimon
RKSimon self-requested a review January 14, 2026 14:55
@SavchenkoValeriy

Copy link
Copy Markdown
Member Author

@RKSimon I can't re-request the review on this PR, but do you mind checking it? I split the test case between X86 and AArch64 targets as you asked.

@preames

preames commented Jan 16, 2026

Copy link
Copy Markdown
Contributor

High level comment. I think this transform can be split into several sub-transforms. I am not expressing an opinion as to whether this change should land before or after such a split, and will leave that up to area reviewers.

Your proposed transform of

 icmp pred (reduce.{add,or,and,umax,umin}(lshr X, BitWidth-1)), C
    ->  icmp slt/sgt (reduce.{or,umax,and,umin}(X)), 0/-1

Possible splits:

(1)
reduce.{or,and,umax,umin}(lshr X, BitWidth-1)) -> lshr (reduce.{or,and,umax,umin} X), BitWidth-1

(This generalizes for any logical op. Note that we should already be canonicalizing smin/smax to umin/umax; if not that's another to play with.)

(2) Your inequality handling is just a transform to convert the inequality form to the equality form (of the source).

I believe that (1) generalizes for any single bit extract (i.e. and X, 2).

Comment thread llvm/lib/Transforms/Vectorize/VectorCombine.cpp Outdated
Comment thread llvm/lib/Transforms/Vectorize/VectorCombine.cpp Outdated
Comment thread llvm/lib/Transforms/Vectorize/VectorCombine.cpp Outdated
Comment thread llvm/lib/Transforms/Vectorize/VectorCombine.cpp
Comment thread llvm/lib/Transforms/Vectorize/VectorCombine.cpp Outdated
@dtcxzyw

dtcxzyw commented Jan 16, 2026

Copy link
Copy Markdown
Member

(1) reduce.{or,and,umax,umin}(lshr X, BitWidth-1)) -> lshr (reduce.{or,and,umax,umin} X), BitWidth-1

(This generalizes for any logical op. Note that we should already be canonicalizing smin/smax to umin/umax; if not that's another to play with.)

This doesn't work for the original motivating issue: #175194 (comment) But I agree (1) should simplify the implementation.

@SavchenkoValeriy

Copy link
Copy Markdown
Member Author

High level comment. I think this transform can be split into several sub-transforms. I am not expressing an opinion as to whether this change should land before or after such a split, and will leave that up to area reviewers.

Thanks for the comment, I always strive to split desired folds into smaller more general folds (as actually the case here as well).

Your proposed transform of

 icmp pred (reduce.{add,or,and,umax,umin}(lshr X, BitWidth-1)), C
    ->  icmp slt/sgt (reduce.{or,umax,and,umin}(X)), 0/-1

Possible splits:

(1) reduce.{or,and,umax,umin}(lshr X, BitWidth-1)) -> lshr (reduce.{or,and,umax,umin} X), BitWidth-1

(This generalizes for any logical op. Note that we should already be canonicalizing smin/smax to umin/umax; if not that's another to play with.)

(2) Your inequality handling is just a transform to convert the inequality form to the equality form (of the source).

I believe that (1) generalizes for any single bit extract (i.e. and X, 2).

I don't oppose it, but it actually doesn't help with the motivation example that is about reduce.addthat I generalized to this transformation. And I don't quite see how implementation would be simpler because we'll be matching shift of a reduction as opposed to reduction of a shift. All the rest is the same. The only benefit that I see is that we would support "shift of a reduction" cases in addition to what I propose in this patch. However,reduce.add` should still check for "reduction of a shift", which will make implementation more cumbersome.

Maybe that can be a follow up work?

@SavchenkoValeriy
SavchenkoValeriy force-pushed the feat/vectorcombine/fold-sign-bit-check branch from 24882ef to 0ce381c Compare January 16, 2026 18:33
@github-actions

github-actions Bot commented Jan 16, 2026

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

@SavchenkoValeriy
SavchenkoValeriy force-pushed the feat/vectorcombine/fold-sign-bit-check branch from 0ce381c to 7d26c62 Compare January 16, 2026 18:37
@SavchenkoValeriy
SavchenkoValeriy force-pushed the feat/vectorcombine/fold-sign-bit-check branch from d0b8428 to 9d64f3d Compare January 22, 2026 17:23
@github-actions

github-actions Bot commented Jan 22, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 189159 tests passed
  • 5018 tests skipped

✅ The build succeeded and all tests passed.

@github-actions

github-actions Bot commented Jan 22, 2026

Copy link
Copy Markdown

🪟 Windows x64 Test Results

  • 130113 tests passed
  • 2880 tests skipped

✅ The build succeeded and all tests passed.

@SavchenkoValeriy
SavchenkoValeriy force-pushed the feat/vectorcombine/fold-sign-bit-check branch from 9d64f3d to 3a0b80a Compare January 22, 2026 18:38
Fold patterns that extract sign bits, reduce them, and compare against boundary values into direct sign checks on the reduced vector.

```
icmp pred (reduce.{add,or,and,umax,umin}(lshr X, BitWidth-1)), C
    ->  icmp slt/sgt (reduce.{or,umax,and,umin}(X)), 0/-1
```

When the comparison is against 0 or MAX (1 for boolean reductions, NumElts for add), the pattern reduces to one of four quantified predicates:
- ∀x: x < 0 (AllNeg)
- ∀x: x ≥ 0 (AllNonNeg)
- ∃x: x < 0 (AnyNeg)
- ∃x: x ≥ 0 (AnyNonNeg)

The transform eliminates the shift and selects between reduce.or/reduce.umax or reduce.and/reduce.umin based on cost modeling.

The matrix of Alive2 proofs for every pair of {reduction, comparison}:

| Reduction | == 0 | != 0 | == MAX | != MAX |
|-----------|------|------|--------|--------|
| or        | [proof](https://alive2.llvm.org/ce/z/_BWxJW) | [proof](https://alive2.llvm.org/ce/z/k3EiK6) | [proof](https://alive2.llvm.org/ce/z/a8cAjp) | [proof](https://alive2.llvm.org/ce/z/ci-HMt) |
| umax      | [proof](https://alive2.llvm.org/ce/z/dWt28G) | [proof](https://alive2.llvm.org/ce/z/_MqxXC) | [proof](https://alive2.llvm.org/ce/z/KQebnF) | [proof](https://alive2.llvm.org/ce/z/mixEgN) |
| and       | [proof](https://alive2.llvm.org/ce/z/JgYrLj) | [proof](https://alive2.llvm.org/ce/z/FZuPLy) | [proof](https://alive2.llvm.org/ce/z/bYCa8V) | [proof](https://alive2.llvm.org/ce/z/9fsLsN) |
| umin      | [proof](https://alive2.llvm.org/ce/z/YnaSL-) | [proof](https://alive2.llvm.org/ce/z/rGrgoM) | [proof](https://alive2.llvm.org/ce/z/pb-ezQ) | [proof](https://alive2.llvm.org/ce/z/JkoqEi) |
| add       | [proof](https://alive2.llvm.org/ce/z/d5w5CF) | [proof](https://alive2.llvm.org/ce/z/GUgQ2Z) | [proof](https://alive2.llvm.org/ce/z/HnstY8) | [proof](https://alive2.llvm.org/ce/z/j8z_3C) |

| Test | Proof |
|------|-------|
| add_ult_max (ult 4 ≡ ne 4) | [proof](https://alive2.llvm.org/ce/z/pyDgTg) |
| add_ugt_max_minus_1 (ugt 3 ≡ eq 4) | [proof](https://alive2.llvm.org/ce/z/mHVXJk) |
| ashr_add_eq_0 (ashr instead of lshr) | [proof](https://alive2.llvm.org/ce/z/oa9Kgo) |

| Check | Equivalence | Proof |
|-----------------|-------------|-------|
| AnyNeg | or slt 0 ≡ umax slt 0 | [proof](https://alive2.llvm.org/ce/z/Do2tNQ) |
| AllNonNeg | or sgt -1 ≡ umax sgt -1 | [proof](https://alive2.llvm.org/ce/z/N4kZ8Z) |
| AllNeg | and slt 0 ≡ umin slt 0 | [proof](https://alive2.llvm.org/ce/z/4mNpMk) |
| AnyNonNeg | and sgt -1 ≡ umin sgt -1 | [proof](https://alive2.llvm.org/ce/z/2pVnyg) |
@SavchenkoValeriy
SavchenkoValeriy force-pushed the feat/vectorcombine/fold-sign-bit-check branch from 3a0b80a to 6459136 Compare January 23, 2026 09:48

@RKSimon RKSimon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - cheers

@RKSimon RKSimon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI failure

@SavchenkoValeriy

Copy link
Copy Markdown
Member Author

@RKSimon the new commit fixes the failure

@RKSimon RKSimon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@RKSimon
RKSimon enabled auto-merge (squash) January 23, 2026 15:30
@RKSimon
RKSimon merged commit 48fb51b into llvm:main Jan 23, 2026
10 of 11 checks passed
SavchenkoValeriy added a commit to swiftlang/llvm-project that referenced this pull request Jan 25, 2026
Fold patterns that extract sign bits, reduce them, and compare against
boundary values into direct sign checks on the reduced vector.

```
icmp pred (reduce.{add,or,and,umax,umin}(lshr X, BitWidth-1)), C
    ->  icmp slt/sgt (reduce.{or,umax,and,umin}(X)), 0/-1
```

When the comparison is against 0 or MAX (1 for boolean reductions,
NumElts for add), the pattern reduces to one of four quantified
predicates:
- ∀x: x < 0 (AllNeg)
- ∀x: x ≥ 0 (AllNonNeg)
- ∃x: x < 0 (AnyNeg)
- ∃x: x ≥ 0 (AnyNonNeg)

The transform eliminates the shift and selects between
reduce.or/reduce.umax or reduce.and/reduce.umin based on cost modeling.

comparison}:

| Reduction | == 0 | != 0 | == MAX | != MAX |
|-----------|------|------|--------|--------|
| or | [proof](https://alive2.llvm.org/ce/z/_BWxJW) |
[proof](https://alive2.llvm.org/ce/z/k3EiK6) |
[proof](https://alive2.llvm.org/ce/z/a8cAjp) |
[proof](https://alive2.llvm.org/ce/z/ci-HMt) |
| umax | [proof](https://alive2.llvm.org/ce/z/dWt28G) |
[proof](https://alive2.llvm.org/ce/z/_MqxXC) |
[proof](https://alive2.llvm.org/ce/z/KQebnF) |
[proof](https://alive2.llvm.org/ce/z/mixEgN) |
| and | [proof](https://alive2.llvm.org/ce/z/JgYrLj) |
[proof](https://alive2.llvm.org/ce/z/FZuPLy) |
[proof](https://alive2.llvm.org/ce/z/bYCa8V) |
[proof](https://alive2.llvm.org/ce/z/9fsLsN) |
| umin | [proof](https://alive2.llvm.org/ce/z/YnaSL-) |
[proof](https://alive2.llvm.org/ce/z/rGrgoM) |
[proof](https://alive2.llvm.org/ce/z/pb-ezQ) |
[proof](https://alive2.llvm.org/ce/z/JkoqEi) |
| add | [proof](https://alive2.llvm.org/ce/z/d5w5CF) |
[proof](https://alive2.llvm.org/ce/z/GUgQ2Z) |
[proof](https://alive2.llvm.org/ce/z/HnstY8) |
[proof](https://alive2.llvm.org/ce/z/j8z_3C) |

| Test | Proof |
|------|-------|
| or_slt_1 (slt 1 ≡ eq 0) | [proof](https://alive2.llvm.org/ce/z/Wdb_uN)
|
| umax_sgt_0 (sgt 0 ≡ ne 0) |
[proof](https://alive2.llvm.org/ce/z/nw6NZc) |
| and_slt_max (slt 1 ≡ ne 1) |
[proof](https://alive2.llvm.org/ce/z/ZDMSXZ) |
| umin_sgt_max_minus_1 (sgt 0 ≡ eq 1) |
[proof](https://alive2.llvm.org/ce/z/Uynf8P) |
| add_ult_max (ult 4 ≡ ne 4) |
[proof](https://alive2.llvm.org/ce/z/pyDgTg) |
| add_ugt_max_minus_1 (ugt 3 ≡ eq 4) |
[proof](https://alive2.llvm.org/ce/z/mHVXJk) |
| ashr_add_eq_0 (ashr instead of lshr) |
[proof](https://alive2.llvm.org/ce/z/oa9Kgo) |

| Check | Equivalence | Proof |
|-----------------|-------------|-------|
| AnyNeg | or slt 0 ≡ umax slt 0 |
[proof](https://alive2.llvm.org/ce/z/Do2tNQ) |
| AllNonNeg | or sgt -1 ≡ umax sgt -1 |
[proof](https://alive2.llvm.org/ce/z/N4kZ8Z) |
| AllNeg | and slt 0 ≡ umin slt 0 |
[proof](https://alive2.llvm.org/ce/z/4mNpMk) |
| AnyNonNeg | and sgt -1 ≡ umin sgt -1 |
[proof](https://alive2.llvm.org/ce/z/2pVnyg) |
SavchenkoValeriy added a commit to swiftlang/llvm-project that referenced this pull request Jan 25, 2026
Fold patterns that extract sign bits, reduce them, and compare against
boundary values into direct sign checks on the reduced vector.

```
icmp pred (reduce.{add,or,and,umax,umin}(lshr X, BitWidth-1)), C
    ->  icmp slt/sgt (reduce.{or,umax,and,umin}(X)), 0/-1
```

When the comparison is against 0 or MAX (1 for boolean reductions,
NumElts for add), the pattern reduces to one of four quantified
predicates:
- ∀x: x < 0 (AllNeg)
- ∀x: x ≥ 0 (AllNonNeg)
- ∃x: x < 0 (AnyNeg)
- ∃x: x ≥ 0 (AnyNonNeg)

The transform eliminates the shift and selects between
reduce.or/reduce.umax or reduce.and/reduce.umin based on cost modeling.

comparison}:

| Reduction | == 0 | != 0 | == MAX | != MAX |
|-----------|------|------|--------|--------|
| or | [proof](https://alive2.llvm.org/ce/z/_BWxJW) |
[proof](https://alive2.llvm.org/ce/z/k3EiK6) |
[proof](https://alive2.llvm.org/ce/z/a8cAjp) |
[proof](https://alive2.llvm.org/ce/z/ci-HMt) |
| umax | [proof](https://alive2.llvm.org/ce/z/dWt28G) |
[proof](https://alive2.llvm.org/ce/z/_MqxXC) |
[proof](https://alive2.llvm.org/ce/z/KQebnF) |
[proof](https://alive2.llvm.org/ce/z/mixEgN) |
| and | [proof](https://alive2.llvm.org/ce/z/JgYrLj) |
[proof](https://alive2.llvm.org/ce/z/FZuPLy) |
[proof](https://alive2.llvm.org/ce/z/bYCa8V) |
[proof](https://alive2.llvm.org/ce/z/9fsLsN) |
| umin | [proof](https://alive2.llvm.org/ce/z/YnaSL-) |
[proof](https://alive2.llvm.org/ce/z/rGrgoM) |
[proof](https://alive2.llvm.org/ce/z/pb-ezQ) |
[proof](https://alive2.llvm.org/ce/z/JkoqEi) |
| add | [proof](https://alive2.llvm.org/ce/z/d5w5CF) |
[proof](https://alive2.llvm.org/ce/z/GUgQ2Z) |
[proof](https://alive2.llvm.org/ce/z/HnstY8) |
[proof](https://alive2.llvm.org/ce/z/j8z_3C) |

| Test | Proof |
|------|-------|
| or_slt_1 (slt 1 ≡ eq 0) | [proof](https://alive2.llvm.org/ce/z/Wdb_uN)
|
| umax_sgt_0 (sgt 0 ≡ ne 0) |
[proof](https://alive2.llvm.org/ce/z/nw6NZc) |
| and_slt_max (slt 1 ≡ ne 1) |
[proof](https://alive2.llvm.org/ce/z/ZDMSXZ) |
| umin_sgt_max_minus_1 (sgt 0 ≡ eq 1) |
[proof](https://alive2.llvm.org/ce/z/Uynf8P) |
| add_ult_max (ult 4 ≡ ne 4) |
[proof](https://alive2.llvm.org/ce/z/pyDgTg) |
| add_ugt_max_minus_1 (ugt 3 ≡ eq 4) |
[proof](https://alive2.llvm.org/ce/z/mHVXJk) |
| ashr_add_eq_0 (ashr instead of lshr) |
[proof](https://alive2.llvm.org/ce/z/oa9Kgo) |

| Check | Equivalence | Proof |
|-----------------|-------------|-------|
| AnyNeg | or slt 0 ≡ umax slt 0 |
[proof](https://alive2.llvm.org/ce/z/Do2tNQ) |
| AllNonNeg | or sgt -1 ≡ umax sgt -1 |
[proof](https://alive2.llvm.org/ce/z/N4kZ8Z) |
| AllNeg | and slt 0 ≡ umin slt 0 |
[proof](https://alive2.llvm.org/ce/z/4mNpMk) |
| AnyNonNeg | and sgt -1 ≡ umin sgt -1 |
[proof](https://alive2.llvm.org/ce/z/2pVnyg) |
SavchenkoValeriy added a commit that referenced this pull request Jan 26, 2026
…7996)

#175194, #177159, and #173069 introduced the code calling
`TTI.getMinMaxReductionCost` with unexpected `Intrinsic::ID` causing
RISC-V to fail with `llvm_unreachable` panic.

Functionally, this is a small fix that also ports tests for the
aforementioned folds to RISCV.
SavchenkoValeriy added a commit to swiftlang/llvm-project that referenced this pull request Jan 26, 2026
…m#177996)

llvm#175194, llvm#177159, and llvm#173069 introduced the code calling
`TTI.getMinMaxReductionCost` with unexpected `Intrinsic::ID` causing
RISC-V to fail with `llvm_unreachable` panic.

Functionally, this is a small fix that also ports tests for the
aforementioned folds to RISCV.
Comment thread llvm/lib/Transforms/Vectorize/VectorCombine.cpp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants