Skip to content

Commit

Permalink
Faster division by 1000 (#1203)
Browse files Browse the repository at this point in the history
  • Loading branch information
xtonik committed Feb 15, 2024
1 parent f46b4d5 commit b2e382a
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 17 deletions.
4 changes: 4 additions & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -426,3 +426,7 @@ Guillaume Lecroc (@gulecroc)
* Contributed #1179: Allow configuring `DefaultPrettyPrinter` separators for empty
Arrays and Objects
(2.17.0)

Antonin Janec (@xtonic)
* Contributed #1203: Faster division by 1000
(2.17.0)
2 changes: 2 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ a pure JSON library.
#1195: Use `BufferRecycler` provided by output (`OutputStream`, `Writer`) object if available
(contributed by Mario F)
#1202: Add `RecyclerPool.clear()` method for dropping all pooled Objects
#1203: Faster division by 1000
(contributed by @xtonik)
#1205: JsonFactory.setStreamReadConstraints(StreamReadConstraints) fails to
update "maxNameLength" for symbol tables
(reported by @denizk)
Expand Down
42 changes: 26 additions & 16 deletions src/main/java/com/fasterxml/jackson/core/io/NumberOutput.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public static int outputInt(int v, char[] b, int off)
}
return _leading3(v, b, off);
}
int thousands = v / 1000;
int thousands = divBy1000(v);
v -= (thousands * 1000); // == value % 1000
off = _leading3(thousands, b, off);
off = _full3(v, b, off);
Expand All @@ -107,10 +107,10 @@ public static int outputInt(int v, char[] b, int off)
}
return _outputFullBillion(v, b, off);
}
int newValue = v / 1000;
int newValue = divBy1000(v);
int ones = (v - (newValue * 1000)); // == value % 1000
v = newValue;
newValue /= 1000;
newValue = divBy1000(newValue);
int thousands = (v - (newValue * 1000));

off = _leading3(newValue, b, off);
Expand All @@ -136,7 +136,7 @@ public static int outputInt(int v, byte[] b, int off)
off = _leading3(v, b, off);
}
} else {
int thousands = v / 1000;
int thousands = divBy1000(v);
v -= (thousands * 1000); // == value % 1000
off = _leading3(thousands, b, off);
off = _full3(v, b, off);
Expand All @@ -153,10 +153,10 @@ public static int outputInt(int v, byte[] b, int off)
}
return _outputFullBillion(v, b, off);
}
int newValue = v / 1000;
int newValue = divBy1000(v);
int ones = (v - (newValue * 1000)); // == value % 1000
v = newValue;
newValue /= 1000;
newValue = divBy1000(newValue);
int thousands = (v - (newValue * 1000));
off = _leading3(newValue, b, off);
off = _full3(thousands, b, off);
Expand Down Expand Up @@ -245,6 +245,16 @@ public static int outputLong(long v, byte[] b, int off)
return _outputFullBillion((int) v, b, off);
}

/**
* Optimized code for integer division by 1000; typically 50% higher
* throughput for calculation
*
* @since 2.17
*/
static int divBy1000(int number) {
return (int) (number * 274_877_907L >>> 38);
}

