[llvm] [ConstantRange] Bail out early in unsignedMulMayOverflow for wide integer types (PR #192275)
via llvm-commits
llvm-commits at lists.llvm.org
Fri Apr 17 04:11:21 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] [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) {
More information about the llvm-commits
mailing list