Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve perf of float and double parsing in TextBuffer #1230

Merged
merged 10 commits into from
Apr 20, 2024
8 changes: 5 additions & 3 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ a pure JSON library.

2.18.0 (not yet released)

#1230: Improve performance of `float` and `double` parsing from `TextBuffer`
(implemented by @pjfanning)
#1251: `InternCache` replace synchronized with `ReentrantLock` - the cache
size limit is no longer strictly enforced for performance reasons but
we should never go far about the limit
(contributed by @pjfanning)
(implemented by @pjfanning)
#1252: `ThreadLocalBufferManager` replace synchronized with `ReentrantLock`
(contributed by @pjfanning)
(implemented by @pjfanning)
#1257: Increase InternCache default max size from 100 to 200
#1262: Add diagnostic method pooledCount() in RecyclerPool
#1262: Add diagnostic method `pooledCount()` in `RecyclerPool`
#1266: Change default recycler pool to `bewConcurrentDequePool()` in 2.18

2.17.1 (not yet released)
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/com/fasterxml/jackson/core/io/NumberInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,32 @@ public static double parseDouble(final String s, final boolean useFastParser) th
return useFastParser ? JavaDoubleParser.parseDouble(s) : Double.parseDouble(s);
}

/**
* @param array a char array containing a number to parse
* @param useFastParser whether to use {@code FastDoubleParser}
* @return closest matching double
* @throws NumberFormatException if value cannot be represented by a double
* @since 2.18
*/
public static double parseDouble(final char[] array, final boolean useFastParser) throws NumberFormatException {
return parseDouble(array, 0, array.length, useFastParser);
}

/**
* @param array a char array containing a number to parse
* @param offset the offset to apply when parsing the number in the char array
* @param len the length of the number in the char array
* @param useFastParser whether to use {@code FastDoubleParser}
* @return closest matching double
* @throws NumberFormatException if value cannot be represented by a double
* @since 2.18
*/
public static double parseDouble(final char[] array, final int offset,
final int len, final boolean useFastParser) throws NumberFormatException {
return useFastParser ? JavaDoubleParser.parseDouble(array, offset, len) :
Double.parseDouble(new String(array, offset, len));
}

/**
* @param s a string representing a number to parse
* @return closest matching float
Expand Down Expand Up @@ -428,6 +454,32 @@ public static float parseFloat(final String s, final boolean useFastParser) thro
return Float.parseFloat(s);
}

/**
* @param array a char array containing a number to parse
* @param useFastParser whether to use {@code FastDoubleParser}
* @return closest matching float
* @throws NumberFormatException if value cannot be represented by a float
* @since 2.18
*/
public static float parseFloat(final char[] array, final boolean useFastParser) throws NumberFormatException {
return parseFloat(array, 0, array.length, useFastParser);
}

/**
* @param array a char array containing a number to parse
* @param offset the offset to apply when parsing the number in the char array
* @param len the length of the number in the char array
* @param useFastParser whether to use {@code FastDoubleParser}
* @return closest matching float
* @throws NumberFormatException if value cannot be represented by a float
* @since 2.18
*/
public static float parseFloat(final char[] array, final int offset,
final int len, final boolean useFastParser) throws NumberFormatException {
return useFastParser ? JavaFloatParser.parseFloat(array, offset, len) :
Float.parseFloat(new String(array, offset, len));
}

/**
* @param s a string representing a number to parse
* @return a BigDecimal
Expand Down
72 changes: 62 additions & 10 deletions src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -354,12 +354,11 @@ private char[] buf(int needed)
private void clearSegments()
{
_hasSegments = false;
/* Let's start using _last_ segment from list; for one, it's
* the biggest one, and it's also most likely to be cached
*/
/* 28-Aug-2009, tatu: Actually, the current segment should
* be the biggest one, already
*/
// Let's start using _last_ segment from list; for one, it's
// the biggest one, and it's also most likely to be cached

