8341470: BigDecimal.stripTrailingZeros() optimization

Reviewed-by: rgiulietti
This commit is contained in:
fabioromano1 2024-10-21 10:14:15 +00:00 committed by Raffaello Giulietti
parent 5d5d88ab9a
commit 27ef6c9df4
3 changed files with 214 additions and 32 deletions

View File

@ -3109,13 +3109,9 @@ public class BigDecimal extends Number implements Comparable<BigDecimal> {
* @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<BigDecimal> {
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<BigDecimal> {
* {@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);
}
/*

View File

@ -2779,6 +2779,13 @@ public class BigInteger extends Number implements Comparable<BigInteger> {
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.

View File

@ -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());
}
}