diff --git a/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/FastIntegerMath.java b/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/FastIntegerMath.java index 93a86277..d9c6607f 100644 --- a/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/FastIntegerMath.java +++ b/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/FastIntegerMath.java @@ -7,10 +7,16 @@ import java.math.BigInteger; import java.util.Iterator; import java.util.Map; +import java.util.Map.Entry; import java.util.NavigableMap; import java.util.TreeMap; class FastIntegerMath { + + private static final int USE_POWER_OF_FIVE_AND_SHIFT_MIN_THRESHOLD = 800; // stated by benchmark results + + private static final int USE_POWER_OF_FIVE_AND_SHIFT_MAX_THRESHOLD = FftMultiplier.FFT_THRESHOLD; + public static final BigInteger FIVE = BigInteger.valueOf(5); final static BigInteger TEN_POW_16 = BigInteger.valueOf(10_000_000_000_000_000L); final static BigInteger FIVE_POW_16 = BigInteger.valueOf(152_587_890_625L); @@ -90,6 +96,22 @@ static NavigableMap createPowersOfTenFloor16Map() { powersOfTen.put(16, TEN_POW_16); return powersOfTen; } + static boolean usePowerOfFiveAndShift(int n) { + return n > USE_POWER_OF_FIVE_AND_SHIFT_MIN_THRESHOLD && n < USE_POWER_OF_FIVE_AND_SHIFT_MAX_THRESHOLD; + } + + static Map createPowersOfFive(Map powersOfTen) { + Map powersOfFive = new TreeMap<>(); + for (Entry entry : powersOfTen.entrySet()) { + int exponent = entry.getKey(); + if (usePowerOfFiveAndShift(exponent)) { + BigInteger powerOfTen = entry.getValue(); + BigInteger powerOfFive = powerOfTen.shiftRight(exponent); + powersOfFive.put(exponent, powerOfFive); + } + } + return powersOfFive; + } public static long estimateNumBits(long numDecimalDigits) { // For the decimal number 10 we need log_2(10) = 3.3219 bits. diff --git a/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/FftMultiplier.java b/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/FftMultiplier.java index 494720d7..59ad32ff 100644 --- a/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/FftMultiplier.java +++ b/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/FftMultiplier.java @@ -35,7 +35,7 @@ class FftMultiplier { * the mag arrays is greater than this threshold, then FFT * multiplication will be used. */ - private static final int FFT_THRESHOLD = 33220; + static final int FFT_THRESHOLD = 33220; /** * This constant limits {@code mag.length} of BigIntegers to the supported * range. diff --git a/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/JavaBigDecimalFromCharArray.java b/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/JavaBigDecimalFromCharArray.java index 2faaf4a6..6c93d131 100644 --- a/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/JavaBigDecimalFromCharArray.java +++ b/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/JavaBigDecimalFromCharArray.java @@ -6,6 +6,7 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.util.Map; import java.util.NavigableMap; import static ch.randelshofer.fastdoubleparser.FastIntegerMath.*; @@ -271,6 +272,7 @@ private BigDecimal valueOfBigDecimalString(char[] str, int integerPartIndex, int int fractionDigitsCount = exponentIndicatorIndex - nonZeroFractionalPartIndex; int integerDigitsCount = decimalPointIndex - integerPartIndex; NavigableMap powersOfTen = null; + Map powersOfFive; // Parse the significand // --------------------- @@ -283,9 +285,10 @@ private BigDecimal valueOfBigDecimalString(char[] str, int integerPartIndex, int if (integerDigitsCount > RECURSION_THRESHOLD) { powersOfTen = createPowersOfTenFloor16Map(); fillPowersOfNFloor16Recursive(powersOfTen, integerPartIndex, decimalPointIndex); - integerPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, integerPartIndex, decimalPointIndex, powersOfTen); + powersOfFive = createPowersOfFive(powersOfTen); + integerPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, integerPartIndex, decimalPointIndex, powersOfTen, powersOfFive); } else { - integerPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, integerPartIndex, decimalPointIndex, null); + integerPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, integerPartIndex, decimalPointIndex, null, null); } } else { integerPart = BigInteger.ZERO; @@ -300,9 +303,10 @@ private BigDecimal valueOfBigDecimalString(char[] str, int integerPartIndex, int powersOfTen = createPowersOfTenFloor16Map(); } fillPowersOfNFloor16Recursive(powersOfTen, decimalPointIndex + 1, exponentIndicatorIndex); - fractionalPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, decimalPointIndex + 1, exponentIndicatorIndex, powersOfTen); + powersOfFive = createPowersOfFive(powersOfTen); + fractionalPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, decimalPointIndex + 1, exponentIndicatorIndex, powersOfTen, powersOfFive); } else { - fractionalPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, decimalPointIndex + 1, exponentIndicatorIndex, null); + fractionalPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, decimalPointIndex + 1, exponentIndicatorIndex, null, null); } // If the integer part is not 0, we combine it with the fraction part. if (integerPart.signum() == 0) { diff --git a/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/JavaBigIntegerFromCharArray.java b/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/JavaBigIntegerFromCharArray.java index c5cab614..e35c91c9 100644 --- a/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/JavaBigIntegerFromCharArray.java +++ b/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/JavaBigIntegerFromCharArray.java @@ -7,6 +7,7 @@ import java.math.BigInteger; import java.util.Map; +import static ch.randelshofer.fastdoubleparser.FastIntegerMath.createPowersOfFive; import static ch.randelshofer.fastdoubleparser.FastIntegerMath.fillPowersOf10Floor16; class JavaBigIntegerFromCharArray extends AbstractBigIntegerParser { @@ -111,7 +112,8 @@ private BigInteger parseManyDecDigits(char[] str, int from, int to, boolean isNe int numDigits = to - from; checkDecBigIntegerBounds(numDigits); Map powersOfTen = fillPowersOf10Floor16(from, to); - BigInteger result = ParseDigitsTaskCharArray.parseDigitsRecursive(str, from, to, powersOfTen); + Map powersOfFive = createPowersOfFive(powersOfTen); + BigInteger result = ParseDigitsTaskCharArray.parseDigitsRecursive(str, from, to, powersOfTen, powersOfFive); return isNegative ? result.negate() : result; } diff --git a/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/ParseDigitsTaskCharArray.java b/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/ParseDigitsTaskCharArray.java index fb6cfc62..73f37ec4 100644 --- a/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/ParseDigitsTaskCharArray.java +++ b/fastdoubleparser-dev/src/main/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/ParseDigitsTaskCharArray.java @@ -66,7 +66,7 @@ static BigInteger parseDigitsIterative(char[] str, int from, int to) { * We achieve better performance by performing multiplications of long bit sequences * in the frequencey domain. */ - static BigInteger parseDigitsRecursive(char[] str, int from, int to, Map powersOfTen) { + static BigInteger parseDigitsRecursive(char[] str, int from, int to, Map powersOfTen, Map powersOfFive) { int numDigits = to - from; // Base case: Short sequences can be parsed iteratively. @@ -76,10 +76,15 @@ static BigInteger parseDigitsRecursive(char[] str, int from, int to, Map + * # JMH version: 1.36 + * # VM version: JDK 20.0.1, OpenJDK 64-Bit Server VM, 20.0.1+9-29 + * # Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz + * + * Benchmark (bits) Mode Cnt Score Error Units + * bigInteger 44000 avgt 4 348.081 ± 8.025 us/op + * bigInteger 44500 avgt 4 350.277 ± 35.341 us/op + * bigInteger 45000 avgt 4 356.652 ± 24.410 us/op + * fft 44000 avgt 4 350.903 ± 5.170 us/op + * fft 44500 avgt 4 344.830 ± 2.741 us/op + * fft 45000 avgt 4 343.596 ± 3.054 us/op + * + * Process finished with exit code 0 + * + */ + +@Fork(value = 1, jvmArgsAppend = { + "-XX:+UnlockExperimentalVMOptions", "--add-modules", "jdk.incubator.vector" + , "--enable-preview" +}) +@Measurement(iterations = 4, time = 1) +@Warmup(iterations = 10, time = 1) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@BenchmarkMode(Mode.AverageTime) +@State(Scope.Benchmark) +public class JmhFftThreshold { + + @Param({"44000", + "44500", // where both variants are equal - FftMultiplier.FFT_THRESHOLD + "45000"}) + public int bits; + private BigInteger a; + private BigInteger b; + private BigInteger bs; + public int zeroes; + + @Setup(Level.Trial) + public void setUp() { + int length = (bits + 7) / 8; + byte[] bytesA = new byte[length]; + byte[] bytesB = new byte[length]; + Random rng = new Random(0); + rng.nextBytes(bytesA); + rng.nextBytes(bytesB); + + // to be positive + bytesA[0] &= ~(1L << 63); + bytesB[0] &= ~(1L << 63); + + // set for b rightmost zeroes like 10^n has + final double lg5 = Math.log(5) / Math.log(2); + zeroes = (int) (length / (lg5 + 1)); + for (int i = 0; i < zeroes; i++) { + bytesB[length - 1 - i] = 0; + } + + a = new BigInteger(1, bytesA); + b = new BigInteger(1, bytesB); + bs = b.shiftRight(zeroes * 8); // preshift value - imitate 5^n + System.out.println(b.getLowestSetBit() + " bits from " + length * 8 + " in total are zero"); + } + + + @Benchmark + public void fft(Blackhole blackhole) { + blackhole.consume(FftMultiplier.multiplyFft(a, b)); + } + + @Benchmark + public void bigInteger(Blackhole blackhole) { + blackhole.consume(a.multiply(bs).shiftLeft(zeroes * 8)); + } +} + + + + + diff --git a/fastdoubleparser-dev/src/test/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/JmhMultiplyByPowerOfFiveAndShiftMultiplier.java b/fastdoubleparser-dev/src/test/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/JmhMultiplyByPowerOfFiveAndShiftMultiplier.java new file mode 100644 index 00000000..824fa2e9 --- /dev/null +++ b/fastdoubleparser-dev/src/test/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/JmhMultiplyByPowerOfFiveAndShiftMultiplier.java @@ -0,0 +1,104 @@ +/* + * @(#)JmhMultiplyByPowerOfFiveAndShiftMultiplier.java + * Copyright © 2023 Werner Randelshofer, Switzerland. MIT License. + */ +package ch.randelshofer.fastdoubleparser; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.math.BigInteger; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +/** + * Benchmarks for selected floating point strings. + *
+ * # JMH version: 1.36
+ * # VM version: JDK 20.0.1, OpenJDK 64-Bit Server VM, 20.0.1+9-29
+ * # Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
+ *
+ * Benchmark  (bits)  Mode  Cnt       Score      Error  Units
+ * optimized     700  avgt    4     250.376 ±    3.773  ns/op
+ * optimized     800  avgt    4     317.921 ±   30.518  ns/op
+ * optimized     900  avgt    4     405.584 ±    2.953  ns/op
+ * optimized    2000  avgt    4    1371.457 ±   15.372  ns/op
+ * optimized   10000  avgt    4   30015.376 ± 1073.798  ns/op
+ * optimized   30000  avgt    4  180735.583 ±  825.962  ns/op
+ * original      700  avgt    4     254.843 ±    1.414  ns/op
+ * original      800  avgt    4     339.564 ±    2.898  ns/op
+ * original      900  avgt    4     463.033 ±    4.538  ns/op
+ * original     2000  avgt    4    1828.848 ±  121.480  ns/op
+ * original    10000  avgt    4   33663.669 ± 3189.014  ns/op
+ * original    30000  avgt    4  204170.758 ± 3690.179  ns/op
+ * 
+ */ + +@Fork(value = 1, jvmArgsAppend = { + "-XX:+UnlockExperimentalVMOptions", "--add-modules", "jdk.incubator.vector" + , "--enable-preview" +}) +@Measurement(iterations = 4, time = 1) +@Warmup(iterations = 4, time = 1) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +@State(Scope.Benchmark) +public class JmhMultiplyByPowerOfFiveAndShiftMultiplier { + + + @Param({ + "700" // above this value, variant with bit shift leads + , "800" + , "900" + , "2000" + , "10000" + , "30000" + }) + public int bits; + private BigInteger a; + private BigInteger b; + private BigInteger bs; + public int zeroes; + + @Setup(Level.Trial) + public void setUp() { + int length = (bits + 7) / 8; + byte[] bytesA = new byte[length]; + byte[] bytesB = new byte[length]; + Random rng = new Random(0); + rng.nextBytes(bytesA); + rng.nextBytes(bytesB); + + // to be positive + bytesA[0] &= ~(1L << 63); + bytesB[0] &= ~(1L << 63); + + // set for b rightmost zeroes like 10^n has + final double lg5 = Math.log(5) / Math.log(2); + zeroes = (int) (length / (lg5 + 1)); + for (int i = 0; i < zeroes; i++) { + bytesB[length - 1 - i] = 0; + } + + a = new BigInteger(1, bytesA); + b = new BigInteger(1, bytesB); + bs = b.shiftRight(zeroes * 8); // preshift value - imitate 5^n + System.out.println(b.getLowestSetBit() + " bits from " + length * 8 + " in total are zero"); + } + + + @Benchmark + public void original(Blackhole blackhole) { + blackhole.consume(FftMultiplier.multiply(a, b)); + } + + @Benchmark + public void optimized(Blackhole blackhole) { + blackhole.consume(FftMultiplier.multiply(a, bs).shiftLeft(zeroes * 8)); + } +} + + + + + diff --git a/fastdoubleparser-dev/src/test/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/JmhParseIterativeThreshold.java b/fastdoubleparser-dev/src/test/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/JmhParseIterativeThreshold.java new file mode 100644 index 00000000..bec070f0 --- /dev/null +++ b/fastdoubleparser-dev/src/test/java/ch.randelshofer.fastdoubleparser/ch/randelshofer/fastdoubleparser/JmhParseIterativeThreshold.java @@ -0,0 +1,59 @@ +/* + * @(#)JmhParseIterativeThreshold.java + * Copyright © 2023 Werner Randelshofer, Switzerland. MIT License. + */ +package ch.randelshofer.fastdoubleparser; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.concurrent.TimeUnit; + +/** + * Benchmarks for selected floating point strings. + *
+ * # JMH version: 1.36
+ * # VM version: JDK 20.0.1, OpenJDK 64-Bit Server VM, 20.0.1+9-29
+ * # Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
+ *
+ * Benchmark   (digits)  Mode  Cnt   Score   Error  Units
+ * bigInteger       300  avgt   10  23.413 ± 0.936  us/op
+ * bigInteger       325  avgt   10  23.268 ± 0.153  us/op
+ * bigInteger       350  avgt   10  23.266 ± 0.222  us/op
+ * bigInteger       375  avgt   10  23.162 ± 0.099  us/op
+ * bigInteger       400  avgt   10  23.069 ± 0.128  us/op
+ * bigInteger       425  avgt   10  23.180 ± 0.148  us/op
+ * bigInteger       450  avgt   10  23.049 ± 0.123  us/op
+ * bigInteger       475  avgt   10  23.218 ± 0.184  us/op
+ * bigInteger       500  avgt   10  24.597 ± 0.097  us/op
+ *
+ * Process finished with exit code 0
+ * 
+ */ + +@Fork(value = 1, jvmArgsAppend = { + "-XX:+UnlockExperimentalVMOptions", "--add-modules", "jdk.incubator.vector" + , "--enable-preview" +}) +@Warmup(iterations = 4, time = 1) +@Measurement(iterations = 10, time = 1) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@BenchmarkMode(Mode.AverageTime) +@State(Scope.Benchmark) +public class JmhParseIterativeThreshold { + + @Param({"300", "325", "350", "375", "400", "425", "450", "475", "500"}) + public int digits; + public String s; + + @Setup(Level.Trial) + public void setUp() { + s = Strings.repeat("3", 1000); + ParseDigitsTaskCharSequence.RECURSION_THRESHOLD = digits; + } + + @Benchmark + public void bigInteger(Blackhole blackhole) { + blackhole.consume(JavaBigIntegerParser.parseBigInteger(s)); + } +} \ No newline at end of file