diff --git a/src/java.base/share/classes/java/math/BigDecimal.java b/src/java.base/share/classes/java/math/BigDecimal.java index d915568a502..abd49aa69bc 100644 --- a/src/java.base/share/classes/java/math/BigDecimal.java +++ b/src/java.base/share/classes/java/math/BigDecimal.java @@ -3109,13 +3109,9 @@ public class BigDecimal extends Number implements Comparable { * @since 1.5 */ public BigDecimal stripTrailingZeros() { - if (intCompact == 0 || (intVal != null && intVal.signum() == 0)) { - return BigDecimal.ZERO; - } else if (intCompact != INFLATED) { - return createAndStripZerosToMatchScale(intCompact, scale, Long.MIN_VALUE); - } else { - return createAndStripZerosToMatchScale(intVal, scale, Long.MIN_VALUE); - } + return intCompact == 0 || (intVal != null && intVal.signum() == 0) + ? BigDecimal.ZERO + : stripZerosToMatchScale(intVal, intCompact, scale, Long.MIN_VALUE); } // Comparison Operations @@ -5219,29 +5215,116 @@ public class BigDecimal extends Number implements Comparable { return commonNeedIncrement(roundingMode, qsign, cmpFracHalf, mq.isOdd()); } + /** + * {@code FIVE_TO_2_TO[n] == 5^(2^n)} + */ + private static final BigInteger[] FIVE_TO_2_TO = new BigInteger[16 + 1]; + + static { + BigInteger pow = FIVE_TO_2_TO[0] = BigInteger.valueOf(5L); + for (int i = 1; i < FIVE_TO_2_TO.length; i++) + FIVE_TO_2_TO[i] = pow = pow.multiply(pow); + } + + /** + * @param n a non-negative integer + * @return {@code 5^(2^n)} + */ + private static BigInteger fiveToTwoToThe(int n) { + int i = Math.min(n, FIVE_TO_2_TO.length - 1); + BigInteger pow = FIVE_TO_2_TO[i]; + for (; i < n; i++) + pow = pow.multiply(pow); + + return pow; + } + + private static final double LOG_5_OF_2 = 0.43067655807339306; // double closest to log5(2) + /** * Remove insignificant trailing zeros from this * {@code BigInteger} value until the preferred scale is reached or no * more zeros can be removed. If the preferred scale is less than * Integer.MIN_VALUE, all the trailing zeros will be removed. + * Assumes {@code intVal != 0}. * * @return new {@code BigDecimal} with a scale possibly reduced * to be closed to the preferred scale. * @throws ArithmeticException if scale overflows. */ private static BigDecimal createAndStripZerosToMatchScale(BigInteger intVal, int scale, long preferredScale) { + // avoid overflow of scale - preferredScale + preferredScale = Math.clamp(preferredScale, Integer.MIN_VALUE - 1L, Integer.MAX_VALUE); + int powsOf2 = intVal.getLowestSetBit(); + // scale - preferredScale >= remainingZeros >= max{n : (intVal % 10^n) == 0 && n <= scale - preferredScale} + // a multiple of 10^n must be a multiple of 2^n + long remainingZeros = Math.min(scale - preferredScale, powsOf2); + if (remainingZeros <= 0L) + return valueOf(intVal, scale, 0); + + final int sign = intVal.signum; + if (sign < 0) + intVal = intVal.negate(); // speed up computation of shiftRight() and bitLength() + + intVal = intVal.shiftRight(powsOf2); // remove powers of 2 + // Let k = max{n : (intVal % 5^n) == 0}, m = max{n : 5^n <= intVal}, so m >= k. + // Let b = intVal.bitLength(). It can be shown that + // | b * LOG_5_OF_2 - b log5(2) | < 2^(-21) (fp viz. real arithmetic), + // which entails m <= maxPowsOf5 <= m + 1, where maxPowsOf5 is as below. + // Hence, maxPowsOf5 >= k. + long maxPowsOf5 = Math.round(intVal.bitLength() * LOG_5_OF_2); + remainingZeros = Math.min(remainingZeros, maxPowsOf5); + BigInteger[] qr; // quotient-remainder pair - while (intVal.compareMagnitude(BigInteger.TEN) >= 0 - && scale > preferredScale) { - if (intVal.testBit(0)) - break; // odd number cannot end in 0 - qr = intVal.divideAndRemainder(BigInteger.TEN); - if (qr[1].signum() != 0) - break; // non-0 remainder - intVal = qr[0]; - scale = checkScale(intVal,(long) scale - 1); // could Overflow + // Remove 5^(2^i) from the factors of intVal, until 5^remainingZeros < 5^(2^i). + // Let z = max{n >= 0 : ((intVal * 2^powsOf2) % 10^n) == 0 && n <= scale - preferredScale}, + // then the condition min(scale - preferredScale, powsOf2) >= remainingZeros >= z + // and the values ((intVal * 2^powsOf2) / 10^z) and (scale - z) + // are preserved invariants after each iteration. + // Note that if intVal % 5^(2^i) != 0, the loop condition will become false. + for (int i = 0; remainingZeros >= 1L << i; i++) { + final int exp = 1 << i; + qr = intVal.divideAndRemainder(fiveToTwoToThe(i)); + if (qr[1].signum != 0) { // non-0 remainder + remainingZeros = exp - 1; + } else { + intVal = qr[0]; + scale = checkScale(intVal, (long) scale - exp); // could Overflow + remainingZeros -= exp; + powsOf2 -= exp; + } } - return valueOf(intVal, scale, 0); + + // bitLength(remainingZeros) == min{n >= 0 : 5^(2^n) > 5^remainingZeros} + // so, while the loop condition is true, + // the invariant i == max{n : 5^(2^n) <= 5^remainingZeros}, + // which is equivalent to i == bitLength(remainingZeros) - 1, + // is preserved at the beginning of each iteration. + // Note that the loop stops exactly when remainingZeros == 0. + // Using the same definition of z for the first loop, the invariants + // min(scale - preferredScale, powsOf2) >= remainingZeros >= z, + // ((intVal * 2^powsOf2) / 10^z) and (scale - z) + // are preserved in this loop as well, so, when the loop ends, + // remainingZeros == 0 implies z == 0, hence (intVal * 2^powsOf2) and scale + // have the correct values to return. + for (int i = BigInteger.bitLengthForLong(remainingZeros) - 1; i >= 0; i--) { + final int exp = 1 << i; + qr = intVal.divideAndRemainder(fiveToTwoToThe(i)); + if (qr[1].signum != 0) { // non-0 remainder + remainingZeros = exp - 1; + } else { + intVal = qr[0]; + scale = checkScale(intVal, (long) scale - exp); // could Overflow + remainingZeros -= exp; + powsOf2 -= exp; + + if (remainingZeros < exp >> 1) // else i == bitLength(remainingZeros) already + i = BigInteger.bitLengthForLong(remainingZeros); + } + } + + intVal = intVal.shiftLeft(powsOf2); // restore remaining powers of 2 + return valueOf(sign >= 0 ? intVal : intVal.negate(), scale, 0); } /** @@ -5249,31 +5332,27 @@ public class BigDecimal extends Number implements Comparable { * {@code long} value until the preferred scale is reached or no * more zeros can be removed. If the preferred scale is less than * Integer.MIN_VALUE, all the trailing zeros will be removed. + * Assumes {@code compactVal != 0 && compactVal != INFLATED}. * * @return new {@code BigDecimal} with a scale possibly reduced * to be closed to the preferred scale. * @throws ArithmeticException if scale overflows. */ private static BigDecimal createAndStripZerosToMatchScale(long compactVal, int scale, long preferredScale) { - while (Math.abs(compactVal) >= 10L && scale > preferredScale) { - if ((compactVal & 1L) != 0L) - break; // odd number cannot end in 0 - long r = compactVal % 10L; - if (r != 0L) - break; // non-0 remainder - compactVal /= 10; - scale = checkScale(compactVal, (long) scale - 1); // could Overflow + while (compactVal % 10L == 0L && scale > preferredScale) { + compactVal /= 10L; + scale = checkScale(compactVal, scale - 1L); // could Overflow } return valueOf(compactVal, scale); } - private static BigDecimal stripZerosToMatchScale(BigInteger intVal, long intCompact, int scale, int preferredScale) { - if(intCompact!=INFLATED) { - return createAndStripZerosToMatchScale(intCompact, scale, preferredScale); - } else { - return createAndStripZerosToMatchScale(intVal==null ? INFLATED_BIGINT : intVal, - scale, preferredScale); - } + /** + * Assumes {@code intVal != 0 && intCompact != 0}. + */ + private static BigDecimal stripZerosToMatchScale(BigInteger intVal, long intCompact, int scale, long preferredScale) { + return intCompact != INFLATED + ? createAndStripZerosToMatchScale(intCompact, scale, preferredScale) + : createAndStripZerosToMatchScale(intVal == null ? INFLATED_BIGINT : intVal, scale, preferredScale); } /* diff --git a/src/java.base/share/classes/java/math/BigInteger.java b/src/java.base/share/classes/java/math/BigInteger.java index 3a5fd143937..fb6d1eca3f5 100644 --- a/src/java.base/share/classes/java/math/BigInteger.java +++ b/src/java.base/share/classes/java/math/BigInteger.java @@ -2779,6 +2779,13 @@ public class BigInteger extends Number implements Comparable { return 32 - Integer.numberOfLeadingZeros(n); } + /** + * Package private method to return bit length for a long. + */ + static int bitLengthForLong(long n) { + return 64 - Long.numberOfLeadingZeros(n); + } + /** * Left shift int array a up to len by n bits. Returns the array that * results from the shift since space may have to be reallocated. diff --git a/test/micro/org/openjdk/bench/java/math/BigDecimalStripTrailingZeros.java b/test/micro/org/openjdk/bench/java/math/BigDecimalStripTrailingZeros.java new file mode 100644 index 00000000000..af5ce3aa9ac --- /dev/null +++ b/test/micro/org/openjdk/bench/java/math/BigDecimalStripTrailingZeros.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.math; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.math.BigInteger; +import java.math.BigDecimal; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Thread) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(value = 3) +public class BigDecimalStripTrailingZeros { + + private BigDecimal xsPow, sPow, mPow, lPow, xlPow; + + @Setup + public void setup() { + xsPow = new BigDecimal(BigInteger.TEN.pow(1 << 4)); + sPow = new BigDecimal(BigInteger.TEN.pow(1 << 5)); + mPow = new BigDecimal(BigInteger.TEN.pow(1 << 10)); + lPow = new BigDecimal(BigInteger.TEN.pow(1 << 15)); + xlPow = new BigDecimal(BigInteger.TEN.pow(1 << 20)); + } + + /** Test BigDecimal.stripTrailingZeros() with 10^16 */ + @Benchmark + @OperationsPerInvocation(1) + public void testXS(Blackhole bh) { + bh.consume(xsPow.stripTrailingZeros()); + } + + /** Test BigDecimal.stripTrailingZeros() with 10^32 */ + @Benchmark + @OperationsPerInvocation(1) + public void testS(Blackhole bh) { + bh.consume(sPow.stripTrailingZeros()); + } + + /** Test BigDecimal.stripTrailingZeros() with 10^1024 */ + @Benchmark + @OperationsPerInvocation(1) + public void testM(Blackhole bh) { + bh.consume(mPow.stripTrailingZeros()); + } + + /** Test BigDecimal.stripTrailingZeros() with 10^32_768 */ + @Benchmark + @OperationsPerInvocation(1) + public void testL(Blackhole bh) { + bh.consume(lPow.stripTrailingZeros()); + } + + /** Test BigDecimal.stripTrailingZeros() with 10^1_048_576 */ + @Benchmark + @OperationsPerInvocation(1) + public void testXL(Blackhole bh) { + bh.consume(xlPow.stripTrailingZeros()); + } +}