From f9250ff57fb37cd011d30651193fc5c52fbd364a Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Thu, 31 Mar 2022 21:17:08 +0200 Subject: [PATCH] init work on inlining fastdoubleparser add one doubleparser test case (as a start) uptake latest code update float parse don't parse floats as doubles first add feature to control double parser impl refactor params remove some deprecations (will be hard to change all calls in jackson-databind to use new methods) add fast-double-parser tests add fast float support Update NumberInput.java update tests Create ReaderTest.java remove char array support add JsonReadFeature.USE_FAST_DOUBLE_PARSER Update FloatParsingTest.java float support Update TextBuffer.java add tests Delete AbstractFloatValueFromCharArray.java latest code from fastdoubleparser update tests latest Werner Randelshofer code update licenses Update package-info.java Update package-info.java --- .../fasterxml/jackson/core/JsonParser.java | 11 +++ .../jackson/core/base/ParserBase.java | 6 +- .../jackson/core/io/NumberInput.java | 72 ++++++++++++++++--- .../jackson/core/json/JsonReadFeature.java | 11 +++ .../jackson/core/util/TextBuffer.java | 36 +++++++++- .../jackson/core/io/TestNumberInput.java | 7 ++ ...astParserNonStandardNumberParsingTest.java | 17 +++++ .../read/FastParserNumberParsingTest.java | 14 ++++ .../jackson/core/read/FloatParsingTest.java | 27 +++++-- .../read/NonStandardNumberParsingTest.java | 12 ++-- .../jackson/core/read/NumberParsingTest.java | 21 +++--- 11 files changed, 202 insertions(+), 32 deletions(-) create mode 100644 src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java create mode 100644 src/test/java/com/fasterxml/jackson/core/read/FastParserNumberParsingTest.java diff --git a/src/main/java/com/fasterxml/jackson/core/JsonParser.java b/src/main/java/com/fasterxml/jackson/core/JsonParser.java index 335fd434e7..fc76afb59b 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonParser.java @@ -325,6 +325,17 @@ public enum Feature { */ INCLUDE_SOURCE_IN_LOCATION(true), + /** + * Feature that determines whether we use the built-in {@link Double#parseDouble(String)} code to parse + * doubles or if we use {@link com.fasterxml.jackson.core.io.doubleparser} + * instead. + *

