diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index c81e5d5d2a..664b4ea45f 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -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) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 89c5b1094c..b2b5f00815 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -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) diff --git a/src/main/java/com/fasterxml/jackson/core/io/NumberOutput.java b/src/main/java/com/fasterxml/jackson/core/io/NumberOutput.java index c78c610de1..ce863718b4 100644 --- a/src/main/java/com/fasterxml/jackson/core/io/NumberOutput.java +++ b/src/main/java/com/fasterxml/jackson/core/io/NumberOutput.java @@ -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); @@ -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); @@ -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); @@ -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); @@ -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 @@ -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); @@ -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); @@ -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); @@ -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]; diff --git a/src/main/java/com/fasterxml/jackson/core/sym/ByteQuadsCanonicalizer.java b/src/main/java/com/fasterxml/jackson/core/sym/ByteQuadsCanonicalizer.java index 1df0e03dd1..121458b1b6 100644 --- a/src/main/java/com/fasterxml/jackson/core/sym/ByteQuadsCanonicalizer.java +++ b/src/main/java/com/fasterxml/jackson/core/sym/ByteQuadsCanonicalizer.java @@ -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; } } diff --git a/src/test/java/com/fasterxml/jackson/core/io/NumberOutputTest.java b/src/test/java/com/fasterxml/jackson/core/io/NumberOutputTest.java new file mode 100644 index 0000000000..e006e418a5 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/io/NumberOutputTest.java @@ -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); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/com/fasterxml/jackson/core/sym/ByteQuadsCanonicalizerTest.java b/src/test/java/com/fasterxml/jackson/core/sym/ByteQuadsCanonicalizerTest.java new file mode 100644 index 0000000000..91cba3340f --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/sym/ByteQuadsCanonicalizerTest.java @@ -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); + } + } +} \ No newline at end of file