From 65650cfe80fcf83777bfd0c5daa223fa66159e4f Mon Sep 17 00:00:00 2001 From: Brett Wooldridge Date: Fri, 4 Sep 2026 12:35:22 +0900 Subject: [PATCH] perf: compare same-type numbers without going through BigDecimal Numbers.compare converted both operands to BigDecimal on every call, two allocations per comparison, and every index key comparison lands there. On a store with numeric index keys the conversion was the hottest frame in a multi-hour index rebuild. The common cases are now answered directly and exactly: any two integral primitives compare as longs, two doubles or two floats compare as themselves once NaN and the infinities have taken the existing special-case path, with -0.0 and 0.0 equal as BigDecimal treats them, and two BigDecimals or two BigIntegers use their own compareTo. Every mixed pairing keeps the exact BigDecimal conversion, so cross-type equality such as Integer 1 against Float 1.0f is unchanged. The parameterized test gains the long extremes, integral mixes, signed zeros, NaN ordering, a float widened against a double, and mixed pairs that must still take the exact path. Co-Authored-By: Claude Fable 5.1 --- .../org/dizitart/no2/common/util/Numbers.java | 34 +++++++++++++++++-- .../dizitart/no2/common/util/NumbersTest.java | 18 ++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/nitrite/src/main/java/org/dizitart/no2/common/util/Numbers.java b/nitrite/src/main/java/org/dizitart/no2/common/util/Numbers.java index a5e52d3e3..ea1ef77a1 100644 --- a/nitrite/src/main/java/org/dizitart/no2/common/util/Numbers.java +++ b/nitrite/src/main/java/org/dizitart/no2/common/util/Numbers.java @@ -32,9 +32,39 @@ private Numbers() { public static int compare(Number x, Number y) { if (isSpecial(x) || isSpecial(y)) { return Double.compare(x.doubleValue(), y.doubleValue()); - } else { - return toBigDecimal(x).compareTo(toBigDecimal(y)); } + + // Every index key comparison lands here, and going through BigDecimal allocates two + // objects per call. The common cases can be answered exactly without it: any two + // integral primitives fit a long, two doubles or two floats compare as themselves + // once NaN and the infinities are out of the way (with -0.0 and 0.0 equal, as + // BigDecimal treats them), and two BigDecimals or two BigIntegers already have a + // compareTo. Everything else, and every mixed pairing, keeps the exact conversion. + if (isIntegral(x) && isIntegral(y)) { + return Long.compare(x.longValue(), y.longValue()); + } + if (x instanceof Double && y instanceof Double) { + return compareFinite(x.doubleValue(), y.doubleValue()); + } + if (x instanceof Float && y instanceof Float) { + return compareFinite(x.floatValue(), y.floatValue()); + } + if (x instanceof BigDecimal && y instanceof BigDecimal) { + return ((BigDecimal) x).compareTo((BigDecimal) y); + } + if (x instanceof BigInteger && y instanceof BigInteger) { + return ((BigInteger) x).compareTo((BigInteger) y); + } + return toBigDecimal(x).compareTo(toBigDecimal(y)); + } + + private static boolean isIntegral(Number number) { + return number instanceof Long || number instanceof Integer + || number instanceof Short || number instanceof Byte; + } + + private static int compareFinite(double a, double b) { + return a < b ? -1 : (a > b ? 1 : 0); } private static boolean isSpecial(Number number) { diff --git a/nitrite/src/test/java/org/dizitart/no2/common/util/NumbersTest.java b/nitrite/src/test/java/org/dizitart/no2/common/util/NumbersTest.java index 853a19943..c0b4d4981 100644 --- a/nitrite/src/test/java/org/dizitart/no2/common/util/NumbersTest.java +++ b/nitrite/src/test/java/org/dizitart/no2/common/util/NumbersTest.java @@ -68,6 +68,24 @@ public static Collection data() { {Double.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, 0}, {Double.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, -1}, {Double.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, 0}, + // same-type fast paths must agree with the BigDecimal conversion + {Long.MAX_VALUE, Long.MIN_VALUE, 1}, + {Long.MIN_VALUE, Long.MAX_VALUE, -1}, + {Long.MAX_VALUE, Long.MAX_VALUE - 1, 1}, + {(byte) 3, 3L, 0}, + {(short) -2, -1, -1}, + {Integer.MIN_VALUE, Long.MIN_VALUE, 1}, + {0.0, -0.0, 0}, + {-0.0, 0.0, 0}, + {1.5, 1.25, 1}, + {1.5f, 1.75f, -1}, + {Double.NaN, 1.0, 1}, + {1.0, Double.NaN, -1}, + {0.1f, 0.1, 1}, // 0.1f widens to 0.10000000149, above 0.1: mixed types keep the exact path + {BigInteger.TEN, BigInteger.ONE, 1}, + {new BigDecimal("2.50"), new BigDecimal("2.5"), 0}, + {Long.MAX_VALUE, Double.MAX_VALUE, -1}, + {Long.MAX_VALUE, new BigInteger("9223372036854775808"), -1}, }); }