// 28-Aug-2009, tatu: Actually, the current segment should
// be the biggest one, already
//_currentSegment = _segments.get(_segments.size() - 1);
_segments.clear();
_currentSize = _segmentSize = 0;
Expand Down Expand Up @@ -525,15 +524,41 @@ public char[] contentsAsArray() throws IOException {
/**
* Convenience method for converting contents of the buffer
* into a Double value.
*<p>
* NOTE! Caller <b>MUST</b> validate contents before calling this method,
* to ensure textual version is valid JSON floating-point token -- this
* method is not guaranteed to do any validation and behavior with invalid
* content is not defined (either throws an exception or returns arbitrary
* number).
*
* @param useFastParser whether to use {@code FastDoubleParser}
* @return Buffered text value parsed as a {@link Double}, if possible
*
* @throws NumberFormatException if contents are not a valid Java number
* @throws NumberFormatException may (but is not guaranteed!) be thrown
* if contents are not a valid JSON floating-point number representation
*
* @since 2.14
*/
public double contentsAsDouble(final boolean useFastParser) throws NumberFormatException {
public double contentsAsDouble(final boolean useFastParser) throws NumberFormatException
{
// Order in which check is somewhat arbitrary... try likeliest ones
// that do not require allocation first

// except _resultString first since it works best with JDK (non-fast parser)
if (_resultString != null) {
return NumberInput.parseDouble(_resultString, useFastParser);
}
if (_inputStart >= 0) { // shared?
return NumberInput.parseDouble(_inputBuffer, _inputStart, _inputLen, useFastParser);
}
if (_currentSize == 0) { // all content in current segment!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a bug, actually -- should use _hasSegments instead! Fixed in #1313.

return NumberInput.parseDouble(_currentSegment, 0, _currentSize, useFastParser);
}
if (_resultArray != null) {
return NumberInput.parseDouble(_resultArray, useFastParser);
}

// Otherwise, segmented so need to use slow path
try {
return NumberInput.parseDouble(contentsAsString(), useFastParser);
} catch (IOException e) {
Expand Down Expand Up @@ -574,14 +599,41 @@ public float contentsAsFloat() throws NumberFormatException {
/**
* Convenience method for converting contents of the buffer
* into a Float value.
*<p>
* NOTE! Caller <b>MUST</b> validate contents before calling this method,
* to ensure textual version is valid JSON floating-point token -- this
* method is not guaranteed to do any validation and behavior with invalid
* content is not defined (either throws an exception or returns arbitrary
* number).
*
* @param useFastParser whether to use {@code FastDoubleParser}
* @return Buffered text value parsed as a {@link Float}, if possible
*
* @throws NumberFormatException if contents are not a valid Java number
* @throws NumberFormatException may (but is not guaranteed!) be thrown
* if contents are not a valid JSON floating-point number representation
*
* @since 2.14
*/
public float contentsAsFloat(final boolean useFastParser) throws NumberFormatException {
public float contentsAsFloat(final boolean useFastParser) throws NumberFormatException
{
// Order in which check is somewhat arbitrary... try likeliest ones
// that do not require allocation first

// except _resultString first since it works best with JDK (non-fast parser)
if (_resultString != null) {
return NumberInput.parseFloat(_resultString, useFastParser);
}
if (_inputStart >= 0) { // shared?
return NumberInput.parseFloat(_inputBuffer, _inputStart, _inputLen, useFastParser);
}
if (_currentSize == 0) { // all content in current segment!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, fixed in #1313

return NumberInput.parseFloat(_currentSegment, 0, _currentSize, useFastParser);
}
if (_resultArray != null) {
return NumberInput.parseFloat(_resultArray, useFastParser);
}

// Otherwise, segmented so need to use slow path
try {
return NumberInput.parseFloat(contentsAsString(), useFastParser);
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import org.junit.jupiter.api.Test;

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.*;

class TestTextBuffer
extends com.fasterxml.jackson.core.JUnit5TestBase
extends com.fasterxml.jackson.core.JUnit5TestBase
{
/**
* Trivially simple basic test to ensure all basic append
Expand Down Expand Up @@ -211,4 +213,27 @@ void getSizeFinishCurrentSegmentAndResetWith() throws Exception {
assertEquals(2, textBuffer.size());
}

public void testContentsAsFloat() throws IOException {
TextBuffer textBuffer = new TextBuffer(null);
textBuffer.resetWithString("1.2345678");
assertEquals(1.2345678f, textBuffer.contentsAsFloat(false));
}

public void testContentsAsFloatFastParser() throws IOException {
TextBuffer textBuffer = new TextBuffer(null);
textBuffer.resetWithString("1.2345678");
assertEquals(1.2345678f, textBuffer.contentsAsFloat(true));
}

public void testContentsAsDouble() throws IOException {
TextBuffer textBuffer = new TextBuffer(null);
textBuffer.resetWithString("1.234567890123456789");
assertEquals(1.234567890123456789d, textBuffer.contentsAsDouble(false));
}

public void testContentsAsDoubleFastParser() throws IOException {
TextBuffer textBuffer = new TextBuffer(null);
textBuffer.resetWithString("1.234567890123456789");
assertEquals(1.234567890123456789d, textBuffer.contentsAsDouble(true));
}
}