[llvm] [ConstantRange] Bail out early in unsignedMulMayOverflow for wide integer types (PR #192275)

via llvm-commits llvm-commits at lists.llvm.org
Sat Apr 18 02:57:41 PDT 2026


https://github.com/Firebear518 updated https://github.com/llvm/llvm-project/pull/192275

>From ad8b8edd135270a4b5ecb45eb25f2d8d8bc765d0 Mon Sep 17 00:00:00 2001
From: Firebear518 <phj040518 at gmail.com>
Date: Fri, 17 Apr 2026 17:49:25 +0900
Subject: [PATCH 1/3] [APInt][ConstantRange] Add umul_has_overflow; fix O(n^2)
 hang in unsignedMulMayOverflow
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Problem
-------
ConstantRange::unsignedMulMayOverflow called APInt::umul_ov on the range
bounds to determine overflow. APInt multiplication is O(n^2) in the word
count, so integer types with very large bit widths (e.g. i5754496) caused
multi-second hangs during InstCombine. Discovered via llvm-opt-fuzzer.

Solution
--------
Introduce APInt::umul_has_overflow, a flag-only variant of umul_ov that
avoids computing the full product. It uses three levels of detection:

  1. LZ >= BitWidth  — leading zeros prove the product fits; return false.
  2. LZ + 1 < BitWidth — leading zeros prove overflow is certain; return true.
  3. Borderline (LZ == BitWidth-1):
     - isOne() guard: 1*x = x always fits, skipping the O(n^2) multiply.
     - Shift trick (same as umul_ov): still O(n^2) for non-trivial values,
       but avoids reconstructing the full product value.

Also apply the LZ >= BitWidth no-overflow fast path and the isOne guard
to umul_ov itself for symmetry (benefits callers such as umul_sat).

Use umul_has_overflow in unsignedMulMayOverflow in place of umul_ov.

Performance (ASan build, i5754496 reproducer):
  Before: ~30 s   After: ~0.23 s

Testing
-------
  - New APIntTest.umul_has_overflow: exhaustive 1-5 bit agreement with
    umul_ov (overflow flag + product value), 64-bit spot checks, edge
    cases, and a 2048-bit wide-integer smoke test that also explicitly
    exercises umul_ov's new isOne borderline path.
  - New ConstantRangeTest.UnsignedMulOverflowWideTypeBailout: verifies
    correctness and fast-path behaviour for 2048-bit types.

Fixes: https://github.com/llvm/llvm-project/issues/192277

Co-Authored-By: Claude Sonnet 4.6 <noreply at anthropic.com>
---
 llvm/docs/Frontend/PerformanceTips.rst  |  4 ++
 llvm/include/llvm/ADT/APInt.h           |  5 ++
 llvm/lib/IR/ConstantRange.cpp           |  7 +--
 llvm/lib/Support/APInt.cpp              | 65 ++++++++++++++++++++++-
 llvm/unittests/ADT/APIntTest.cpp        | 70 +++++++++++++++++++++++++
 llvm/unittests/IR/ConstantRangeTest.cpp | 28 ++++++++++
 6 files changed, 173 insertions(+), 6 deletions(-)

diff --git a/llvm/docs/Frontend/PerformanceTips.rst b/llvm/docs/Frontend/PerformanceTips.rst
index a11c8e3a16857..17f7cdd04a8e6 100644
--- a/llvm/docs/Frontend/PerformanceTips.rst
+++ b/llvm/docs/Frontend/PerformanceTips.rst
@@ -157,6 +157,10 @@ As a result, alignment is mandatory for atomic loads and stores.
 Other Things to Consider
 ^^^^^^^^^^^^^^^^^^^^^^^^
 
+#. Avoid emitting extremely wide integer types unless necessary.
+   The optimizer is not designed for types exceeding a few thousand bits, 
+   and the super-linear complexity of several passes will cause extreme compile-time delays.
+
 #. Use ptrtoint/inttoptr sparingly (they interfere with pointer aliasing
    analysis), prefer GEPs
 