/*
/**********************************************************
/* Convenience serialization methods
Expand Down Expand Up @@ -359,13 +369,13 @@ private static int _outputUptoBillion(int v, char[] b, int off)
if (v < 1000) {
return _leading3(v, b, off);
}
int thousands = v / 1000;
int thousands = divBy1000(v);
int ones = v - (thousands * 1000); // == value % 1000
return _outputUptoMillion(b, off, thousands, ones);
}
int thousands = v / 1000;
int thousands = divBy1000(v);
int ones = (v - (thousands * 1000)); // == value % 1000
int millions = thousands / 1000;
int millions = divBy1000(thousands);
thousands -= (millions * 1000);

off = _leading3(millions, b, off);
Expand All @@ -385,9 +395,9 @@ private static int _outputUptoBillion(int v, char[] b, int off)

private static int _outputFullBillion(int v, char[] b, int off)
{
int thousands = v / 1000;
int thousands = divBy1000(v);
int ones = (v - (thousands * 1000)); // == value % 1000
int millions = thousands / 1000;
int millions = divBy1000(thousands);

int enc = TRIPLET_TO_CHARS[millions];
b[off++] = (char) (enc >> 16);
Expand All @@ -414,13 +424,13 @@ private static int _outputUptoBillion(int v, byte[] b, int off)
if (v < 1000) {
return _leading3(v, b, off);
}
int thousands = v / 1000;
int thousands = divBy1000(v);
int ones = v - (thousands * 1000); // == value % 1000
return _outputUptoMillion(b, off, thousands, ones);
}
int thousands = v / 1000;
int thousands = divBy1000(v);
int ones = (v - (thousands * 1000)); // == value % 1000
int millions = thousands / 1000;
int millions = divBy1000(thousands);
thousands -= (millions * 1000);

off = _leading3(millions, b, off);
Expand All @@ -440,9 +450,9 @@ private static int _outputUptoBillion(int v, byte[] b, int off)

private static int _outputFullBillion(int v, byte[] b, int off)
{
int thousands = v / 1000;
int thousands = divBy1000(v);
int ones = (v - (thousands * 1000)); // == value % 1000
int millions = thousands / 1000;
int millions = divBy1000(thousands);
thousands -= (millions * 1000);

int enc = TRIPLET_TO_CHARS[millions];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1096,13 +1096,18 @@ private int _resizeAndFindOffsetForAdd(int hash) throws StreamConstraintsExcepti
return offset;
}

// @since 2.17
static int multiplyByFourFifths(int number) {
return (int) (number * 3_435_973_837L >>> 32);
}

// Helper method for checking if we should simply rehash() before add
private boolean _checkNeedForRehash() {
// Yes if above 80%, or above 50% AND have ~1% spill-overs
if (_count > (_hashSize >> 1)) { // over 50%
int spillCount = (_spilloverEnd - _spilloverStart()) >> 2;
if ((spillCount > (1 + _count >> 7))
|| (_count > (_hashSize * 0.80))) {
|| (_count > multiplyByFourFifths(_hashSize))) {
return true;
}
}
Expand Down
48 changes: 48 additions & 0 deletions src/test/java/com/fasterxml/jackson/core/io/NumberOutputTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.fasterxml.jackson.core.io;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

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

public class NumberOutputTest
{
@Test
public void testDivBy1000Small()
{
for (int number = 0; number <= 999_999; ++number) {
int expected = number / 1000;
int actual = NumberOutput.divBy1000(number);
if (expected != actual) { // only construct String if fail
fail("With "+number+" should get "+expected+", got: "+actual);
}
}
}

@Test
public void testDivBy1000Sampled()
{
for (int number = 1_000_000; number > 0; number += 7) {
int expected = number / 1000;
int actual = NumberOutput.divBy1000(number);
if (expected != actual) { // only construct String if fail
fail("With "+number+" should get "+expected+", got: "+actual);
}
}
}

// And then full range, not included in CI since code shouldn't change;
// but has been run to verify full range manually
@Test
// Comment out for manual testing:
@Disabled
public void testDivBy1000FullRange() {
for (int number = 0; number <= Integer.MAX_VALUE; ++number) {
int expected = number / 1000;
int actual = NumberOutput.divBy1000(number);
if (expected != actual) { // only construct String if fail
fail("With "+number+" should get "+expected+", got: "+actual);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.fasterxml.jackson.core.sym;

import org.junit.Ignore;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class ByteQuadsCanonicalizerTest {
@Test
@Ignore
public void testMultiplyByFourFifths() {
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
int number = (int) i;
int expected = (int) (number * 0.80);
int actual = ByteQuadsCanonicalizer.multiplyByFourFifths(number);
assertEquals("input=" + number, expected, actual);
}
}
}

0 comments on commit b2e382a

Please sign in to comment.