Skip to content

Commit

Permalink
Add feature to enable fast double parsing (#747)
Browse files Browse the repository at this point in the history
  • Loading branch information
pjfanning committed Jun 20, 2022
1 parent 07dccdd commit c85fcd3
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 32 deletions.
11 changes: 11 additions & 0 deletions src/main/java/com/fasterxml/jackson/core/JsonParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*<p>
* This setting is disabled by default.
*
* @since 2.14
*/
USE_FAST_DOUBLE_PARSER(false),

;

/**
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/com/fasterxml/jackson/core/base/ParserBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
72 changes: 63 additions & 9 deletions src/main/java/com/fasterxml/jackson/core/io/NumberInput.java
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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();
Expand All @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*<p>
* This setting is disabled by default.
*
* @since 2.14
*/
USE_FAST_DOUBLE_PARSER(false, JsonParser.Feature.USE_FAST_DOUBLE_PARSER)
;

final private boolean _defaultState;
Expand Down
36 changes: 34 additions & 2 deletions src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}
}

Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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
Expand Down

0 comments on commit c85fcd3

Please sign in to comment.