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 type byte[] bytes = input.readBytes(length - 1); if (type != BigInteger.class && type != null) { // Use reflection for subclasses. - try { - Constructor 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 type } return new BigInteger(bytes); } + + private static BigInteger newBigIntegerSubclass(Class type, byte[] bytes) { + try { + Constructor 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 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 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 type, BigInteger unscaledValue, int scale) { + try { + Constructor 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