diff --git a/src/java.base/share/classes/java/math/BigDecimal.java b/src/java.base/share/classes/java/math/BigDecimal.java index b00970963b6b3..524e0303d9e0f 100644 --- a/src/java.base/share/classes/java/math/BigDecimal.java +++ b/src/java.base/share/classes/java/math/BigDecimal.java @@ -3495,15 +3495,18 @@ public String toEngineeringString() { * @see #toEngineeringString() */ public String toPlainString() { - if(scale==0) { - if(intCompact!=INFLATED) { - return Long.toString(intCompact); - } else { - return intVal.toString(); - } - } - if(this.scale<0) { // No decimal point - if(signum()==0) { + int scale = this.scale; + long intCompact = this.intCompact; + + if (scale == 0) + return unscaledString(); + // currency fast path + if (scale == 2 && intCompact != INFLATED) + return scale2(intCompact); + + int signum = signum(); + if (this.scale < 0) { // No decimal point + if (signum == 0) { return "0"; } int trailingZeros = checkScaleNonZero((-(long)scale)); @@ -3514,18 +3517,14 @@ public String toPlainString() { if (len < 0) { throw new OutOfMemoryError("too large to fit in a String"); } - StringBuilder buf = new StringBuilder(len); - buf.append(str); - buf.repeat('0', trailingZeros); - return buf.toString(); - } - String str; - if(intCompact!=INFLATED) { - str = Long.toString(Math.abs(intCompact)); - } else { - str = intVal.abs().toString(); + + return new StringBuilder(len) + .append(str) + .repeat('0', trailingZeros) + .toString(); } - return getValueString(signum(), str, scale); + + return getValueString(signum, unscaledAbsString(), scale); } /* Returns a digit.digit string */ @@ -3534,21 +3533,22 @@ private static String getValueString(int signum, String intString, int scale) { StringBuilder buf; int insertionPoint = intString.length() - scale; if (insertionPoint == 0) { /* Point goes just before intVal */ - return (signum<0 ? "-0." : "0.") + intString; + return (signum < 0 ? "-0." : "0.").concat(intString); } else if (insertionPoint > 0) { /* Point goes inside intVal */ - buf = new StringBuilder(intString); - buf.insert(insertionPoint, '.'); + buf = new StringBuilder(); if (signum < 0) - buf.insert(0, '-'); + buf.append('-'); + buf.append(intString) + .insert(insertionPoint + (signum < 0 ? 1 : 0), '.'); } else { /* We must insert zeros between point and intVal */ int len = (signum < 0 ? 3 : 2) + scale; if (len < 0) { throw new OutOfMemoryError("too large to fit in a String"); } - buf = new StringBuilder(len); - buf.append(signum<0 ? "-0." : "0."); - buf.repeat('0', -insertionPoint); // insertionPoint != MIN_VALUE - buf.append(intString); + buf = new StringBuilder(len) + .append(signum<0 ? "-0." : "0.") + .repeat('0', -insertionPoint) // insertionPoint != MIN_VALUE + .append(intString); } return buf.toString(); } @@ -4185,75 +4185,45 @@ private String layoutChars(boolean sci) { long intCompact = this.intCompact; int scale = this.scale; if (scale == 0) // zero scale is trivial - return (intCompact != INFLATED) ? - Long.toString(intCompact): - intVal.toString(); - if (scale == 2 && - intCompact >= 0 && intCompact < Integer.MAX_VALUE) { - // currency fast path - int lowInt = (int)intCompact % 100; - int highInt = (int)intCompact / 100; - int highIntSize = DecimalDigits.stringSize(highInt); - byte[] buf = new byte[highIntSize + 3]; - DecimalDigits.putPairLatin1(buf, highIntSize + 1, lowInt); - buf[highIntSize] = '.'; - DecimalDigits.getCharsLatin1(highInt, highIntSize, buf); - try { - return JLA.newStringNoRepl(buf, StandardCharsets.ISO_8859_1); - } catch (CharacterCodingException cce) { - throw new AssertionError(cce); - } - } + return unscaledString(); + if (scale == 2 && intCompact != INFLATED) + return scale2(intCompact); - char[] coeff; - int offset; // offset is the starting index for coeff array // Get the significand as an absolute value - if (intCompact != INFLATED) { - coeff = new char[19]; - offset = DecimalDigits.getChars(Math.abs(intCompact), coeff.length, coeff); - } else { - offset = 0; - coeff = intVal.abs().toString().toCharArray(); - } + String coeff = unscaledAbsString(); // Construct a buffer, with sufficient capacity for all cases. // If E-notation is needed, length will be: +1 if negative, +1 // if '.' needed, +2 for "E+", + up to 10 for adjusted exponent. // Otherwise it could have +1 if negative, plus leading "0.00000" - StringBuilder buf = new StringBuilder(32);; - if (signum() < 0) // prefix '-' if negative - buf.append('-'); - int coeffLen = coeff.length - offset; + int coeffLen = coeff.length(); long adjusted = -(long)scale + (coeffLen -1); if ((scale >= 0) && (adjusted >= -6)) { // plain number - int pad = scale - coeffLen; // count of padding zeros - if (pad >= 0) { // 0.xxx form - buf.append('0'); - buf.append('.'); - for (; pad>0; pad--) { - buf.append('0'); - } - buf.append(coeff, offset, coeffLen); - } else { // xx.xx form - buf.append(coeff, offset, -pad); - buf.append('.'); - buf.append(coeff, -pad + offset, scale); + return getValueString(signum(), coeff, scale); + } + // E-notation is needed + return layoutCharsE(sci, coeff, coeffLen, adjusted); + } + + private String layoutCharsE(boolean sci, String coeff, int coeffLen, long adjusted) { + StringBuilder buf = new StringBuilder(32); + int signum = signum(); + if (signum < 0) // prefix '-' if negative + buf.append('-'); + if (sci) { // Scientific notation + buf.append(coeff.charAt(0)); // first character + if (coeffLen > 1) { // more to come + buf.append('.') + .append(coeff, 1, coeffLen); } - } else { // E-notation is needed - if (sci) { // Scientific notation - buf.append(coeff[offset]); // first character - if (coeffLen > 1) { // more to come - buf.append('.'); - buf.append(coeff, offset + 1, coeffLen - 1); - } - } else { // Engineering notation - int sig = (int)(adjusted % 3); - if (sig < 0) - sig += 3; // [adjusted was negative] - adjusted -= sig; // now a multiple of 3 - sig++; - if (signum() == 0) { - switch (sig) { + } else { // Engineering notation + int sig = (int)(adjusted % 3); + if (sig < 0) + sig += 3; // [adjusted was negative] + adjusted -= sig; // now a multiple of 3 + sig++; + if (signum == 0) { + switch (sig) { case 1: buf.append('0'); // exponent is a multiple of three break; @@ -4267,29 +4237,65 @@ private String layoutChars(boolean sci) { break; default: throw new AssertionError("Unexpected sig value " + sig); - } - } else if (sig >= coeffLen) { // significand all in integer - buf.append(coeff, offset, coeffLen); - // may need some zeros, too - for (int i = sig - coeffLen; i > 0; i--) { - buf.append('0'); - } - } else { // xx.xxE form - buf.append(coeff, offset, sig); - buf.append('.'); - buf.append(coeff, offset + sig, coeffLen - sig); } + } else if (sig >= coeffLen) {// significand all in integer + buf.append(coeff, 0, coeffLen) + .repeat('0', sig - coeffLen); // may need some zeros, too + } else { // xx.xxE form + buf.append(coeff, 0, sig) + .append('.') + .append(coeff, sig, coeffLen); } - if (adjusted != 0) { // [!sci could have made 0] - buf.append('E'); - if (adjusted > 0) // force sign for positive - buf.append('+'); - buf.append(adjusted); - } + } + if (adjusted != 0) { // [!sci could have made 0] + buf.append('E'); + if (adjusted > 0) // force sign for positive + buf.append('+'); + buf.append(adjusted); } return buf.toString(); } + private String scale2(long intCompact) { + boolean negative = intCompact < 0; + if (negative) { + intCompact = -intCompact; + } + int lowInt = (int) (intCompact % 100); + long highInt = intCompact / 100; + int highIntSize = DecimalDigits.stringSize(highInt); + if (negative) { + highIntSize++; + } + byte[] buf = new byte[highIntSize + 3]; + if (negative) { + buf[0] = '-'; + } + DecimalDigits.putPairLatin1(buf, highIntSize + 1, lowInt); + buf[highIntSize] = '.'; + DecimalDigits.getCharsLatin1(highInt, highIntSize, buf); + try { + return JLA.newStringNoRepl(buf, StandardCharsets.ISO_8859_1); + } catch (CharacterCodingException cce) { + throw new AssertionError(cce); + } + } + + /** + * Get the significand as an absolute value + */ + private String unscaledAbsString() { + return intCompact != INFLATED + ? Long.toString(Math.abs(intCompact)) + : intVal.abs().toString(); + } + + private String unscaledString() { + return intCompact != INFLATED + ? Long.toString(intCompact) + : intVal.toString(); + } + /** * Return 10 to the power n, as a {@code BigInteger}. * diff --git a/src/java.base/share/classes/jdk/internal/util/DecimalDigits.java b/src/java.base/share/classes/jdk/internal/util/DecimalDigits.java index 75e67e3f9cc90..3158de2849f89 100644 --- a/src/java.base/share/classes/jdk/internal/util/DecimalDigits.java +++ b/src/java.base/share/classes/jdk/internal/util/DecimalDigits.java @@ -336,70 +336,6 @@ public static int getCharsUTF16(long i, int index, byte[] buf) { return charPos; } - /** - * This is a variant of {@link DecimalDigits#getCharsUTF16(long, int, byte[])}, but for - * UTF-16 coder. - * - * @param i value to convert - * @param index next index, after the least significant digit - * @param buf target buffer, UTF16-coded. - * @return index of the most significant digit or minus sign, if present - */ - public static int getChars(long i, int index, char[] buf) { - // Used by trusted callers. Assumes all necessary bounds checks have been done by the caller. - long q; - int charPos = index; - - boolean negative = (i < 0); - if (!negative) { - i = -i; - } - - // Get 2 digits/iteration using longs until quotient fits into an int - while (i <= Integer.MIN_VALUE) { - q = i / 100; - charPos -= 2; - putPair(buf, charPos, (int)((q * 100) - i)); - i = q; - } - - // Get 2 digits/iteration using ints - int q2; - int i2 = (int)i; - while (i2 <= -100) { - q2 = i2 / 100; - charPos -= 2; - putPair(buf, charPos, (q2 * 100) - i2); - i2 = q2; - } - - // We know there are at most two digits left at this point. - if (i2 < -9) { - charPos -= 2; - putPair(buf, charPos, -i2); - } else { - buf[--charPos] = (char) ('0' - i2); - } - - if (negative) { - buf[--charPos] = '-'; - } - return charPos; - } - - /** - * Insert the 2-chars integer into the buf as 2 decimal digit ASCII chars, - * only least significant 16 bits of {@code v} are used. - * @param buf byte buffer to copy into - * @param charPos insert point - * @param v to convert - */ - public static void putPair(char[] buf, int charPos, int v) { - int packed = DIGITS[v]; - buf[charPos ] = (char) (packed & 0xFF); - buf[charPos + 1] = (char) (packed >> 8); - } - /** * Insert the 2-bytes integer into the buf as 2 decimal digit ASCII bytes, * only least significant 16 bits of {@code v} are used. diff --git a/test/micro/org/openjdk/bench/java/math/BigDecimals.java b/test/micro/org/openjdk/bench/java/math/BigDecimals.java index 3c443db013fe3..824b1f348a0f4 100644 --- a/test/micro/org/openjdk/bench/java/math/BigDecimals.java +++ b/test/micro/org/openjdk/bench/java/math/BigDecimals.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, Alibaba Group Holding Limited. 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 @@ -35,6 +36,7 @@ import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; +import java.lang.invoke.*; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Random; @@ -45,7 +47,7 @@ @State(Scope.Thread) @Warmup(iterations = 5, time = 1) @Measurement(iterations = 5, time = 1) -@Fork(value = 3) +@Fork(value = 3, jvmArgs = {"--add-opens", "java.base/java.math=ALL-UNNAMED"}) public class BigDecimals { /** Make sure TEST_SIZE is used to size the arrays. We need this constant to parametrize the operations count. */ @@ -60,7 +62,9 @@ public class BigDecimals { private BigDecimal[] bigDecimals; private String[] stringInputs; private double[] doubleInputs; - private BigDecimal[] hugeArray, largeArray, smallArray; + private BigDecimal[] hugeArray; + private BigDecimal[] large2Array, small2Array; + private BigDecimal[] large3Array, small3Array; @Setup public void setup() { @@ -96,22 +100,28 @@ public void setup() { hugeArray = new BigDecimal[TEST_SIZE]; /* - * Large numbers less than MAX_LONG but larger than MAX_INT - */ - largeArray = new BigDecimal[TEST_SIZE]; + * Large numbers less than MAX_LONG but larger than MAX_INT + */ + large2Array = new BigDecimal[TEST_SIZE]; + large3Array = new BigDecimal[TEST_SIZE]; /* - * Small number less than MAX_INT - */ - smallArray = new BigDecimal[TEST_SIZE]; + * Small number less than MAX_INT + */ + small2Array = new BigDecimal[TEST_SIZE]; + small3Array = new BigDecimal[TEST_SIZE]; dummyStringArray = new String[TEST_SIZE]; for (int i = 0; i < TEST_SIZE; i++) { int value = Math.abs(r.nextInt()); hugeArray[i] = new BigDecimal("" + ((long) value + (long) Integer.MAX_VALUE) + ((long) value + (long) Integer.MAX_VALUE) + ".55"); - largeArray[i] = new BigDecimal("" + ((long) value + (long) Integer.MAX_VALUE) + ".55"); - smallArray[i] = new BigDecimal("" + ((long) value / 1000) + ".55"); + + large2Array[i] = new BigDecimal("" + ((long) value + (long) Integer.MAX_VALUE) + ".55"); + large3Array[i] = new BigDecimal("" + ((long) value + (long) Integer.MAX_VALUE) + ".555"); + + small2Array[i] = new BigDecimal("" + ((long) value / 1000) + ".55"); + small3Array[i] = new BigDecimal("" + ((long) value / 1000) + ".555"); } } @@ -185,46 +195,161 @@ public void testMultiply(Blackhole bh) { bh.consume(tmp); } - /** Test divide with huge/small numbers */ + /** Invokes the compareTo method of BigDecimal with various different values. */ + @Benchmark + @OperationsPerInvocation(TEST_SIZE - 1) + public void testCompareTo(Blackhole bh) { + BigDecimal c = bigDecimals[0]; + for (BigDecimal s : bigDecimals) { + bh.consume(c.compareTo(s)); + } + } + + /** Test BigDecimal.toString() with large numbers (scale 2) less than MAX_LONG but larger than MAX_INT */ @Benchmark - @OperationsPerInvocation(TEST_SIZE * TEST_SIZE) - public void testHugeSmallDivide(Blackhole bh) { + @OperationsPerInvocation(TEST_SIZE) + public void hugeLayoutCharsToString(Blackhole bh) throws Throwable { for (BigDecimal s : hugeArray) { - for (BigDecimal t : smallArray) { - bh.consume(s.divide(t, RoundingMode.DOWN)); - } + bh.consume((String) BigDecimalAccess.layoutChars.invokeExact(s, true)); } } - /** Test divide with large/small numbers */ + /** Test BigDecimal.toEngineeringString() with huge numbers larger than MAX_LONG */ @Benchmark - @OperationsPerInvocation(TEST_SIZE * TEST_SIZE) - public void testLargeSmallDivide(Blackhole bh) { - for (BigDecimal s : largeArray) { - for (BigDecimal t : smallArray) { - bh.consume(s.divide(t, RoundingMode.DOWN)); - } + @OperationsPerInvocation(TEST_SIZE) + public void hugeEngineeringToString(Blackhole bh) { + for (BigDecimal s : hugeArray) { + bh.consume(s.toEngineeringString()); } } - /** Test divide with huge/large numbers */ + /** Test BigDecimal.toEngineeringString() with huge numbers larger than MAX_LONG */ @Benchmark - @OperationsPerInvocation(TEST_SIZE * TEST_SIZE) - public void testHugeLargeDivide(Blackhole bh) { + @OperationsPerInvocation(TEST_SIZE) + public void hugePlainToString(Blackhole bh) { for (BigDecimal s : hugeArray) { - for (BigDecimal t : largeArray) { - bh.consume(s.divide(t, RoundingMode.DOWN)); - } + bh.consume(s.toPlainString()); } } - /** Invokes the compareTo method of BigDecimal with various different values. */ + /** Test BigDecimal.toString() with large numbers (scale 2) less than MAX_LONG but larger than MAX_INT */ @Benchmark - @OperationsPerInvocation(TEST_SIZE - 1) - public void testCompareTo(Blackhole bh) { - BigDecimal c = bigDecimals[0]; - for (BigDecimal s : bigDecimals) { - bh.consume(c.compareTo(s)); + @OperationsPerInvocation(TEST_SIZE) + public void largeScale2LayoutCharsToString(Blackhole bh) throws Throwable { + for (BigDecimal s : large2Array) { + bh.consume((String) BigDecimalAccess.layoutChars.invokeExact(s, true)); + } + } + + /** Test BigDecimal.toEngineeringString() with large numbers (scale 2) less than MAX_LONG but larger than MAX_INT */ + @Benchmark + @OperationsPerInvocation(TEST_SIZE) + public void largeScale2EngineeringToString(Blackhole bh) { + for (BigDecimal s : large2Array) { + bh.consume(s.toEngineeringString()); + } + } + + /** Test BigDecimal.toPlainString() with large numbers (scale 2) less than MAX_LONG but larger than MAX_INT */ + @Benchmark + @OperationsPerInvocation(TEST_SIZE) + public void largeScale2PlainToString(Blackhole bh) { + for (BigDecimal s : large2Array) { + bh.consume(s.toPlainString()); + } + } + + /** Test BigDecimal.toString() with large numbers (scale 3) less than MAX_LONG but larger than MAX_INT */ + @Benchmark + @OperationsPerInvocation(TEST_SIZE) + public void largeScale3LayoutCharsToString(Blackhole bh) throws Throwable { + for (BigDecimal s : large3Array) { + bh.consume((String) BigDecimalAccess.layoutChars.invokeExact(s, true)); + } + } + + /** Test BigDecimal.toEngineeringString() with large numbers (scale 3) less than MAX_LONG but larger than MAX_INT */ + @Benchmark + @OperationsPerInvocation(TEST_SIZE) + public void largeScale3EngineeringToString(Blackhole bh) { + for (BigDecimal s : large3Array) { + bh.consume(s.toEngineeringString()); + } + } + + + /** Test BigDecimal.toPlainString() with large numbers (scale 3) less than MAX_LONG but larger than MAX_INT */ + @Benchmark + @OperationsPerInvocation(TEST_SIZE) + public void largeScale3PlainToString(Blackhole bh) { + for (BigDecimal s : large3Array) { + bh.consume(s.toPlainString()); + } + } + + /** Test BigDecimal.toString() with small numbers (scale 2) less than MAX_INT */ + @Benchmark + @OperationsPerInvocation(TEST_SIZE) + public void smallScale2LayoutCharsToString(Blackhole bh) throws Throwable { + for (BigDecimal s : small2Array) { + bh.consume((String) BigDecimalAccess.layoutChars.invokeExact(s, true)); + } + } + + /** Test BigDecimal.toEngineeringString() with small numbers (scale 2) less than MAX_INT */ + @Benchmark + @OperationsPerInvocation(TEST_SIZE) + public void smallScale2EngineeringToString(Blackhole bh) { + for (BigDecimal s : small2Array) { + bh.consume(s.toEngineeringString()); + } + } + + /** Test BigDecimal.toPlainString() with small numbers (scale 3) less than MAX_INT */ + @Benchmark + @OperationsPerInvocation(TEST_SIZE) + public void smallScale2PlainToString(Blackhole bh) { + for (BigDecimal s : small2Array) { + bh.consume(s.toPlainString()); + } + } + + /** Test BigDecimal.toString() with small numbers (scale 3) less than MAX_INT */ + @Benchmark + @OperationsPerInvocation(TEST_SIZE) + public void smallScale3LayoutCharsToString(Blackhole bh) throws Throwable { + for (BigDecimal s : small3Array) { + bh.consume((String) BigDecimalAccess.layoutChars.invokeExact(s, true)); + } + } + + /** Test BigDecimal.toEngineeringString() with small numbers (scale 3) less than MAX_INT */ + @Benchmark + @OperationsPerInvocation(TEST_SIZE) + public void smallScale3EngineeringToString(Blackhole bh) { + for (BigDecimal s : small3Array) { + bh.consume(s.toEngineeringString()); + } + } + + /** Test BigDecimal.toPlainString() with small numbers (scale 3) less than MAX_INT */ + @Benchmark + @OperationsPerInvocation(TEST_SIZE) + public void smallScale3PlainToString(Blackhole bh) { + for (BigDecimal s : small3Array) { + bh.consume(s.toPlainString()); + } + } + + static class BigDecimalAccess { + final static MethodHandle layoutChars; + static { + try { + MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(BigDecimal.class, MethodHandles.lookup()); + layoutChars = lookup.findVirtual(BigDecimal.class, "layoutChars", MethodType.methodType(String.class, boolean.class)); + } catch (Throwable e) { + throw new AssertionError(e); + } } } }