Skip to content

Commit c85fcd3

Browse files
authored
Add feature to enable fast double parsing (#747)
1 parent 07dccdd commit c85fcd3

File tree

11 files changed

+202
-32
lines changed

11 files changed

+202
-32
lines changed

src/main/java/com/fasterxml/jackson/core/JsonParser.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,17 @@ public enum Feature {
325325
*/
326326
INCLUDE_SOURCE_IN_LOCATION(true),
327327

328+
/**
329+
* Feature that determines whether we use the built-in {@link Double#parseDouble(String)} code to parse
330+
* doubles or if we use {@link com.fasterxml.jackson.core.io.doubleparser}
331+
* instead.
332+
*<p>
333+
* This setting is disabled by default.
334+
*
335+
* @since 2.14
336+
*/
337+
USE_FAST_DOUBLE_PARSER(false),
338+
328339
;
329340

330341
/**

src/main/java/com/fasterxml/jackson/core/base/ParserBase.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -893,11 +893,11 @@ private void _parseSlowFloat(int expType) throws IOException
893893
_numberBigDecimal = _textBuffer.contentsAsDecimal();
894894
_numTypesValid = NR_BIGDECIMAL;
895895
} else if (expType == NR_FLOAT) {
896-
_numberFloat = _textBuffer.contentsAsFloat();
896+
_numberFloat = _textBuffer.contentsAsFloat(isEnabled(Feature.USE_FAST_DOUBLE_PARSER));
897897
_numTypesValid = NR_FLOAT;
898898
} else {
899899
// Otherwise double has to do
900-
_numberDouble = _textBuffer.contentsAsDouble();
900+
_numberDouble = _textBuffer.contentsAsDouble(isEnabled(Feature.USE_FAST_DOUBLE_PARSER));
901901
_numTypesValid = NR_DOUBLE;
902902
}
903903
} catch (NumberFormatException nex) {
@@ -927,7 +927,7 @@ private void _parseSlowInt(int expType) throws IOException
927927
_reportTooLongIntegral(expType, numStr);
928928
}
929929
if ((expType == NR_DOUBLE) || (expType == NR_FLOAT)) {
930-
_numberDouble = NumberInput.parseDouble(numStr);
930+
_numberDouble = NumberInput.parseDouble(numStr, isEnabled(Feature.USE_FAST_DOUBLE_PARSER));
931931
_numTypesValid = NR_DOUBLE;
932932
} else {
933933
// nope, need the heavy guns... (rare case)

src/main/java/com/fasterxml/jackson/core/io/NumberInput.java

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.fasterxml.jackson.core.io;
22

3+
import com.fasterxml.jackson.core.io.doubleparser.FastDoubleParser;
4+
import com.fasterxml.jackson.core.io.doubleparser.FastFloatParser;
5+
36
import java.math.BigDecimal;
47

58
public final class NumberInput
@@ -239,7 +242,9 @@ public static int parseAsInt(String s, int def)
239242
// if other symbols, parse as Double, coerce
240243
if (c > '9' || c < '0') {
241244
try {
242-
return (int) parseDouble(s);
245+
//useFastParser=true is used because there is a lot less risk that small changes in result will have an affect
246+
//and performance benefit is useful
247+
return (int) parseDouble(s, true);
243248
} catch (NumberFormatException e) {
244249
return def;
245250
}
@@ -276,7 +281,9 @@ public static long parseAsLong(String s, long def)
276281
// if other symbols, parse as Double, coerce
277282
if (c > '9' || c < '0') {
278283
try {
279-
return (long) parseDouble(s);
284+
//useFastParser=true is used because there is a lot less risk that small changes in result will have an affect
285+
//and performance benefit is useful
286+
return (long) parseDouble(s, true);
280287
} catch (NumberFormatException e) {
281288
return def;
282289
}
@@ -287,8 +294,26 @@ public static long parseAsLong(String s, long def)
287294
} catch (NumberFormatException e) { }
288295
return def;
289296
}
290-
291-
public static double parseAsDouble(String s, double def)
297+
298+
/**
299+
* @param s a string representing a number to parse
300+
* @param def the default to return if `s` is not a parseable number
301+
* @return closest matching double (or `def` if there is an issue with `s`) where useFastParser=false
302+
* @see #parseAsDouble(String, double, boolean)
303+
*/
304+
public static double parseAsDouble(final String s, final double def)
305+
{
306+
return parseAsDouble(s, def, false);
307+
}
308+
309+
/**
310+
* @param s a string representing a number to parse
311+
* @param def the default to return if `s` is not a parseable number
312+
* @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser}
313+
* @return closest matching double (or `def` if there is an issue with `s`)
314+
* @since 2.14
315+
*/
316+
public static double parseAsDouble(String s, final double def, final boolean useFastParser)
292317
{
293318
if (s == null) { return def; }
294319
s = s.trim();
@@ -297,23 +322,52 @@ public static double parseAsDouble(String s, double def)
297322
return def;
298323
}
299324
try {
300-
return parseDouble(s);
325+
return parseDouble(s, useFastParser);
301326
} catch (NumberFormatException e) { }
302327
return def;
303328
}
304329

305-
public static double parseDouble(String s) throws NumberFormatException {
306-
return Double.parseDouble(s);
330+
/**
331+
* @param s a string representing a number to parse
332+
* @return closest matching double
333+
* @throws NumberFormatException if string cannot be represented by a double where useFastParser=false
334+
* @see #parseDouble(String, boolean)
335+
*/
336+
public static double parseDouble(final String s) throws NumberFormatException {
337+
return parseDouble(s, false);
338+
}
339+
340+
/**
341+
* @param s a string representing a number to parse
342+
* @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser}
343+
* @return closest matching double
344+
* @throws NumberFormatException if string cannot be represented by a double
345+
* @since v2.14
346+
*/
347+
public static double parseDouble(final String s, final boolean useFastParser) throws NumberFormatException {
348+
return useFastParser ? FastDoubleParser.parseDouble(s) : Double.parseDouble(s);
349+
}
350+
351+
/**
352+
* @param s a string representing a number to parse
353+
* @return closest matching float
354+
* @throws NumberFormatException if string cannot be represented by a float where useFastParser=false
355+
* @see #parseFloat(String, boolean)
356+
* @since v2.14
357+
*/
358+
public static float parseFloat(final String s) throws NumberFormatException {
359+
return parseFloat(s, false);
307360
}
308361

309362
/**
310363
* @param s a string representing a number to parse
364+
* @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser}
311365
* @return closest matching float
312366
* @throws NumberFormatException if string cannot be represented by a float
313367
* @since v2.14
314368
*/
315-
public static float parseFloat(String s) throws NumberFormatException {
316-
return Float.parseFloat(s);
369+
public static float parseFloat(final String s, final boolean useFastParser) throws NumberFormatException {
370+
return useFastParser ? FastFloatParser.parseFloat(s) : Float.parseFloat(s);
317371
}
318372

319373
public static BigDecimal parseBigDecimal(String s) throws NumberFormatException {

src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,17 @@ public enum JsonReadFeature
182182
*/
183183
@SuppressWarnings("deprecation")
184184
ALLOW_TRAILING_COMMA(false, JsonParser.Feature.ALLOW_TRAILING_COMMA),
185+
186+
/**
187+
* Feature that determines whether we use the built-in {@link Double#parseDouble(String)} code to parse
188+
* doubles or if we use {@link com.fasterxml.jackson.core.io.doubleparser}
189+
* instead.
190+
*<p>
191+
* This setting is disabled by default.
192+
*
193+
* @since 2.14
194+
*/
195+
USE_FAST_DOUBLE_PARSER(false, JsonParser.Feature.USE_FAST_DOUBLE_PARSER)
185196
;
186197

187198
final private boolean _defaultState;

src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,9 +512,25 @@ public BigDecimal contentsAsDecimal() throws NumberFormatException
512512
* @return Buffered text value parsed as a {@link Double}, if possible
513513
*
514514
* @throws NumberFormatException if contents are not a valid Java number
515+
* @deprecated use {@link #contentsAsDouble(boolean)}
515516
*/
517+
@Deprecated
516518
public double contentsAsDouble() throws NumberFormatException {
517-
return NumberInput.parseDouble(contentsAsString());
519+
return contentsAsDouble(false);
520+
}
521+
522+
/**
523+
* Convenience method for converting contents of the buffer
524+
* into a Double value.
525+
*
526+
* @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser}
527+
* @return Buffered text value parsed as a {@link Double}, if possible
528+
*
529+
* @throws NumberFormatException if contents are not a valid Java number
530+
* @since 2.14
531+
*/
532+
public double contentsAsDouble(final boolean useFastParser) throws NumberFormatException {
533+
return NumberInput.parseDouble(contentsAsString(), useFastParser);
518534
}
519535

520536
/**
@@ -525,9 +541,25 @@ public double contentsAsDouble() throws NumberFormatException {
525541
*
526542
* @throws NumberFormatException if contents are not a valid Java number
527543
* @since 2.14
544+
* @deprecated use {@link #contentsAsFloat(boolean)}
528545
*/
546+
@Deprecated
529547
public float contentsAsFloat() throws NumberFormatException {
530-
return NumberInput.parseFloat(contentsAsString());
548+
return contentsAsFloat(false);
549+
}
550+
551+
/**
552+
* Convenience method for converting contents of the buffer
553+
* into a Float value.
554+
*
555+
* @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser}
556+
* @return Buffered text value parsed as a {@link Float}, if possible
557+
*
558+
* @throws NumberFormatException if contents are not a valid Java number
559+
* @since 2.14
560+
*/
561+
public float contentsAsFloat(final boolean useFastParser) throws NumberFormatException {
562+
return NumberInput.parseFloat(contentsAsString(), useFastParser);
531563
}
532564

533565
/**

src/test/java/com/fasterxml/jackson/core/io/TestNumberInput.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,20 @@ public void testNastySmallDouble()
99
//prior to jackson v2.14, this value used to be returned as Double.MIN_VALUE
1010
final String nastySmallDouble = "2.2250738585072012e-308";
1111
assertEquals(Double.parseDouble(nastySmallDouble), NumberInput.parseDouble(nastySmallDouble));
12+
assertEquals(Double.parseDouble(nastySmallDouble), NumberInput.parseDouble(nastySmallDouble, true));
1213
}
1314

1415
public void testParseFloat()
1516
{
1617
final String exampleFloat = "1.199999988079071";
1718
assertEquals(1.1999999f, NumberInput.parseFloat(exampleFloat));
19+
assertEquals(1.1999999f, NumberInput.parseFloat(exampleFloat, true));
1820
assertEquals(1.2f, (float)NumberInput.parseDouble(exampleFloat));
21+
assertEquals(1.2f, (float)NumberInput.parseDouble(exampleFloat, true));
22+
23+
final String exampleFloat2 = "7.006492321624086e-46";
24+
assertEquals("1.4E-45", Float.toString(NumberInput.parseFloat(exampleFloat2)));
25+
assertEquals("1.4E-45", Float.toString(NumberInput.parseFloat(exampleFloat2, true)));
1926
}
2027
}
2128

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.fasterxml.jackson.core.read;
2+
3+
import com.fasterxml.jackson.core.JsonFactory;
4+
import com.fasterxml.jackson.core.json.JsonReadFeature;
5+
6+
public class FastParserNonStandardNumberParsingTest extends NonStandardNumberParsingTest {
7+
private final JsonFactory fastFactory =
8+
JsonFactory.builder()
9+
.enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS)
10+
.enable(JsonReadFeature.USE_FAST_DOUBLE_PARSER)
11+
.build();
12+
13+
@Override
14+
protected JsonFactory jsonFactory() {
15+
return fastFactory;
16+
}
17+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.fasterxml.jackson.core.read;
2+
3+
import com.fasterxml.jackson.core.JsonFactory;
4+
import com.fasterxml.jackson.core.JsonParser;
5+
6+
public class FastParserNumberParsingTest extends NumberParsingTest {
7+
8+
private final JsonFactory fastFactory = new JsonFactory().enable(JsonParser.Feature.USE_FAST_DOUBLE_PARSER);
9+
10+
@Override
11+
protected JsonFactory jsonFactory() {
12+
return fastFactory;
13+
}
14+
}

src/test/java/com/fasterxml/jackson/core/read/FloatParsingTest.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,41 @@ public class FloatParsingTest extends BaseTest
1212

1313
public void testFloatArrayViaInputStream() throws Exception
1414
{
15-
_testFloatArray(MODE_INPUT_STREAM);
16-
_testFloatArray(MODE_INPUT_STREAM_THROTTLED);
15+
_testFloatArray(MODE_INPUT_STREAM, false);
16+
_testFloatArray(MODE_INPUT_STREAM_THROTTLED, false);
17+
}
18+
19+
public void testFloatArrayViaInputStreamWithFastParser() throws Exception
20+
{
21+
_testFloatArray(MODE_INPUT_STREAM, true);
22+
_testFloatArray(MODE_INPUT_STREAM_THROTTLED, true);
1723
}
1824

1925
public void testFloatArrayViaReader() throws Exception {
20-
_testFloatArray(MODE_READER);
26+
_testFloatArray(MODE_READER, false);
27+
}
28+
29+
public void testFloatArrayViaReaderWithFastParser() throws Exception {
30+
_testFloatArray(MODE_READER, true);
2131
}
2232

2333
public void testFloatArrayViaDataInput() throws Exception {
24-
_testFloatArray(MODE_DATA_INPUT);
34+
_testFloatArray(MODE_DATA_INPUT, false);
35+
}
36+
37+
public void testFloatArrayViaDataInputWithFasrtParser() throws Exception {
38+
_testFloatArray(MODE_DATA_INPUT, true);
2539
}
2640

27-
private void _testFloatArray(int mode) throws Exception
41+
private void _testFloatArray(int mode, boolean useFastParser) throws Exception
2842
{
2943
// construct new instance to reduce buffer recycling etc:
3044
TokenStreamFactory jsonF = newStreamFactory();
3145

3246
JsonParser p = createParser(jsonF, mode, FLOATS_DOC);
47+
if (useFastParser) {
48+
p.enable(JsonParser.Feature.USE_FAST_DOUBLE_PARSER);
49+
}
3350

3451
assertToken(JsonToken.START_ARRAY, p.nextToken());
3552

src/test/java/com/fasterxml/jackson/core/read/NonStandardNumberParsingTest.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ public class NonStandardNumberParsingTest
1010
.enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS)
1111
.build();
1212

13+
protected JsonFactory jsonFactory() {
14+
return JSON_F;
15+
}
16+
1317
/**
1418
* The format ".NNN" (as opposed to "0.NNN") is not valid JSON, so this should fail
1519
*/
@@ -27,16 +31,16 @@ public void testLeadingDotInDecimal() throws Exception {
2731
}
2832

2933
public void testLeadingDotInDecimalAllowedAsync() throws Exception {
30-
_testLeadingDotInDecimalAllowed(JSON_F, MODE_DATA_INPUT);
34+
_testLeadingDotInDecimalAllowed(jsonFactory(), MODE_DATA_INPUT);
3135
}
3236

3337
public void testLeadingDotInDecimalAllowedBytes() throws Exception {
34-
_testLeadingDotInDecimalAllowed(JSON_F, MODE_INPUT_STREAM);
35-
_testLeadingDotInDecimalAllowed(JSON_F, MODE_INPUT_STREAM_THROTTLED);
38+
_testLeadingDotInDecimalAllowed(jsonFactory(), MODE_INPUT_STREAM);
39+
_testLeadingDotInDecimalAllowed(jsonFactory(), MODE_INPUT_STREAM_THROTTLED);
3640
}
3741

3842
public void testLeadingDotInDecimalAllowedReader() throws Exception {
39-
_testLeadingDotInDecimalAllowed(JSON_F, MODE_READER);
43+
_testLeadingDotInDecimalAllowed(jsonFactory(), MODE_READER);
4044
}
4145

4246
private void _testLeadingDotInDecimalAllowed(JsonFactory f, int mode) throws Exception

0 commit comments

Comments
 (0)