diff --git a/llvm/include/llvm/ADT/APInt.h b/llvm/include/llvm/ADT/APInt.h
index 683eea4fb19a3..d1334af1d33bc 100644
--- a/llvm/include/llvm/ADT/APInt.h
+++ b/llvm/include/llvm/ADT/APInt.h
@@ -1026,6 +1026,11 @@ class [[nodiscard]] APInt {
   LLVM_ABI APInt ushl_ov(const APInt &Amt, bool &Overflow) const;
   LLVM_ABI APInt ushl_ov(unsigned Amt, bool &Overflow) const;
 
+  // Overflow predicates (flag only, no product or shifted value computed).
+  /// Return true if unsigned multiplication of this and RHS would overflow.
+  /// Prefer this over umul_ov when only the overflow flag is needed.
+  LLVM_ABI bool umul_has_overflow(const APInt &RHS) const;
+
   /// Signed integer floor division operation.
   ///
   /// Rounds towards negative infinity, i.e. 5 / -2 = -3. Iff minimum value
diff --git a/llvm/lib/IR/ConstantRange.cpp b/llvm/lib/IR/ConstantRange.cpp
index 4c10dfb140cbe..5945855cd738e 100644
--- a/llvm/lib/IR/ConstantRange.cpp
+++ b/llvm/lib/IR/ConstantRange.cpp
@@ -2263,14 +2263,11 @@ ConstantRange::OverflowResult ConstantRange::unsignedMulMayOverflow(
 
   APInt Min = getUnsignedMin(), Max = getUnsignedMax();
   APInt OtherMin = Other.getUnsignedMin(), OtherMax = Other.getUnsignedMax();
-  bool Overflow;
 
-  (void) Min.umul_ov(OtherMin, Overflow);
-  if (Overflow)
+  if (Min.umul_has_overflow(OtherMin))
     return OverflowResult::AlwaysOverflowsHigh;
 
-  (void) Max.umul_ov(OtherMax, Overflow);
-  if (Overflow)
+  if (Max.umul_has_overflow(OtherMax))
     return OverflowResult::MayOverflow;
 
   return OverflowResult::NeverOverflows;
diff --git a/llvm/lib/Support/APInt.cpp b/llvm/lib/Support/APInt.cpp
index 6aa5fc615a302..4e08d9418bad7 100644
--- a/llvm/lib/Support/APInt.cpp
+++ b/llvm/lib/Support/APInt.cpp
@@ -2009,11 +2009,40 @@ APInt APInt::smul_ov(const APInt &RHS, bool &Overflow) const {
 }
 
 APInt APInt::umul_ov(const APInt &RHS, bool &Overflow) const {
-  if (countl_zero() + RHS.countl_zero() + 2 <= BitWidth) {
+  if (BitWidth == 0) {
+    Overflow = false;
+    return APInt(BitWidth, 0);
+  }
+  unsigned LZ = countl_zero() + RHS.countl_zero();
+
+  // No-overflow: product fits in BitWidth bits (bitlen(a)+bitlen(b) <=
+  // BitWidth).
+  if (LZ >= BitWidth) {
+    Overflow = false;
+    return *this * RHS;
+  }
+
+  // Guaranteed overflow: product needs more than BitWidth bits.
+  if (LZ < BitWidth - 1) {
     Overflow = true;
     return *this * RHS;
   }
 
+  // Borderline (LZ == BitWidth-1): skip the slow path for trivial multipliers.
+  // *this==1: 1*x=x always fits, and avoids a wasteful lshr(1)==0 multiply.
+  // RHS==1:   x*1=x always fits.
+  if (isOne()) {
+    Overflow = false;
+    return RHS;
+  }
+  if (RHS.isOne()) {
+    Overflow = false;
+    return *this;
+  }
+
+  // Shift trick: a*b = 2*(a>>1)*b + (a&1)*b.
+  // 1. If (a>>1)*b has MSB set, the `<< 1` overflows.
+  // 2. Add (a&1)*b and check for unsigned wrap-around.
   APInt Res = lshr(1) * RHS;
   Overflow = Res.isNegative();
   Res <<= 1;
@@ -2022,9 +2051,43 @@ APInt APInt::umul_ov(const APInt &RHS, bool &Overflow) const {
     if (Res.ult(RHS))
       Overflow = true;
   }
+
   return Res;
 }
 
+bool APInt::umul_has_overflow(const APInt &RHS) const {
+  // Use leading zeros to bound the product's bit length.
+  // This implicitly handles isZero() cases (where LZ >= BitWidth).
+  unsigned LZ = countl_zero() + RHS.countl_zero();
+
+  // Fast path: Guaranteed to fit.
+  if (LZ >= BitWidth)
+    return false;
+
+  // Fast path: Guaranteed to overflow.
+  if (LZ < BitWidth - 1)
+    return true;
+
+  // Borderline (LZ == BitWidth-1): avoid O(n^2) slow path for trivial
+  // multipliers.
+  if (isOne() || RHS.isOne())
+    return false;
+
+  // Slow path: O(n^2) shift trick.
+  // Note: checking only Res.isNegative() is insufficient when *this is odd;
+  // the (a&1)*RHS term must be accounted for via the ult wrap-around check.
+  APInt Res = lshr(1) * RHS;
+  if (Res.isNegative())
+    return true;
+  Res <<= 1;
+  if ((*this)[0]) {
+    Res += RHS;
+    return Res.ult(RHS);
+  }
+
+  return false;
+}
+
 APInt APInt::sshl_ov(const APInt &ShAmt, bool &Overflow) const {
   return sshl_ov(ShAmt.getLimitedValue(getBitWidth()), Overflow);
 }
diff --git a/llvm/unittests/ADT/APIntTest.cpp b/llvm/unittests/ADT/APIntTest.cpp
index ee4c59de34fc4..4715fd23fc128 100644
--- a/llvm/unittests/ADT/APIntTest.cpp
+++ b/llvm/unittests/ADT/APIntTest.cpp
@@ -3419,6 +3419,76 @@ TEST(APIntTest, umul_ov) {
       }
 }
 
+TEST(APIntTest, umul_has_overflow) {
+  // umul_has_overflow must agree with umul_ov for all values up to 5 bits.
+  // Also verify that umul_ov returns the correct modular product value,
+  // exercising the fast paths added to umul_ov (LZ >= BitWidth, LZ <
+  // BitWidth-1, and the isOne() borderline guard).
+  for (unsigned Bits = 1; Bits <= 5; ++Bits) {
+    for (unsigned A = 0; A != 1u << Bits; ++A) {
+      for (unsigned B = 0; B != 1u << Bits; ++B) {
+        APInt N1(Bits, A), N2(Bits, B);
+        bool OvFromOv;
+        APInt Product = N1.umul_ov(N2, OvFromOv);
+        EXPECT_EQ(OvFromOv, N1.umul_has_overflow(N2))
+            << "Overflow flag mismatch at " << Bits << "-bit " << A << " * "
+            << B;
+        EXPECT_EQ(Product, N1 * N2)
+            << "Product mismatch at " << Bits << "-bit " << A << " * " << B;
+      }
+    }
+  }
+
+  // Spot-check 64-bit known-overflow and known-non-overflow cases.
+  const std::pair<uint64_t, uint64_t> Overflows64[] = {
+      {0x8000000000000000ULL, 2},
+      {0x5555555555555556ULL, 3},
+      {4294967296ULL, 4294967296ULL},
+  };
+  const std::pair<uint64_t, uint64_t> NonOverflows64[] = {
+      {0x7fffffffffffffffULL, 2},
+      {0x5555555555555555ULL, 3},
+      {4294967295ULL, 4294967297ULL},
+  };
+  for (auto &[X, Y] : Overflows64) {
+    EXPECT_TRUE(APInt(64, X).umul_has_overflow(APInt(64, Y)));
+  }
+  for (auto &[X, Y] : NonOverflows64) {
+    EXPECT_FALSE(APInt(64, X).umul_has_overflow(APInt(64, Y)));
+  }
+
+  // Edge cases: zero and one never overflow.
+  EXPECT_FALSE(APInt(64, 0).umul_has_overflow(APInt(64, UINT64_MAX)));
+  EXPECT_FALSE(APInt(64, UINT64_MAX).umul_has_overflow(APInt(64, 0)));
+  EXPECT_FALSE(APInt(64, 1).umul_has_overflow(APInt(64, UINT64_MAX)));
+  EXPECT_FALSE(APInt(64, UINT64_MAX).umul_has_overflow(APInt(64, 1)));
+
+  // Wide-integer performance: 2048-bit values should complete instantly.
+  // (Without the O(n) fast paths these would take seconds for ~1M-bit types.)
+  const unsigned W = 2048;
+  APInt WMax = APInt::getMaxValue(W);
+  APInt WOne(W, 1);
+  APInt WTwo(W, 2);
+  APInt WHalf = APInt::getSignedMaxValue(W); // top bit clear, rest 1s
+  EXPECT_TRUE(WMax.umul_has_overflow(WTwo)); // 0xFF..F * 2 overflows
+  EXPECT_TRUE(WTwo.umul_has_overflow(WMax)); // symmetric
+  EXPECT_FALSE(APInt(W, 0).umul_has_overflow(WMax));
+  EXPECT_FALSE(WOne.umul_has_overflow(WMax));
+  EXPECT_FALSE(
+      WHalf.umul_has_overflow(WTwo)); // (2^W/2 - 1) * 2 < 2^W, no overflow
+
+  // Verify umul_ov's isOne borderline guard for wide types: 1 * MAX is a
+  // borderline case (LZ == W-1) that hits the isOne() fast path. Check both
+  // the overflow flag and the returned product value.
+  bool OvWide;
+  APInt ProdWide = WOne.umul_ov(WMax, OvWide);
+  EXPECT_FALSE(OvWide);
+  EXPECT_EQ(ProdWide, WMax);
+  ProdWide = WMax.umul_ov(WOne, OvWide); // symmetric
+  EXPECT_FALSE(OvWide);
+  EXPECT_EQ(ProdWide, WMax);
+}
+
 TEST(APIntTest, smul_ov) {
   for (unsigned Bits = 1; Bits <= 5; ++Bits)
     for (unsigned A = 0; A != 1u << Bits; ++A)
diff --git a/llvm/unittests/IR/ConstantRangeTest.cpp b/llvm/unittests/IR/ConstantRangeTest.cpp
index 13712a76d3edf..8d1b38ef1e16e 100644
--- a/llvm/unittests/IR/ConstantRangeTest.cpp
+++ b/llvm/unittests/IR/ConstantRangeTest.cpp
@@ -2506,6 +2506,34 @@ TEST_F(ConstantRangeTest, UnsignedMulOverflowExhaustive) {
       });
 }
 
+// Regression test: very wide integer types triggered an O(n^2) APInt
+// multiplication inside unsignedMulMayOverflow via APInt::umul_ov, causing
+// multi-second hangs in InstCombine (discovered via llvm-opt-fuzzer with
+// i5754496). The fix uses umul_has_overflow with O(1)/O(n) fast paths.
+TEST_F(ConstantRangeTest, UnsignedMulOverflowWideTypeBailout) {
+  // Verify correctness and fast-path for wide integer types (2048-bit).
+  // Previously, unsignedMulMayOverflow called umul_ov which is O(n^2) and
+  // caused multi-second hangs on wide types. Now umul_has_overflow is used
+  // with O(1) fast paths via leading-zero counts.
+  const unsigned W = 2048;
+  ConstantRange WFull = ConstantRange::getFull(W);
+  ConstantRange WOne(APInt(W, 1));
+  ConstantRange WMax(APInt::getMaxValue(W));
+  ConstantRange WEmpty = ConstantRange::getEmpty(W);
+
+  // [0, MAX] x [0, MAX]: 0*0 ok, MAX*MAX overflows -> MayOverflow.
+  EXPECT_MAY_OVERFLOW(WFull.unsignedMulMayOverflow(WFull));
+  // 1 * MAX = MAX, fits in W bits -> NeverOverflows (precise result).
+  EXPECT_NEVER_OVERFLOWS(WOne.unsignedMulMayOverflow(WMax));
+  EXPECT_NEVER_OVERFLOWS(WMax.unsignedMulMayOverflow(WOne));
+  // MAX * MAX always overflows; fast path via countl_zero ->
+  // AlwaysOverflowsHigh.
+  EXPECT_ALWAYS_OVERFLOWS_HIGH(WMax.unsignedMulMayOverflow(WMax));
+  // Empty-set cases return MayOverflow by convention.
+  EXPECT_MAY_OVERFLOW(WEmpty.unsignedMulMayOverflow(WFull));
+  EXPECT_MAY_OVERFLOW(WFull.unsignedMulMayOverflow(WEmpty));
+}
+
 TEST_F(ConstantRangeTest, SignedAddOverflowExhaustive) {
   TestOverflowExhaustive(
       [](bool &IsOverflowHigh, const APInt &N1, const APInt &N2) {

>From 9472bece2100a22c222c7df78628f6bd77a1f732 Mon Sep 17 00:00:00 2001
From: Firebear518 <phj040518 at gmail.com>
Date: Sat, 18 Apr 2026 09:17:27 +0900
Subject: [PATCH 2/3] [APInt][Docs] Fix umul_ov/umul_has_overflow; update
 PerformanceTips

- APInt::umul_ov: merge isOne()/RHS.isOne() checks into single branch;
  move leading-zero comment to clarify zero-handling intent
- APInt::umul_has_overflow: add BitWidth==0 early return guard
- PerformanceTips.rst: remove trailing whitespace; tighten wording on
  wide-integer-type warning

Co-Authored-By: Claude Sonnet 4.6 <noreply at anthropic.com>
---
 llvm/docs/Frontend/PerformanceTips.rst |  4 ++--
 llvm/lib/Support/APInt.cpp             | 12 +++++-------
 2 files changed, 7 insertions(+), 9 deletions(-)

diff --git a/llvm/docs/Frontend/PerformanceTips.rst b/llvm/docs/Frontend/PerformanceTips.rst
index 17f7cdd04a8e6..e0c46b6fdf154 100644
--- a/llvm/docs/Frontend/PerformanceTips.rst
+++ b/llvm/docs/Frontend/PerformanceTips.rst
@@ -157,8 +157,8 @@ As a result, alignment is mandatory for atomic loads and stores.
 Other Things to Consider
 ^^^^^^^^^^^^^^^^^^^^^^^^
 
-#. Avoid emitting extremely wide integer types unless necessary.
-   The optimizer is not designed for types exceeding a few thousand bits, 
+#. Avoid emitting extremely wide integer types.
+   The optimizer is not designed for types exceeding a few thousand bits,
    and the super-linear complexity of several passes will cause extreme compile-time delays.
 
 #. Use ptrtoint/inttoptr sparingly (they interfere with pointer aliasing
diff --git a/llvm/lib/Support/APInt.cpp b/llvm/lib/Support/APInt.cpp
index 4e08d9418bad7..b3bad7bf27887 100644
--- a/llvm/lib/Support/APInt.cpp
+++ b/llvm/lib/Support/APInt.cpp
@@ -2013,6 +2013,8 @@ APInt APInt::umul_ov(const APInt &RHS, bool &Overflow) const {
     Overflow = false;
     return APInt(BitWidth, 0);
   }
+  // Use leading zeros to bound the product's bit length.
+  // This implicitly handles isZero() cases (where LZ >= BitWidth).
   unsigned LZ = countl_zero() + RHS.countl_zero();
 
   // No-overflow: product fits in BitWidth bits (bitlen(a)+bitlen(b) <=
@@ -2031,14 +2033,10 @@ APInt APInt::umul_ov(const APInt &RHS, bool &Overflow) const {
   // Borderline (LZ == BitWidth-1): skip the slow path for trivial multipliers.
   // *this==1: 1*x=x always fits, and avoids a wasteful lshr(1)==0 multiply.
   // RHS==1:   x*1=x always fits.
-  if (isOne()) {
+  if (isOne() || RHS.isOne()) {
     Overflow = false;
     return RHS;
   }
-  if (RHS.isOne()) {
-    Overflow = false;
-    return *this;
-  }
 
   // Shift trick: a*b = 2*(a>>1)*b + (a&1)*b.
   // 1. If (a>>1)*b has MSB set, the `<< 1` overflows.
@@ -2056,8 +2054,8 @@ APInt APInt::umul_ov(const APInt &RHS, bool &Overflow) const {
 }
 
 bool APInt::umul_has_overflow(const APInt &RHS) const {
-  // Use leading zeros to bound the product's bit length.
-  // This implicitly handles isZero() cases (where LZ >= BitWidth).
+  if (BitWidth == 0)
+    return false;
   unsigned LZ = countl_zero() + RHS.countl_zero();
 
   // Fast path: Guaranteed to fit.

>From 8f5179c3505b5812666e92a3ac721b9a47f58644 Mon Sep 17 00:00:00 2001
From: Firebear518 <phj040518 at gmail.com>
Date: Sat, 18 Apr 2026 10:24:43 +0900
Subject: [PATCH 3/3] [APInt] Refine umul_ov: fix isOne return value; remove
 stale comments

- umul_ov: return correct value when RHS.isOne() (was always returning
  RHS; now returns *this when *this is not one)
- Remove blank lines and outdated borderline-case comment for clarity
- umul_has_overflow: remove stray blank line after LZ>=BitWidth check

Co-Authored-By: Claude Sonnet 4.6 <noreply at anthropic.com>
---
 llvm/lib/Support/APInt.cpp | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/llvm/lib/Support/APInt.cpp b/llvm/lib/Support/APInt.cpp
index b3bad7bf27887..d91707fb8889b 100644
--- a/llvm/lib/Support/APInt.cpp
+++ b/llvm/lib/Support/APInt.cpp
@@ -2023,19 +2023,15 @@ APInt APInt::umul_ov(const APInt &RHS, bool &Overflow) const {
     Overflow = false;
     return *this * RHS;
   }
-
   // Guaranteed overflow: product needs more than BitWidth bits.
   if (LZ < BitWidth - 1) {
     Overflow = true;
     return *this * RHS;
   }
 
-  // Borderline (LZ == BitWidth-1): skip the slow path for trivial multipliers.
-  // *this==1: 1*x=x always fits, and avoids a wasteful lshr(1)==0 multiply.
-  // RHS==1:   x*1=x always fits.
   if (isOne() || RHS.isOne()) {
     Overflow = false;
-    return RHS;
+    return isOne() ? RHS : *this;
   }
 
   // Shift trick: a*b = 2*(a>>1)*b + (a&1)*b.
@@ -2061,7 +2057,6 @@ bool APInt::umul_has_overflow(const APInt &RHS) const {
   // Fast path: Guaranteed to fit.
   if (LZ >= BitWidth)
     return false;
-
   // Fast path: Guaranteed to overflow.
   if (LZ < BitWidth - 1)
     return true;



More information about the llvm-commits mailing list