diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml
index 377f1df16..d6e44f453 100644
--- a/benchmarks/pom.xml
+++ b/benchmarks/pom.xml
@@ -16,8 +16,8 @@
${basedir}/..
- 1.36
- 1.14.4
+ 1.37
+ 1.14.9
benchmarks
UTF-8
@@ -51,7 +51,7 @@
maven-dependency-plugin
- 3.5.0
+ 3.6.0
build-classpath
diff --git a/benchmarks/run.sh b/benchmarks/run.sh
index 380f12289..8c334a5a3 100644
--- a/benchmarks/run.sh
+++ b/benchmarks/run.sh
@@ -1,10 +1,11 @@
#!/usr/bin/env bash
args="-f 4 -wi 5 -i 3 -t 2 -w 2s -r 2s -rf csv -rff"
-jmh="$JAVA_HOME/bin/java -cp ../bin;../lib/*;lib/* com.esotericsoftware.kryo.benchmarks.KryoBenchmarks $args"
+jmh="$JAVA_HOME/bin/java -cp ../eclipse/bin;../eclipse/.apt_generated;../lib/*;lib/* com.esotericsoftware.kryo.benchmarks.KryoBenchmarks $args"
set -ex
+mkdir -p charts/results
$jmh charts/results/fieldSerializer.csv FieldSerializerBenchmark
$jmh charts/results/array.csv ArrayBenchmark
$jmh charts/results/string.csv StringBenchmark
diff --git a/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/BigDecimalBenchmark.java b/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/BigDecimalBenchmark.java
new file mode 100644
index 000000000..2f3711616
--- /dev/null
+++ b/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/BigDecimalBenchmark.java
@@ -0,0 +1,102 @@
+package com.esotericsoftware.kryo.benchmarks;
+
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+import com.esotericsoftware.kryo.serializers.DefaultSerializers;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+import java.math.BigDecimal;
+
+import static java.lang.Integer.parseInt;
+import static java.math.BigDecimal.ONE;
+import static java.math.BigDecimal.ZERO;
+import static java.util.concurrent.TimeUnit.MICROSECONDS;
+import static org.openjdk.jmh.runner.options.TimeValue.seconds;
+
+public class BigDecimalBenchmark {
+
+ @State(Scope.Thread)
+ public static class MyState {
+ final Serializer serializer = new DefaultSerializers.BigDecimalSerializer();
+
+ Output output;
+ Input input;
+
+ @Param({
+ "null", "zero", "one", "0",
+ "2", "10", "max_in_long", "20", // twenty is more than the number of digits in Long.MAX_VALUE
+ "-2", "-10", "min_in_long", "-20" // twenty is more than the number of digits in Long.MIN_VALUE
+ })
+ String numOfDigits = "5";
+ int scale = 2;
+
+ BigDecimal decimal;
+
+ @Setup(Level.Iteration)
+ public void setUp() {
+ decimal = newDecimal(numOfDigits, scale);
+ output = new Output(2, -1);
+ serializer.write(null, output, decimal);
+ input = new Input(output.toBytes());
+ output.reset();
+ }
+
+ private static BigDecimal newDecimal(String numOfDigits, int scale) {
+ switch (numOfDigits) {
+ case "null": return null;
+ case "zero": return ZERO;
+ case "one": return ONE;
+ case "0": return BigDecimal.valueOf(0, scale);
+ case "max_in_long": return BigDecimal.valueOf(Long.MAX_VALUE, scale);
+ case "min_in_long": return BigDecimal.valueOf(Long.MIN_VALUE, scale);
+ default:
+ int digits = parseInt(numOfDigits.replace("-", ""));
+ BigDecimal d = BigDecimal.valueOf(10, 1 - digits).subtract(ONE).scaleByPowerOfTen(-scale); // '9' repeated numOfDigit times
+ return numOfDigits.charAt(0) != '-' ? d : d.negate();
+ }
+ }
+
+ @TearDown(Level.Iteration)
+ public void tearDown () {
+ output.close();
+ input.close();
+ }
+ }
+
+ @Benchmark
+ public byte[] write (MyState state) {
+ state.output.reset();
+ state.serializer.write(null, state.output, state.decimal);
+ return state.output.getBuffer();
+ }
+
+ @Benchmark
+ public BigDecimal read (MyState state) {
+ state.input.reset();
+ return state.serializer.read(null, state.input, BigDecimal.class);
+ }
+
+ public static void main (String[] args) throws RunnerException {
+ final Options opt = new OptionsBuilder()
+ .include(".*" + BigDecimalBenchmark.class.getSimpleName() + ".*")
+ .timeUnit(MICROSECONDS)
+ .warmupIterations(1)
+ .warmupTime(seconds(1))
+ .measurementIterations(4)
+ .measurementTime(seconds(1))
+ .forks(1)
+ .build();
+ new Runner(opt).run();
+ }
+}
diff --git a/eclipse/.settings/org.eclipse.jdt.apt.core.prefs b/eclipse/.settings/org.eclipse.jdt.apt.core.prefs
index 7d52ece52..fa6bcfb3f 100644
--- a/eclipse/.settings/org.eclipse.jdt.apt.core.prefs
+++ b/eclipse/.settings/org.eclipse.jdt.apt.core.prefs
@@ -1,4 +1,5 @@
eclipse.preferences.version=1
org.eclipse.jdt.apt.aptEnabled=true
org.eclipse.jdt.apt.genSrcDir=.apt_generated
+org.eclipse.jdt.apt.genTestSrcDir=.apt_generated_tests
org.eclipse.jdt.apt.reconcileEnabled=true
diff --git a/eclipse/.settings/org.eclipse.jdt.core.prefs b/eclipse/.settings/org.eclipse.jdt.core.prefs
index f424b7ec5..c45bb828e 100644
--- a/eclipse/.settings/org.eclipse.jdt.core.prefs
+++ b/eclipse/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,5 @@
eclipse.preferences.version=1
+org.eclipse.jdt.core.builder.annotationPath.allLocations=disabled
org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=enabled
org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
org.eclipse.jdt.core.compiler.annotation.nonnull=com.esotericsoftware.kryo.util.Null.NonNull
@@ -126,6 +127,7 @@ org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=ignore
org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore
org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.processAnnotations=enabled
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=1.8
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
diff --git a/pom.xml b/pom.xml
index c7d928794..b4aac4a5a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,8 +51,8 @@
5
1.8
UTF-8
- 5.9.3
- 1.8.21
+ 5.10.0
+ 1.9.10
true
@@ -85,7 +85,7 @@
org.apache.commons
commons-lang3
- 3.12.0
+ 3.13.0
test
@@ -105,7 +105,7 @@
org.apache.maven.plugins
maven-clean-plugin
- 3.2.0
+ 3.3.1
@@ -146,7 +146,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 3.1.0
+ 3.1.2
@@ -168,7 +168,7 @@
org.apache.maven.plugins
maven-javadoc-plugin
- 3.5.0
+ 3.6.0
none
@@ -186,7 +186,7 @@
org.apache.maven.plugins
maven-release-plugin
- 3.0.0
+ 3.0.1
true
false
@@ -204,25 +204,25 @@
org.apache.felix
maven-bundle-plugin
- 5.1.8
+ 5.1.9
org.apache.maven.plugins
maven-shade-plugin
- 3.4.1
+ 3.5.1
org.apache.maven.plugins
maven-enforcer-plugin
- 3.3.0
+ 3.4.1
net.revelc.code.formatter
formatter-maven-plugin
- 2.22.0
+ 2.23.0
${kryo.root}/eclipse/code-format.xml
KEEP
@@ -271,7 +271,7 @@
org.apache.maven.plugins
maven-source-plugin
- 3.2.1
+ 3.3.0
attach-sources
@@ -284,7 +284,7 @@
org.apache.maven.plugins
maven-javadoc-plugin
- 3.5.0
+ 3.6.0
attach-javadocs
@@ -320,7 +320,7 @@
org.apache.maven.plugins
maven-enforcer-plugin
- 3.3.0
+ 3.4.1
diff --git a/src/com/esotericsoftware/kryo/serializers/DefaultSerializers.java b/src/com/esotericsoftware/kryo/serializers/DefaultSerializers.java
index 6915cdeb7..32e5cffa8 100644
--- a/src/com/esotericsoftware/kryo/serializers/DefaultSerializers.java
+++ b/src/com/esotericsoftware/kryo/serializers/DefaultSerializers.java
@@ -205,18 +205,7 @@ public BigInteger read (Kryo kryo, Input input, Class extends BigInteger> type
byte[] bytes = input.readBytes(length - 1);
if (type != BigInteger.class && type != null) {
// Use reflection for subclasses.
- try {
- Constructor extends BigInteger> constructor = type.getConstructor(byte[].class);
- if (!constructor.isAccessible()) {
- try {
- constructor.setAccessible(true);
- } catch (SecurityException ignored) {
- }
- }
- return constructor.newInstance(bytes);
- } catch (Exception ex) {
- throw new KryoException(ex);
- }
+ return newBigIntegerSubclass(type, bytes);
}
if (length == 2) {
// Fast-path optimizations for BigInteger constants.
@@ -231,13 +220,26 @@ public BigInteger read (Kryo kryo, Input input, Class extends BigInteger> type
}
return new BigInteger(bytes);
}
+
+ private static BigInteger newBigIntegerSubclass(Class extends BigInteger> type, byte[] bytes) {
+ try {
+ Constructor extends BigInteger> constructor = type.getConstructor(byte[].class);
+ if (!constructor.isAccessible()) {
+ try {
+ constructor.setAccessible(true);
+ } catch (SecurityException ignored) {
+ }
+ }
+ return constructor.newInstance(bytes);
+ } catch (Exception ex) {
+ throw new KryoException(ex);
+ }
+ }
}
/** Serializer for {@link BigDecimal} and any subclass.
* @author Tumi (enhacements) */
public static class BigDecimalSerializer extends ImmutableSerializer {
- private final BigIntegerSerializer bigIntegerSerializer = new BigIntegerSerializer();
-
{
setAcceptsNull(true);
}
@@ -247,42 +249,112 @@ public void write (Kryo kryo, Output output, BigDecimal object) {
output.writeByte(NULL);
return;
}
- // fast-path optimizations for BigDecimal constants
if (object == BigDecimal.ZERO) {
- bigIntegerSerializer.write(kryo, output, BigInteger.ZERO);
- output.writeInt(0, false); // for backwards compatibility
+ output.writeVarInt(2, true);
+ output.writeByte((byte) 0);
+ output.writeInt(0, false);
return;
}
- // default behaviour
- bigIntegerSerializer.write(kryo, output, object.unscaledValue());
+ if (object == BigDecimal.ONE) {
+ output.writeVarInt(2, true);
+ output.writeByte((byte) 1);
+ output.writeInt(0, false);
+ return;
+ }
+
+ BigInteger unscaledBig = null; // avoid getting it from BigDecimal, as non-inflated BigDecimal will have to create it
+ boolean compactForm = object.precision() < 19; // less than nineteen decimal digits for sure fits in a long
+ if (!compactForm) {
+ unscaledBig = object.unscaledValue(); // get and remember for possible use in non-compact form
+ compactForm = unscaledBig.bitLength() <= 63; // check exactly if unscaled value will fit in a long
+ }
+
+ if (!compactForm) {
+ byte[] bytes = unscaledBig.toByteArray();
+ output.writeVarInt(bytes.length + 1, true);
+ output.writeBytes(bytes);
+ } else {
+ long unscaledLong = object.scaleByPowerOfTen(object.scale()).longValue(); // best way to get unscaled long value without creating unscaled BigInteger on the way
+ writeUnscaledLong(output, unscaledLong);
+ }
+
output.writeInt(object.scale(), false);
}
+ // compatible with writing unscaled value represented as BigInteger's bytes
+ private static void writeUnscaledLong(Output output, long unscaledLong) {
+ if (unscaledLong >>> 7 == 0) { // optimize for tiny values
+ output.writeVarInt(2, true);
+ output.writeByte((byte) unscaledLong);
+ } else {
+ byte[] bytes = new byte[8];
+ int pos = 8;
+ do {
+ bytes[--pos] = (byte) (unscaledLong & 0xFF);
+ unscaledLong >>= 8;
+ } while (unscaledLong != 0 && unscaledLong != -1); // out of bits
+
+ if (((bytes[pos] ^ unscaledLong) & 0x80) != 0) {
+ // sign bit didn't fit in previous byte, need to add another byte
+ bytes[--pos] = (byte) unscaledLong;
+ }
+
+ int length = 8 - pos;
+ output.writeVarInt(length + 1, true);
+ output.writeBytes(bytes, pos, length);
+ }
+ }
+
public BigDecimal read (Kryo kryo, Input input, Class extends BigDecimal> type) {
- BigInteger unscaledValue = bigIntegerSerializer.read(kryo, input, BigInteger.class);
- if (unscaledValue == null) return null;
+ BigInteger unscaledBig = null;
+ long unscaledLong = 0;
+
+ int length = input.readVarInt(true);
+ if (length == NULL) return null;
+ length--;
+
+ byte[] bytes = input.readBytes(length);
+ if (length > 8) {
+ unscaledBig = new BigInteger(bytes);
+ } else {
+ unscaledLong = bytes[0];
+ for (int i = 1; i < bytes.length; i++) {
+ unscaledLong <<= 8;
+ unscaledLong |= (bytes[i] & 0xFF);
+ }
+ }
+
int scale = input.readInt(false);
if (type != BigDecimal.class && type != null) {
// For subclasses, use reflection
- try {
- Constructor extends BigDecimal> constructor = type.getConstructor(BigInteger.class, int.class);
- if (!constructor.isAccessible()) {
- try {
- constructor.setAccessible(true);
- } catch (SecurityException ignored) {
- }
+ return newBigDecimalSubclass(type, unscaledBig != null ? unscaledBig : BigInteger.valueOf(unscaledLong), scale);
+ } else {
+ // For BigDecimal, if possible use factory methods to avoid instantiating BigInteger
+ if (unscaledBig != null) {
+ return new BigDecimal(unscaledBig, scale);
+ } else {
+ if (scale == 0) {
+ if (unscaledLong == 0) return BigDecimal.ZERO;
+ if (unscaledLong == 1) return BigDecimal.ONE;
}
- return constructor.newInstance(unscaledValue, scale);
- } catch (Exception ex) {
- throw new KryoException(ex);
+ return BigDecimal.valueOf(unscaledLong, scale);
}
}
- // fast-path optimizations for BigDecimal constants
- if (unscaledValue == BigInteger.ZERO && scale == 0) {
- return BigDecimal.ZERO;
+ }
+
+ private static BigDecimal newBigDecimalSubclass(Class extends BigDecimal> type, BigInteger unscaledValue, int scale) {
+ try {
+ Constructor extends BigDecimal> constructor = type.getConstructor(BigInteger.class, int.class);
+ if (!constructor.isAccessible()) {
+ try {
+ constructor.setAccessible(true);
+ } catch (SecurityException ignored) {
+ }
+ }
+ return constructor.newInstance(unscaledValue, scale);
+ } catch (Exception ex) {
+ throw new KryoException(ex);
}
- // default behaviour
- return new BigDecimal(unscaledValue, scale);
}
}
diff --git a/test/com/esotericsoftware/kryo/serializers/DefaultSerializersTest.java b/test/com/esotericsoftware/kryo/serializers/DefaultSerializersTest.java
index 271a961d2..a5bfb9476 100644
--- a/test/com/esotericsoftware/kryo/serializers/DefaultSerializersTest.java
+++ b/test/com/esotericsoftware/kryo/serializers/DefaultSerializersTest.java
@@ -257,13 +257,60 @@ private java.sql.Timestamp newTimestamp(long time, int nanos) {
void testBigDecimalSerializer () {
kryo.register(BigDecimal.class);
kryo.register(BigDecimalSubclass.class);
+ roundTrip(4, BigDecimal.ZERO);
+
+ // postive values
roundTrip(5, BigDecimal.valueOf(12345, 2));
roundTrip(7, new BigDecimal("12345.12345"));
- roundTrip(4, BigDecimal.ZERO);
roundTrip(4, BigDecimal.ONE);
roundTrip(4, BigDecimal.TEN);
roundTrip(5, new BigDecimalSubclass(new BigInteger("12345"), 2));
roundTrip(7, new BigDecimalSubclass("12345.12345"));
+ roundTrip(11, BigDecimal.valueOf(Long.MAX_VALUE, 2));
+ roundTrip(12, BigDecimal.valueOf(Long.MAX_VALUE, 2).add(BigDecimal.valueOf(1, 2)));
+
+ // negative values
+ roundTrip(5, BigDecimal.valueOf(-12345, 2));
+ roundTrip(7, new BigDecimal("-12345.12345"));
+ roundTrip(4, BigDecimal.ONE.negate());
+ roundTrip(4, BigDecimal.TEN.negate());
+ roundTrip(5, new BigDecimalSubclass(new BigInteger("-12345"), 2));
+ roundTrip(7, new BigDecimalSubclass("-12345.12345"));
+ roundTrip(11, BigDecimal.valueOf(Long.MIN_VALUE, 2));
+ roundTrip(12, BigDecimal.valueOf(Long.MIN_VALUE, 2).subtract(BigDecimal.valueOf(1, 2)));
+ }
+
+ @Test
+ void testBigDecimalSerializerBackwardCompatibility () {
+ kryo.register(BigDecimal.class);
+ output = new Output(8, -1);
+ input = new Input();
+ for (int i = -100000; i < 100000; i++) {
+ output.reset(); input.reset();
+ BigDecimal decimal = BigDecimal.valueOf(i, 2);
+
+ // that's how it was serialized before optimization for small values was implemented
+ byte[] expectedBytes = decimal.unscaledValue().toByteArray();
+ int expectedLength = expectedBytes.length;
+
+ // make sure that after optimizations it is serialized in the same way
+ kryo.writeObject(output, decimal);
+ input.setBuffer(output.getBuffer());
+ int actualLength = input.readVarInt(true) - 1;
+ byte[] actualBytes = input.readBytes(actualLength);
+
+ assertArrayEquals(expectedBytes, actualBytes, () -> String.format(
+ "for %s expected %s but got %s",
+ decimal, Arrays.toString(expectedBytes), Arrays.toString(actualBytes)
+ ));
+ assertEquals(expectedLength, actualLength);
+ assertEquals(decimal.scale(), input.readInt(false));
+
+ // additionaly make sure that after deserialization we get the same value
+ input.reset();
+ BigDecimal actual = kryo.readObject(input, BigDecimal.class);
+ assertEquals(decimal, actual);
+ }
}
@Test