+ * This setting is disabled by default. + * + * @since 2.14 + */ + USE_FAST_DOUBLE_PARSER(false), + ; /** diff --git a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java index f744b97b2e..62c1e27e6c 100644 --- a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java +++ b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java @@ -893,11 +893,11 @@ private void _parseSlowFloat(int expType) throws IOException _numberBigDecimal = _textBuffer.contentsAsDecimal(); _numTypesValid = NR_BIGDECIMAL; } else if (expType == NR_FLOAT) { - _numberFloat = _textBuffer.contentsAsFloat(); + _numberFloat = _textBuffer.contentsAsFloat(isEnabled(Feature.USE_FAST_DOUBLE_PARSER)); _numTypesValid = NR_FLOAT; } else { // Otherwise double has to do - _numberDouble = _textBuffer.contentsAsDouble(); + _numberDouble = _textBuffer.contentsAsDouble(isEnabled(Feature.USE_FAST_DOUBLE_PARSER)); _numTypesValid = NR_DOUBLE; } } catch (NumberFormatException nex) { @@ -927,7 +927,7 @@ private void _parseSlowInt(int expType) throws IOException _reportTooLongIntegral(expType, numStr); } if ((expType == NR_DOUBLE) || (expType == NR_FLOAT)) { - _numberDouble = NumberInput.parseDouble(numStr); + _numberDouble = NumberInput.parseDouble(numStr, isEnabled(Feature.USE_FAST_DOUBLE_PARSER)); _numTypesValid = NR_DOUBLE; } else { // nope, need the heavy guns... (rare case) diff --git a/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java b/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java index 7ed502937f..754b4bf8f5 100644 --- a/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java +++ b/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java @@ -1,5 +1,8 @@ package com.fasterxml.jackson.core.io; +import com.fasterxml.jackson.core.io.doubleparser.FastDoubleParser; +import com.fasterxml.jackson.core.io.doubleparser.FastFloatParser; + import java.math.BigDecimal; public final class NumberInput @@ -239,7 +242,9 @@ public static int parseAsInt(String s, int def) // if other symbols, parse as Double, coerce if (c > '9' || c < '0') { try { - return (int) parseDouble(s); + //useFastParser=true is used because there is a lot less risk that small changes in result will have an affect + //and performance benefit is useful + return (int) parseDouble(s, true); } catch (NumberFormatException e) { return def; } @@ -276,7 +281,9 @@ public static long parseAsLong(String s, long def) // if other symbols, parse as Double, coerce if (c > '9' || c < '0') { try { - return (long) parseDouble(s); + //useFastParser=true is used because there is a lot less risk that small changes in result will have an affect + //and performance benefit is useful + return (long) parseDouble(s, true); } catch (NumberFormatException e) { return def; } @@ -287,8 +294,26 @@ public static long parseAsLong(String s, long def) } catch (NumberFormatException e) { } return def; } - - public static double parseAsDouble(String s, double def) + + /** + * @param s a string representing a number to parse + * @param def the default to return if `s` is not a parseable number + * @return closest matching double (or `def` if there is an issue with `s`) where useFastParser=false + * @see #parseAsDouble(String, double, boolean) + */ + public static double parseAsDouble(final String s, final double def) + { + return parseAsDouble(s, def, false); + } + + /** + * @param s a string representing a number to parse + * @param def the default to return if `s` is not a parseable number + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} + * @return closest matching double (or `def` if there is an issue with `s`) + * @since 2.14 + */ + public static double parseAsDouble(String s, final double def, final boolean useFastParser) { if (s == null) { return def; } s = s.trim(); @@ -297,23 +322,52 @@ public static double parseAsDouble(String s, double def) return def; } try { - return parseDouble(s); + return parseDouble(s, useFastParser); } catch (NumberFormatException e) { } return def; } - public static double parseDouble(String s) throws NumberFormatException { - return Double.parseDouble(s); + /** + * @param s a string representing a number to parse + * @return closest matching double + * @throws NumberFormatException if string cannot be represented by a double where useFastParser=false + * @see #parseDouble(String, boolean) + */ + public static double parseDouble(final String s) throws NumberFormatException { + return parseDouble(s, false); + } + + /** + * @param s a string representing a number to parse + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} + * @return closest matching double + * @throws NumberFormatException if string cannot be represented by a double + * @since v2.14 + */ + public static double parseDouble(final String s, final boolean useFastParser) throws NumberFormatException { + return useFastParser ? FastDoubleParser.parseDouble(s) : Double.parseDouble(s); + } + + /** + * @param s a string representing a number to parse + * @return closest matching float + * @throws NumberFormatException if string cannot be represented by a float where useFastParser=false + * @see #parseFloat(String, boolean) + * @since v2.14 + */ + public static float parseFloat(final String s) throws NumberFormatException { + return parseFloat(s, false); } /** * @param s a string representing a number to parse + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} * @return closest matching float * @throws NumberFormatException if string cannot be represented by a float * @since v2.14 */ - public static float parseFloat(String s) throws NumberFormatException { - return Float.parseFloat(s); + public static float parseFloat(final String s, final boolean useFastParser) throws NumberFormatException { + return useFastParser ? FastFloatParser.parseFloat(s) : Float.parseFloat(s); } public static BigDecimal parseBigDecimal(String s) throws NumberFormatException { diff --git a/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java b/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java index 304b320a12..e02291ce10 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java +++ b/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java @@ -182,6 +182,17 @@ public enum JsonReadFeature */ @SuppressWarnings("deprecation") ALLOW_TRAILING_COMMA(false, JsonParser.Feature.ALLOW_TRAILING_COMMA), + + /** + * Feature that determines whether we use the built-in {@link Double#parseDouble(String)} code to parse + * doubles or if we use {@link com.fasterxml.jackson.core.io.doubleparser} + * instead. + *

+ * This setting is disabled by default. + * + * @since 2.14 + */ + USE_FAST_DOUBLE_PARSER(false, JsonParser.Feature.USE_FAST_DOUBLE_PARSER) ; final private boolean _defaultState; diff --git a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java index 2d98a05073..c1b1a9c17a 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java +++ b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java @@ -512,9 +512,25 @@ public BigDecimal contentsAsDecimal() throws NumberFormatException * @return Buffered text value parsed as a {@link Double}, if possible * * @throws NumberFormatException if contents are not a valid Java number + * @deprecated use {@link #contentsAsDouble(boolean)} */ + @Deprecated public double contentsAsDouble() throws NumberFormatException { - return NumberInput.parseDouble(contentsAsString()); + return contentsAsDouble(false); + } + + /** + * Convenience method for converting contents of the buffer + * into a Double value. + * + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} + * @return Buffered text value parsed as a {@link Double}, if possible + * + * @throws NumberFormatException if contents are not a valid Java number + * @since 2.14 + */ + public double contentsAsDouble(final boolean useFastParser) throws NumberFormatException { + return NumberInput.parseDouble(contentsAsString(), useFastParser); } /** @@ -525,9 +541,25 @@ public double contentsAsDouble() throws NumberFormatException { * * @throws NumberFormatException if contents are not a valid Java number * @since 2.14 + * @deprecated use {@link #contentsAsFloat(boolean)} */ + @Deprecated public float contentsAsFloat() throws NumberFormatException { - return NumberInput.parseFloat(contentsAsString()); + return contentsAsFloat(false); + } + + /** + * Convenience method for converting contents of the buffer + * into a Float value. + * + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} + * @return Buffered text value parsed as a {@link Float}, if possible + * + * @throws NumberFormatException if contents are not a valid Java number + * @since 2.14 + */ + public float contentsAsFloat(final boolean useFastParser) throws NumberFormatException { + return NumberInput.parseFloat(contentsAsString(), useFastParser); } /** diff --git a/src/test/java/com/fasterxml/jackson/core/io/TestNumberInput.java b/src/test/java/com/fasterxml/jackson/core/io/TestNumberInput.java index 1035ac6c8d..bf1f80c247 100644 --- a/src/test/java/com/fasterxml/jackson/core/io/TestNumberInput.java +++ b/src/test/java/com/fasterxml/jackson/core/io/TestNumberInput.java @@ -9,13 +9,20 @@ public void testNastySmallDouble() //prior to jackson v2.14, this value used to be returned as Double.MIN_VALUE final String nastySmallDouble = "2.2250738585072012e-308"; assertEquals(Double.parseDouble(nastySmallDouble), NumberInput.parseDouble(nastySmallDouble)); + assertEquals(Double.parseDouble(nastySmallDouble), NumberInput.parseDouble(nastySmallDouble, true)); } public void testParseFloat() { final String exampleFloat = "1.199999988079071"; assertEquals(1.1999999f, NumberInput.parseFloat(exampleFloat)); + assertEquals(1.1999999f, NumberInput.parseFloat(exampleFloat, true)); assertEquals(1.2f, (float)NumberInput.parseDouble(exampleFloat)); + assertEquals(1.2f, (float)NumberInput.parseDouble(exampleFloat, true)); + + final String exampleFloat2 = "7.006492321624086e-46"; + assertEquals("1.4E-45", Float.toString(NumberInput.parseFloat(exampleFloat2))); + assertEquals("1.4E-45", Float.toString(NumberInput.parseFloat(exampleFloat2, true))); } } diff --git a/src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java new file mode 100644 index 0000000000..35034ea344 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java @@ -0,0 +1,17 @@ +package com.fasterxml.jackson.core.read; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.json.JsonReadFeature; + +public class FastParserNonStandardNumberParsingTest extends NonStandardNumberParsingTest { + private final JsonFactory fastFactory = + JsonFactory.builder() + .enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS) + .enable(JsonReadFeature.USE_FAST_DOUBLE_PARSER) + .build(); + + @Override + protected JsonFactory jsonFactory() { + return fastFactory; + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/read/FastParserNumberParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/FastParserNumberParsingTest.java new file mode 100644 index 0000000000..23260459b4 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/read/FastParserNumberParsingTest.java @@ -0,0 +1,14 @@ +package com.fasterxml.jackson.core.read; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; + +public class FastParserNumberParsingTest extends NumberParsingTest { + + private final JsonFactory fastFactory = new JsonFactory().enable(JsonParser.Feature.USE_FAST_DOUBLE_PARSER); + + @Override + protected JsonFactory jsonFactory() { + return fastFactory; + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/read/FloatParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/FloatParsingTest.java index 6d2762b038..159c158483 100644 --- a/src/test/java/com/fasterxml/jackson/core/read/FloatParsingTest.java +++ b/src/test/java/com/fasterxml/jackson/core/read/FloatParsingTest.java @@ -12,24 +12,41 @@ public class FloatParsingTest extends BaseTest public void testFloatArrayViaInputStream() throws Exception { - _testFloatArray(MODE_INPUT_STREAM); - _testFloatArray(MODE_INPUT_STREAM_THROTTLED); + _testFloatArray(MODE_INPUT_STREAM, false); + _testFloatArray(MODE_INPUT_STREAM_THROTTLED, false); + } + + public void testFloatArrayViaInputStreamWithFastParser() throws Exception + { + _testFloatArray(MODE_INPUT_STREAM, true); + _testFloatArray(MODE_INPUT_STREAM_THROTTLED, true); } public void testFloatArrayViaReader() throws Exception { - _testFloatArray(MODE_READER); + _testFloatArray(MODE_READER, false); + } + + public void testFloatArrayViaReaderWithFastParser() throws Exception { + _testFloatArray(MODE_READER, true); } public void testFloatArrayViaDataInput() throws Exception { - _testFloatArray(MODE_DATA_INPUT); + _testFloatArray(MODE_DATA_INPUT, false); + } + + public void testFloatArrayViaDataInputWithFasrtParser() throws Exception { + _testFloatArray(MODE_DATA_INPUT, true); } - private void _testFloatArray(int mode) throws Exception + private void _testFloatArray(int mode, boolean useFastParser) throws Exception { // construct new instance to reduce buffer recycling etc: TokenStreamFactory jsonF = newStreamFactory(); JsonParser p = createParser(jsonF, mode, FLOATS_DOC); + if (useFastParser) { + p.enable(JsonParser.Feature.USE_FAST_DOUBLE_PARSER); + } assertToken(JsonToken.START_ARRAY, p.nextToken()); diff --git a/src/test/java/com/fasterxml/jackson/core/read/NonStandardNumberParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/NonStandardNumberParsingTest.java index 82537b1211..7cdd194909 100644 --- a/src/test/java/com/fasterxml/jackson/core/read/NonStandardNumberParsingTest.java +++ b/src/test/java/com/fasterxml/jackson/core/read/NonStandardNumberParsingTest.java @@ -10,6 +10,10 @@ public class NonStandardNumberParsingTest .enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS) .build(); + protected JsonFactory jsonFactory() { + return JSON_F; + } + /** * The format ".NNN" (as opposed to "0.NNN") is not valid JSON, so this should fail */ @@ -27,16 +31,16 @@ public void testLeadingDotInDecimal() throws Exception { } public void testLeadingDotInDecimalAllowedAsync() throws Exception { - _testLeadingDotInDecimalAllowed(JSON_F, MODE_DATA_INPUT); + _testLeadingDotInDecimalAllowed(jsonFactory(), MODE_DATA_INPUT); } public void testLeadingDotInDecimalAllowedBytes() throws Exception { - _testLeadingDotInDecimalAllowed(JSON_F, MODE_INPUT_STREAM); - _testLeadingDotInDecimalAllowed(JSON_F, MODE_INPUT_STREAM_THROTTLED); + _testLeadingDotInDecimalAllowed(jsonFactory(), MODE_INPUT_STREAM); + _testLeadingDotInDecimalAllowed(jsonFactory(), MODE_INPUT_STREAM_THROTTLED); } public void testLeadingDotInDecimalAllowedReader() throws Exception { - _testLeadingDotInDecimalAllowed(JSON_F, MODE_READER); + _testLeadingDotInDecimalAllowed(jsonFactory(), MODE_READER); } private void _testLeadingDotInDecimalAllowed(JsonFactory f, int mode) throws Exception diff --git a/src/test/java/com/fasterxml/jackson/core/read/NumberParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/NumberParsingTest.java index 898dec8969..a5c024c1b3 100644 --- a/src/test/java/com/fasterxml/jackson/core/read/NumberParsingTest.java +++ b/src/test/java/com/fasterxml/jackson/core/read/NumberParsingTest.java @@ -20,7 +20,10 @@ public class NumberParsingTest extends com.fasterxml.jackson.core.BaseTest { - private final JsonFactory FACTORY = sharedStreamFactory(); + + protected JsonFactory jsonFactory() { + return sharedStreamFactory(); + } /* /********************************************************************** @@ -414,7 +417,7 @@ public void testFloatBoundary146Chars() throws Exception arr[i + 3] = '-'; arr[i + 4] = '1'; CharArrayReader r = new CharArrayReader(arr, 0, i+5); - JsonParser p = FACTORY.createParser(r); + JsonParser p = jsonFactory().createParser(r); assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); p.close(); } @@ -431,7 +434,7 @@ public void testFloatBoundary146Bytes() throws Exception arr[i + 3] = '-'; arr[i + 4] = '1'; ByteArrayInputStream in = new ByteArrayInputStream(arr, 0, i+5); - JsonParser p = FACTORY.createParser(in); + JsonParser p = jsonFactory().createParser(in); assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); p.close(); } @@ -612,7 +615,7 @@ public void testParsingOfLongerSequences() throws Exception if (input == 0) { p = createParserUsingStream(DOC, "UTF-8"); } else { - p = FACTORY.createParser(DOC); + p = jsonFactory().createParser(DOC); } assertToken(JsonToken.START_ARRAY, p.nextToken()); @@ -671,8 +674,8 @@ public void testLongNumbers2() throws Exception private void _testIssue160LongNumbers(JsonFactory f, String doc, boolean useStream) throws Exception { JsonParser p = useStream - ? FACTORY.createParser(doc.getBytes("UTF-8")) - : FACTORY.createParser(doc); + ? jsonFactory().createParser(doc.getBytes("UTF-8")) + : jsonFactory().createParser(doc); assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); BigInteger v = p.getBigIntegerValue(); assertNull(p.nextToken()); @@ -686,7 +689,7 @@ private void _testIssue160LongNumbers(JsonFactory f, String doc, boolean useStre */ public void testParsingOfLongerSequencesWithNonNumeric() throws Exception { - JsonFactory f = JsonFactory.builder() + JsonFactory f = jsonFactory().builder() .enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS) .build(); _testParsingOfLongerSequencesWithNonNumeric(f, MODE_INPUT_STREAM); @@ -816,11 +819,11 @@ public void testLongerFloatingPoint() throws Exception // test out with both Reader and ByteArrayInputStream JsonParser p; - p = FACTORY.createParser(new StringReader(DOC)); + p = jsonFactory().createParser(new StringReader(DOC)); _testLongerFloat(p, DOC); p.close(); - p = FACTORY.createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8"))); + p = jsonFactory().createParser(new ByteArrayInputStream(DOC.getBytes("UTF-8"))); _testLongerFloat(p, DOC); p.close(); }