This is an automated email from the ASF dual-hosted git repository. mattjuntunen pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-geometry.git
The following commit(s) were added to refs/heads/master by this push: new b9ff14e improvements to DoubleFormats performance b9ff14e is described below commit b9ff14e03b06f5b4ca1881ec905d64926ba09f21 Author: Matt Juntunen <mattjuntu...@apache.org> AuthorDate: Fri Jun 11 22:08:41 2021 -0400 improvements to DoubleFormats performance --- commons-geometry-examples/examples-jmh/pom.xml | 5 + .../geometry/examples/jmh/BenchmarkUtils.java | 43 ++++- .../AffineTransformMatrixPerformance.java | 6 +- .../jmh/io/core/DoubleFormatsPerformance.java | 183 +++++++++++++++++++++ .../examples/jmh/io/core/package-info.java | 23 +++ .../geometry/io/core/utils/ParsedDouble.java | 137 ++++++++------- 6 files changed, 323 insertions(+), 74 deletions(-) diff --git a/commons-geometry-examples/examples-jmh/pom.xml b/commons-geometry-examples/examples-jmh/pom.xml index 0165312..7527390 100644 --- a/commons-geometry-examples/examples-jmh/pom.xml +++ b/commons-geometry-examples/examples-jmh/pom.xml @@ -58,6 +58,11 @@ <dependency> <groupId>org.apache.commons</groupId> + <artifactId>commons-geometry-io-core</artifactId> + </dependency> + + <dependency> + <groupId>org.apache.commons</groupId> <artifactId>commons-rng-simple</artifactId> </dependency> diff --git a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/BenchmarkUtils.java b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/BenchmarkUtils.java index a6e7583..526a1f8 100644 --- a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/BenchmarkUtils.java +++ b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/BenchmarkUtils.java @@ -22,22 +22,36 @@ import org.apache.commons.rng.UniformRandomProvider; */ public final class BenchmarkUtils { + /** Default min exponent for random double values. */ + public static final int DEFAULT_MIN_EXP = -64; + + /** Default max exponent for random double values. */ + public static final int DEFAULT_MAX_EXP = 64; + /** Utility class; no instantiation. */ private BenchmarkUtils() {} - /** Creates a random double number with a random sign and mantissa and a large range for - * the exponent. The numbers will not be uniform over the range. + /** Creates a random double number with a random sign and mantissa and a large, default + * range for the exponent. The numbers will not be uniform over the range. * @param rng random number generator * @return the random number */ public static double randomDouble(final UniformRandomProvider rng) { + return randomDouble(DEFAULT_MIN_EXP, DEFAULT_MAX_EXP, rng); + } + + /** Create a random double value with exponent in the range {@code [minExp, maxExp]}. + * @param minExp minimum exponent; must be less than {@code maxExp} + * @param maxExp maximum exponent; must be greater than {@code minExp} + * @param rng random number generator + * @return random double + */ + public static double randomDouble(final int minExp, final int maxExp, final UniformRandomProvider rng) { // Create random doubles using random bits in the sign bit and the mantissa. - // Then create an exponent in the range -64 to 64. Thus the sum product - // of 4 max or min values will not over or underflow. final long mask = ((1L << 52) - 1) | 1L << 63; final long bits = rng.nextLong() & mask; // The exponent must be unsigned so + 1023 to the signed exponent - final long exp = rng.nextInt(129) - 64 + 1023; + final long exp = rng.nextInt(maxExp - minExp + 1) + minExp + 1023; return Double.longBitsToDouble(bits | (exp << 52)); } @@ -46,13 +60,24 @@ public final class BenchmarkUtils { * @param len array length * @return array containing {@code len} random doubles */ - public static double[] randomDoubleArray(final UniformRandomProvider rng, final int len) { - final double[] arr = new double[len]; + public static double[] randomDoubleArray(final int len, final UniformRandomProvider rng) { + return randomDoubleArray(len, DEFAULT_MIN_EXP, DEFAULT_MAX_EXP, rng); + } + /** Create an array with the given length containing random doubles with exponents in the range + * {@code [minExp, maxExp]}. + * @param len array length + * @param minExp minimum exponent; must be less than {@code maxExp} + * @param maxExp maximum exponent; must be greater than {@code minExp} + * @param rng random number generator + * @return array of random doubles + */ + public static double[] randomDoubleArray(final int len, final int minExp, final int maxExp, + final UniformRandomProvider rng) { + final double[] arr = new double[len]; for (int i = 0; i < arr.length; ++i) { - arr[i] = randomDouble(rng); + arr[i] = randomDouble(minExp, maxExp, rng); } - return arr; } } diff --git a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/AffineTransformMatrixPerformance.java b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/AffineTransformMatrixPerformance.java index f05d321..d437f4b 100644 --- a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/AffineTransformMatrixPerformance.java +++ b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/AffineTransformMatrixPerformance.java @@ -111,7 +111,7 @@ public class AffineTransformMatrixPerformance { public void setup() { final UniformRandomProvider rand = RandomSource.create(RandomSource.XO_RO_SHI_RO_128_PP); - transform = AffineTransformMatrix1D.of(BenchmarkUtils.randomDoubleArray(rand, 2)); + transform = AffineTransformMatrix1D.of(BenchmarkUtils.randomDoubleArray(2, rand)); } } @@ -135,7 +135,7 @@ public class AffineTransformMatrixPerformance { public void setup() { final UniformRandomProvider rand = RandomSource.create(RandomSource.XO_RO_SHI_RO_128_PP); - transform = AffineTransformMatrix2D.of(BenchmarkUtils.randomDoubleArray(rand, 6)); + transform = AffineTransformMatrix2D.of(BenchmarkUtils.randomDoubleArray(6, rand)); } } @@ -159,7 +159,7 @@ public class AffineTransformMatrixPerformance { public void setup() { final UniformRandomProvider rand = RandomSource.create(RandomSource.XO_RO_SHI_RO_128_PP); - transform = AffineTransformMatrix3D.of(BenchmarkUtils.randomDoubleArray(rand, 12)); + transform = AffineTransformMatrix3D.of(BenchmarkUtils.randomDoubleArray(12, rand)); } } diff --git a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/io/core/DoubleFormatsPerformance.java b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/io/core/DoubleFormatsPerformance.java new file mode 100644 index 0000000..dc83014 --- /dev/null +++ b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/io/core/DoubleFormatsPerformance.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.geometry.examples.jmh.io.core; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.util.concurrent.TimeUnit; +import java.util.function.DoubleFunction; + +import org.apache.commons.geometry.examples.jmh.BenchmarkUtils; +import org.apache.commons.geometry.io.core.utils.DoubleFormats; +import org.apache.commons.rng.simple.RandomSource; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** Benchmarks for the {@link DoubleFormats} class. + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1, jvmArgs = {"-server", "-Xms512M", "-Xmx512M"}) +public class DoubleFormatsPerformance { + + /** Benchmark input providing a source of random double values. */ + @State(Scope.Thread) + public static class DoubleInput { + + /** The number of doubles in the input array. */ + @Param({"10000"}) + private int size; + + /** Minimum base 2 exponent for random input doubles. */ + @Param("-20") + private int minExp; + + /** Maximum base 2 exponent for random input doubles. */ + @Param("20") + private int maxExp; + + /** Double input array. */ + private double[] input; + + /** Get the input doubles. + * @return the input doubles + */ + public double[] getInput() { + return input; + } + + /** Set up the instance for the benchmark. */ + @Setup(Level.Iteration) + public void setup() { + input = BenchmarkUtils.randomDoubleArray(size, minExp, maxExp, + RandomSource.create(RandomSource.XO_RO_SHI_RO_128_PP)); + } + } + + /** Run a benchmark test on a function accepting a double argument. + * @param <T> function output type + * @param input double array + * @param bh jmh blackhole for consuming output + * @param fn function to call + */ + private static <T> void runDoubleFunction(final DoubleInput input, final Blackhole bh, + final DoubleFunction<T> fn) { + for (final double d : input.getInput()) { + bh.consume(fn.apply(d)); + } + } + + /** Benchmark testing just the overhead of the benchmark harness. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void baseline(final DoubleInput input, final Blackhole bh) { + runDoubleFunction(input, bh, d -> ""); + } + + /** Benchmark testing the {@link Double#toString()} method. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void doubleToString(final DoubleInput input, final Blackhole bh) { + runDoubleFunction(input, bh, Double::toString); + } + + /** Benchmark testing the {@link String#format(String, Object...)} method. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void stringFormat(final DoubleInput input, final Blackhole bh) { + runDoubleFunction(input, bh, d -> String.format("%d", d)); + } + + /** Benchmark testing the BigDecimal formatting performance. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void bigDecimal(final DoubleInput input, final Blackhole bh) { + final DoubleFunction<String> fn = d -> BigDecimal.valueOf(d) + .setScale(3, RoundingMode.HALF_EVEN) + .stripTrailingZeros() + .toString(); + runDoubleFunction(input, bh, fn); + } + + /** Benchmark testing the {@link DecimalFormat} class. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void decimalFormat(final DoubleInput input, final Blackhole bh) { + final DecimalFormat fmt = new DecimalFormat("0.###"); + runDoubleFunction(input, bh, fmt::format); + } + + /** Benchmark testing the {@link DoubleFormats#createDefault(int, int)} method. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void doubleFormatsDefault(final DoubleInput input, final Blackhole bh) { + runDoubleFunction(input, bh, DoubleFormats.createDefault(0, -3)); + } + + /** Benchmark testing the {@link DoubleFormats#createPlain(int, int)} method. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void doubleFormatsPlain(final DoubleInput input, final Blackhole bh) { + runDoubleFunction(input, bh, DoubleFormats.createPlain(0, -3)); + } + + /** Benchmark testing the {@link DoubleFormats#createScientific(int, int)} method. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void doubleFormatsScientific(final DoubleInput input, final Blackhole bh) { + runDoubleFunction(input, bh, DoubleFormats.createScientific(0, -3)); + } + + /** Benchmark testing the {@link DoubleFormats#createEngineering(int, int)} method. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void doubleFormatsEngineering(final DoubleInput input, final Blackhole bh) { + runDoubleFunction(input, bh, DoubleFormats.createEngineering(0, -3)); + } +} diff --git a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/io/core/package-info.java b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/io/core/package-info.java new file mode 100644 index 0000000..6f2beb2 --- /dev/null +++ b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/io/core/package-info.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Benchmarks for the components in the {@code org.apache.commons.geometry.io.core} + * package. + */ + +package org.apache.commons.geometry.examples.jmh.io.core; diff --git a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/utils/ParsedDouble.java b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/utils/ParsedDouble.java index c97ece0..d6d4057 100644 --- a/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/utils/ParsedDouble.java +++ b/commons-geometry-io-core/src/main/java/org/apache/commons/geometry/io/core/utils/ParsedDouble.java @@ -39,6 +39,9 @@ final class ParsedDouble { /** Minus sign character. */ private static final char MINUS_CHAR = '-'; + /** Plus sign character. */ + private static final char PLUS_CHAR = '+'; + /** Decimal separator character. */ private static final char DECIMAL_SEP_CHAR = '.'; @@ -54,6 +57,9 @@ final class ParsedDouble { /** String containing the decimal digits '0' - '9' in sequence. */ private static final String DECIMAL_DIGITS = "0123456789"; + /** Initial size to use for string builder instances. */ + private static final int INITIAL_STR_BUILDER_SIZE = 32; + /** Shared instance representing the positive zero double value. */ private static final ParsedDouble POS_ZERO = new ParsedDouble(false, String.valueOf(ZERO_CHAR), 0); @@ -172,11 +178,9 @@ final class ParsedDouble { final int currentPrecision = getPrecision(); if (currentPrecision > precision) { // we need to round to reduce the number of digits - String resultDigits = digits.substring(0, precision); - - if (shouldRoundUp(precision)) { - resultDigits = addOne(resultDigits); - } + String resultDigits = shouldRoundUp(precision) ? + addOne(digits, precision) : + digits.substring(0, precision); // compute the initial result exponent int resultExponent = exponent + (currentPrecision - precision); @@ -209,7 +213,7 @@ final class ParsedDouble { public String toPlainString(final boolean includeDecimalPlaceholder) { final int precision = getPrecision(); - final StringBuilder sb = new StringBuilder(); + final StringBuilder sb = new StringBuilder(INITIAL_STR_BUILDER_SIZE); if (negative) { sb.append(MINUS_CHAR); } @@ -303,7 +307,7 @@ final class ParsedDouble { private String toScientificString(final int wholeDigits, final boolean includeDecimalPlaceholder) { final int precision = getPrecision(); - final StringBuilder sb = new StringBuilder(); + final StringBuilder sb = new StringBuilder(INITIAL_STR_BUILDER_SIZE); if (negative) { sb.append(MINUS_CHAR); } @@ -367,43 +371,54 @@ final class ParsedDouble { } final String str = Double.toString(d); + final char[] strChars = str.toCharArray(); // extract the different portions of the string representation // (since double is finite, str is guaranteed to not be empty and to contain a // single decimal point according to the Double.toString() API) - final boolean negative = str.charAt(0) == MINUS_CHAR; + final boolean negative = strChars[0] == MINUS_CHAR; final int digitStartIdx = negative ? 1 : 0; - final StringBuilder digitStr = new StringBuilder(str.length()); + final char[] digitChars = new char[strChars.length]; int decimalSepIdx = -1; int exponentIdx = -1; + int digitCount = 0; + int firstNonZeroDigitIdx = -1; + int lastNonZeroDigitIdx = -1; - char ch; - for (int i = digitStartIdx; i < str.length(); ++i) { - ch = str.charAt(i); + for (int i = digitStartIdx; i < strChars.length; ++i) { + final char ch = strChars[i]; if (ch == DECIMAL_SEP_CHAR) { decimalSepIdx = i; } else if (ch == EXPONENT_CHAR) { exponentIdx = i; } else if (exponentIdx < 0) { - digitStr.append(ch); + // this is a significand digit + if (ch != ZERO_CHAR) { + if (firstNonZeroDigitIdx < 0) { + firstNonZeroDigitIdx = digitCount; + } + lastNonZeroDigitIdx = digitCount; + } + + digitChars[digitCount++] = ch; } } - final int firstNonZeroIdx = findFirstNonZero(digitStr); - if (firstNonZeroIdx > -1) { - final int lastNonZeroIdx = findLastNonZero(digitStr); - + if (firstNonZeroDigitIdx > -1) { // determine the exponent final int explicitExponent = exponentIdx > -1 ? - Integer.parseInt(str.substring(exponentIdx + 1)) : + parseExponent(str, exponentIdx + 1) : 0; - final int exponent = explicitExponent + decimalSepIdx - digitStartIdx - lastNonZeroIdx - 1; + final int exponent = explicitExponent + decimalSepIdx - digitStartIdx - lastNonZeroDigitIdx - 1; // get the digit string without any leading or trailing zeros - final String digits = digitStr.substring(firstNonZeroIdx, lastNonZeroIdx + 1); + final String digits = String.valueOf( + digitChars, + firstNonZeroDigitIdx, + lastNonZeroDigitIdx - firstNonZeroDigitIdx + 1); return new ParsedDouble(negative, digits, exponent); } @@ -414,21 +429,27 @@ final class ParsedDouble { POS_ZERO; } - /** Return the index of the first character in the argument not equal - * to {@code '0'} or {@code -1} if no such character can be found. - * @param seq sequence to search - * @return the index of the first non-zero character or {@code -1} if not found + /** Parse a double exponent value from {@code seq}, starting at the {@code start} + * index and continuing through the end of the sequence. + * @param seq sequence to part a double exponent value from + * @param start start index + * @return parsed exponent value */ - private static int findFirstNonZero(final CharSequence seq) { - char ch; - for (int i = 0; i < seq.length(); ++i) { - ch = seq.charAt(i); - if (ch != ZERO_CHAR) { - return i; + private static int parseExponent(final CharSequence seq, final int start) { + int exp = 0; + boolean neg = false; + + final int len = seq.length(); + for (int i = start; i < len; ++i) { + final char ch = seq.charAt(i); + if (ch == MINUS_CHAR) { + neg = !neg; + } else if (ch != PLUS_CHAR) { + exp = (exp * 10) + digitValue(ch); } } - return -1; + return neg ? -exp : exp; } /** Return the index of the last character in the argument not equal @@ -458,44 +479,36 @@ final class ParsedDouble { return ch - ZERO_CHAR; } - /** Add one to the value of the integer represented by the given string, returning - * the result as another string. The input is assumed to contain only digit characters + /** Add one to the value of the integer represented by the substring of length {@code len} + * starting at index {@code 0}, returning the result as another string. The input is assumed + * to contain only digit characters * (i.e. '0' - '9'). No validation is performed. * @param digitStr string containing a representation of an integer + * @param len number of characters to use from {@code str} * @return string representation of the result of adding 1 to the integer represented - * by the input + * by the input substring */ - private static String addOne(final String digitStr) { - final char[] digitChars = digitStr.toCharArray(); - if (addOne(digitChars)) { - return new StringBuilder() - .append(ONE_CHAR) - .append(digitChars) - .toString(); + private static String addOne(final String digitStr, final int len) { + final char[] resultChars = new char[len + 1]; + + boolean carrying = true; + for (int i = len - 1; i >= 0; --i) { + final char inChar = digitStr.charAt(i); + final char outChar = carrying ? + DECIMAL_DIGITS.charAt((digitValue(inChar) + 1) % DECIMAL_DIGITS.length()) : + inChar; + resultChars[i + 1] = outChar; + + if (carrying && outChar != ZERO_CHAR) { + carrying = false; + } } - return String.valueOf(digitChars); - } - - /** Add one to the integer value represented by the given sequence of digit - * characters (i.e. '0' - '9'). The characters are modified in place. True is - * returned if the operation resulted is a carry-out of 1. False is returned if - * the result is fully contained in the passed array. - * @param digitChars sequence of digit characters - * @return true if a 1 was carried out of the operation; otherwise false - */ - private static boolean addOne(final char[] digitChars) { - int i; - char c; - for (i = digitChars.length - 1; i >= 0; --i) { - c = DECIMAL_DIGITS.charAt((digitValue(digitChars[i]) + 1) % DECIMAL_DIGITS.length()); - digitChars[i] = c; - - if (c != ZERO_CHAR) { - break; // no carry over; stop - } + if (carrying) { + resultChars[0] = ONE_CHAR; + return String.valueOf(resultChars); } - return i < 0; + return String.valueOf(resultChars, 1, len); } }