8247532: Records deserialization is slow

8248135: Build microbenchmarks with --enable-preview

Test contributed by Chris Hegarty <chris.hegarty@oracle.com>

Reviewed-by: chegar, psandoz, redestad, ihse
This commit is contained in:
Peter Levart 2020-06-24 11:05:09 +02:00
parent 4bcd70acc0
commit 2f09989ec0
5 changed files with 972 additions and 70 deletions

View File

@ -90,10 +90,11 @@ $(eval $(call SetupJavaCompilation, BUILD_JDK_MICROBENCHMARK, \
TARGET_RELEASE := $(TARGET_RELEASE_NEWJDK_UPGRADED), \
SMALL_JAVA := false, \
CLASSPATH := $(MICROBENCHMARK_CLASSPATH), \
DISABLED_WARNINGS := processing rawtypes cast serial, \
DISABLED_WARNINGS := processing rawtypes cast serial preview, \
SRC := $(MICROBENCHMARK_SRC), \
BIN := $(MICROBENCHMARK_CLASSES), \
JAVA_FLAGS := --add-modules jdk.unsupported --limit-modules java.management, \
JAVAC_FLAGS := --enable-preview, \
))
$(BUILD_JDK_MICROBENCHMARK): $(JMH_COMPILE_JARS)

View File

@ -2182,7 +2182,7 @@ public class ObjectInputStream
handles.markException(passHandle, resolveEx);
}
final boolean isRecord = cl != null && isRecord(cl) ? true : false;
final boolean isRecord = cl != null && isRecord(cl);
if (isRecord) {
assert obj == null;
obj = readRecord(desc);
@ -2289,14 +2289,14 @@ public class ObjectInputStream
FieldValues fieldValues = defaultReadFields(null, desc);
// retrieve the canonical constructor
MethodHandle ctrMH = desc.getRecordConstructor();
// bind the stream field values
ctrMH = RecordSupport.bindCtrValues(ctrMH, desc, fieldValues);
// get canonical record constructor adapted to take two arguments:
// - byte[] primValues
// - Object[] objValues
// and return Object
MethodHandle ctrMH = RecordSupport.deserializationCtr(desc);
try {
return ctrMH.invoke();
return (Object) ctrMH.invokeExact(fieldValues.primValues, fieldValues.objValues);
} catch (Exception e) {
InvalidObjectException ioe = new InvalidObjectException(e.getMessage());
ioe.initCause(e);

View File

@ -27,6 +27,7 @@ package java.io;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
@ -55,6 +56,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@ -191,8 +193,14 @@ public class ObjectStreamClass implements Serializable {
/** serialization-appropriate constructor, or null if none */
private Constructor<?> cons;
/** record canonical constructor, or null */
/** record canonical constructor (shared among OSCs for same class), or null */
private MethodHandle canonicalCtr;
/** cache of record deserialization constructors per unique set of stream fields
* (shared among OSCs for same class), or null */
private DeserializationConstructorsCache deserializationCtrs;
/** session-cache of record deserialization constructor
* (in de-serialized OSC only), or null */
private MethodHandle deserializationCtr;
/** protection domains that need to be checked when calling the constructor */
private ProtectionDomain[] domains;
@ -525,6 +533,7 @@ public class ObjectStreamClass implements Serializable {
if (isRecord) {
canonicalCtr = canonicalRecordCtr(cl);
deserializationCtrs = new DeserializationConstructorsCache();
} else if (externalizable) {
cons = getExternalizableConstructor(cl);
} else {
@ -740,7 +749,10 @@ public class ObjectStreamClass implements Serializable {
this.cl = cl;
if (cl != null) {
this.isRecord = isRecord(cl);
// canonical record constructor is shared
this.canonicalCtr = osc.canonicalCtr;
// cache of deserialization constructors is shared
this.deserializationCtrs = osc.deserializationCtrs;
}
this.resolveEx = resolveEx;
this.superDesc = superDesc;
@ -2528,14 +2540,135 @@ public class ObjectStreamClass implements Serializable {
}
}
// a LRA cache of record deserialization constructors
@SuppressWarnings("serial")
private static final class DeserializationConstructorsCache
extends ConcurrentHashMap<DeserializationConstructorsCache.Key, MethodHandle> {
// keep max. 10 cached entries - when the 11th element is inserted the oldest
// is removed and 10 remains - 11 is the biggest map size where internal
// table of 16 elements is sufficient (inserting 12th element would resize it to 32)
private static final int MAX_SIZE = 10;
private Key.Impl first, last; // first and last in FIFO queue
DeserializationConstructorsCache() {
// start small - if there is more than one shape of ObjectStreamClass
// deserialized, there will typically be two (current version and previous version)
super(2);
}
MethodHandle get(ObjectStreamField[] fields) {
return get(new Key.Lookup(fields));
}
synchronized MethodHandle putIfAbsentAndGet(ObjectStreamField[] fields, MethodHandle mh) {
Key.Impl key = new Key.Impl(fields);
var oldMh = putIfAbsent(key, mh);
if (oldMh != null) return oldMh;
// else we did insert new entry -> link the new key as last
if (last == null) {
last = first = key;
} else {
last = (last.next = key);
}
// may need to remove first
if (size() > MAX_SIZE) {
assert first != null;
remove(first);
first = first.next;
if (first == null) {
last = null;
}
}
return mh;
}
// a key composed of ObjectStreamField[] names and types
static abstract class Key {
abstract int length();
abstract String fieldName(int i);
abstract Class<?> fieldType(int i);
@Override
public final int hashCode() {
int n = length();
int h = 0;
for (int i = 0; i < n; i++) h = h * 31 + fieldType(i).hashCode();
for (int i = 0; i < n; i++) h = h * 31 + fieldName(i).hashCode();
return h;
}
@Override
public final boolean equals(Object obj) {
if (!(obj instanceof Key)) return false;
Key other = (Key) obj;
int n = length();
if (n != other.length()) return false;
for (int i = 0; i < n; i++) if (fieldType(i) != other.fieldType(i)) return false;
for (int i = 0; i < n; i++) if (!fieldName(i).equals(other.fieldName(i))) return false;
return true;
}
// lookup key - just wraps ObjectStreamField[]
static final class Lookup extends Key {
final ObjectStreamField[] fields;
Lookup(ObjectStreamField[] fields) { this.fields = fields; }
@Override
int length() { return fields.length; }
@Override
String fieldName(int i) { return fields[i].getName(); }
@Override
Class<?> fieldType(int i) { return fields[i].getType(); }
}
// real key - copies field names and types and forms FIFO queue in cache
static final class Impl extends Key {
Impl next;
final String[] fieldNames;
final Class<?>[] fieldTypes;
Impl(ObjectStreamField[] fields) {
this.fieldNames = new String[fields.length];
this.fieldTypes = new Class<?>[fields.length];
for (int i = 0; i < fields.length; i++) {
fieldNames[i] = fields[i].getName();
fieldTypes[i] = fields[i].getType();
}
}
@Override
int length() { return fieldNames.length; }
@Override
String fieldName(int i) { return fieldNames[i]; }
@Override
Class<?> fieldType(int i) { return fieldTypes[i]; }
}
}
}
/** Record specific support for retrieving and binding stream field values. */
static final class RecordSupport {
/** Binds the given stream field values to the given method handle. */
/**
* Returns canonical record constructor adapted to take two arguments:
* {@code (byte[] primValues, Object[] objValues)}
* and return
* {@code Object}
*/
@SuppressWarnings("preview")
static MethodHandle bindCtrValues(MethodHandle ctrMH,
ObjectStreamClass desc,
ObjectInputStream.FieldValues fieldValues) {
static MethodHandle deserializationCtr(ObjectStreamClass desc) {
// check the cached value 1st
MethodHandle mh = desc.deserializationCtr;
if (mh != null) return mh;
mh = desc.deserializationCtrs.get(desc.getFields(false));
if (mh != null) return desc.deserializationCtr = mh;
// retrieve record components
RecordComponent[] recordComponents;
try {
Class<?> cls = desc.forClass();
@ -2545,15 +2678,36 @@ public class ObjectStreamClass implements Serializable {
throw new InternalError(e.getCause());
}
Object[] args = new Object[recordComponents.length];
for (int i = 0; i < recordComponents.length; i++) {
String name = recordComponents[i].getName();
Class<?> type= recordComponents[i].getType();
Object o = streamFieldValue(name, type, desc, fieldValues);
args[i] = o;
}
// retrieve the canonical constructor
// (T1, T2, ..., Tn):TR
mh = desc.getRecordConstructor();
return MethodHandles.insertArguments(ctrMH, 0, args);
// change return type to Object
// (T1, T2, ..., Tn):TR -> (T1, T2, ..., Tn):Object
mh = mh.asType(mh.type().changeReturnType(Object.class));
// drop last 2 arguments representing primValues and objValues arrays
// (T1, T2, ..., Tn):Object -> (T1, T2, ..., Tn, byte[], Object[]):Object
mh = MethodHandles.dropArguments(mh, mh.type().parameterCount(), byte[].class, Object[].class);
for (int i = recordComponents.length-1; i >= 0; i--) {
String name = recordComponents[i].getName();
Class<?> type = recordComponents[i].getType();
// obtain stream field extractor that extracts argument at
// position i (Ti+1) from primValues and objValues arrays
// (byte[], Object[]):Ti+1
MethodHandle combiner = streamFieldExtractor(name, type, desc);
// fold byte[] privValues and Object[] objValues into argument at position i (Ti+1)
// (..., Ti, Ti+1, byte[], Object[]):Object -> (..., Ti, byte[], Object[]):Object
mh = MethodHandles.foldArguments(mh, i, combiner);
}
// what we are left with is a MethodHandle taking just the primValues
// and objValues arrays and returning the constructed record instance
// (byte[], Object[]):Object
// store it into cache and return the 1st value stored
return desc.deserializationCtr =
desc.deserializationCtrs.putIfAbsentAndGet(desc.getFields(false), mh);
}
/** Returns the number of primitive fields for the given descriptor. */
@ -2569,37 +2723,15 @@ public class ObjectStreamClass implements Serializable {
return primValueCount;
}
/** Returns the default value for the given type. */
private static Object defaultValueFor(Class<?> pType) {
if (pType == Integer.TYPE)
return 0;
else if (pType == Byte.TYPE)
return (byte)0;
else if (pType == Long.TYPE)
return 0L;
else if (pType == Float.TYPE)
return 0.0f;
else if (pType == Double.TYPE)
return 0.0d;
else if (pType == Short.TYPE)
return (short)0;
else if (pType == Character.TYPE)
return '\u0000';
else if (pType == Boolean.TYPE)
return false;
else
return null;
}
/**
* Returns the stream field value for the given name. The default value
* for the given type is returned if the field value is absent.
* Returns extractor MethodHandle taking the primValues and objValues arrays
* and extracting the argument of canonical constructor with given name and type
* or producing default value for the given type if the field is absent.
*/
private static Object streamFieldValue(String pName,
Class<?> pType,
ObjectStreamClass desc,
ObjectInputStream.FieldValues fieldValues) {
ObjectStreamField[] fields = desc.getFields();
private static MethodHandle streamFieldExtractor(String pName,
Class<?> pType,
ObjectStreamClass desc) {
ObjectStreamField[] fields = desc.getFields(false);
for (int i = 0; i < fields.length; i++) {
ObjectStreamField f = fields[i];
@ -2612,30 +2744,62 @@ public class ObjectStreamClass implements Serializable {
throw new InternalError(fName + " unassignable, pType:" + pType + ", fType:" + fType);
if (f.isPrimitive()) {
if (pType == Integer.TYPE)
return Bits.getInt(fieldValues.primValues, f.getOffset());
else if (fType == Byte.TYPE)
return fieldValues.primValues[f.getOffset()];
else if (fType == Long.TYPE)
return Bits.getLong(fieldValues.primValues, f.getOffset());
else if (fType == Float.TYPE)
return Bits.getFloat(fieldValues.primValues, f.getOffset());
else if (fType == Double.TYPE)
return Bits.getDouble(fieldValues.primValues, f.getOffset());
else if (fType == Short.TYPE)
return Bits.getShort(fieldValues.primValues, f.getOffset());
else if (fType == Character.TYPE)
return Bits.getChar(fieldValues.primValues, f.getOffset());
else if (fType == Boolean.TYPE)
return Bits.getBoolean(fieldValues.primValues, f.getOffset());
else
// (byte[], int):fType
MethodHandle mh = PRIM_VALUE_EXTRACTORS.get(fType);
if (mh == null) {
throw new InternalError("Unexpected type: " + fType);
}
// bind offset
// (byte[], int):fType -> (byte[]):fType
mh = MethodHandles.insertArguments(mh, 1, f.getOffset());
// drop objValues argument
// (byte[]):fType -> (byte[], Object[]):fType
mh = MethodHandles.dropArguments(mh, 1, Object[].class);
// adapt return type to pType
// (byte[], Object[]):fType -> (byte[], Object[]):pType
if (pType != fType) {
mh = mh.asType(mh.type().changeReturnType(pType));
}
return mh;
} else { // reference
return fieldValues.objValues[i - numberPrimValues(desc)];
// (Object[], int):Object
MethodHandle mh = MethodHandles.arrayElementGetter(Object[].class);
// bind index
// (Object[], int):Object -> (Object[]):Object
mh = MethodHandles.insertArguments(mh, 1, i - numberPrimValues(desc));
// drop primValues argument
// (Object[]):Object -> (byte[], Object[]):Object
mh = MethodHandles.dropArguments(mh, 0, byte[].class);
// adapt return type to pType
// (byte[], Object[]):Object -> (byte[], Object[]):pType
if (pType != Object.class) {
mh = mh.asType(mh.type().changeReturnType(pType));
}
return mh;
}
}
return defaultValueFor(pType);
// return default value extractor if no field matches pName
return MethodHandles.empty(MethodType.methodType(pType, byte[].class, Object[].class));
}
private static final Map<Class<?>, MethodHandle> PRIM_VALUE_EXTRACTORS;
static {
var lkp = MethodHandles.lookup();
try {
PRIM_VALUE_EXTRACTORS = Map.of(
byte.class, MethodHandles.arrayElementGetter(byte[].class),
short.class, lkp.findStatic(Bits.class, "getShort", MethodType.methodType(short.class, byte[].class, int.class)),
int.class, lkp.findStatic(Bits.class, "getInt", MethodType.methodType(int.class, byte[].class, int.class)),
long.class, lkp.findStatic(Bits.class, "getLong", MethodType.methodType(long.class, byte[].class, int.class)),
float.class, lkp.findStatic(Bits.class, "getFloat", MethodType.methodType(float.class, byte[].class, int.class)),
double.class, lkp.findStatic(Bits.class, "getDouble", MethodType.methodType(double.class, byte[].class, int.class)),
char.class, lkp.findStatic(Bits.class, "getChar", MethodType.methodType(char.class, byte[].class, int.class)),
boolean.class, lkp.findStatic(Bits.class, "getBoolean", MethodType.methodType(boolean.class, byte[].class, int.class))
);
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new InternalError("Can't lookup Bits.getXXX", e);
}
}
}
}

View File

@ -0,0 +1,563 @@
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @summary Checks that the appropriate value is given to the canonical ctr
* @compile --enable-preview -source ${jdk.version} DifferentStreamFieldsTest.java
* @run testng/othervm --enable-preview DifferentStreamFieldsTest
* @run testng/othervm/java.security.policy=empty_security.policy --enable-preview DifferentStreamFieldsTest
*/
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static java.io.ObjectStreamConstants.SC_SERIALIZABLE;
import static java.io.ObjectStreamConstants.STREAM_MAGIC;
import static java.io.ObjectStreamConstants.STREAM_VERSION;
import static java.io.ObjectStreamConstants.TC_CLASSDESC;
import static java.io.ObjectStreamConstants.TC_ENDBLOCKDATA;
import static java.io.ObjectStreamConstants.TC_NULL;
import static java.io.ObjectStreamConstants.TC_OBJECT;
import static java.io.ObjectStreamConstants.TC_STRING;
import static java.lang.System.out;
import static org.testng.Assert.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.UncheckedIOException;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Checks that the appropriate value is given to the canonical ctr.
*/
public class DifferentStreamFieldsTest {
record R01(boolean x) implements Serializable {}
record R02(byte x) implements Serializable {}
record R03(short x) implements Serializable {}
record R04(char x) implements Serializable {}
record R05(int x) implements Serializable {}
record R06(long x) implements Serializable {}
record R07(float x) implements Serializable {}
record R08(double x) implements Serializable {}
record R09(Object x) implements Serializable {}
record R10(String x) implements Serializable {}
record R11(int[]x) implements Serializable {}
record R12(Object[]x) implements Serializable {}
record R13(R12 x) implements Serializable {}
record R14(R13[]x) implements Serializable {}
@DataProvider(name = "recordTypeAndExpectedValue")
public Object[][] recordTypeAndExpectedValue() {
return new Object[][]{
new Object[]{R01.class, false},
new Object[]{R02.class, (byte) 0},
new Object[]{R03.class, (short) 0},
new Object[]{R04.class, '\u0000'},
new Object[]{R05.class, 0},
new Object[]{R06.class, 0L},
new Object[]{R07.class, 0.0f},
new Object[]{R08.class, 0.0d},
new Object[]{R09.class, null},
new Object[]{R10.class, null},
new Object[]{R11.class, null},
new Object[]{R12.class, null},
new Object[]{R13.class, null},
new Object[]{R14.class, null}
};
}
@Test(dataProvider = "recordTypeAndExpectedValue")
public void testWithDifferentTypes(Class<?> clazz, Object expectedXValue)
throws Exception {
out.println("\n---");
assert clazz.isRecord();
byte[] bytes = SerialByteStreamBuilder
.newBuilder(clazz.getName())
.build();
Object obj = deserialize(bytes);
out.println("deserialized: " + obj);
Object actualXValue = clazz.getDeclaredMethod("x").invoke(obj);
assertEquals(actualXValue, expectedXValue);
bytes = SerialByteStreamBuilder
.newBuilder(clazz.getName())
.addPrimitiveField("y", int.class, 5) // stream junk
.build();
obj = deserialize(bytes);
out.println("deserialized: " + obj);
actualXValue = clazz.getDeclaredMethod("x").invoke(obj);
assertEquals(actualXValue, expectedXValue);
}
// --- all together
@Test
public void testWithAllTogether() throws Exception {
out.println("\n---");
record R15(boolean a, byte b, short c, char d, int e, long f, float g,
double h, Object i, String j, long[]k, Object[]l)
implements Serializable {}
byte[] bytes = SerialByteStreamBuilder
.newBuilder(R15.class.getName())
.addPrimitiveField("x", int.class, 5) // stream junk
.build();
R15 obj = deserialize(bytes);
out.println("deserialized: " + obj);
assertEquals(obj.a, false);
assertEquals(obj.b, 0);
assertEquals(obj.c, 0);
assertEquals(obj.d, '\u0000');
assertEquals(obj.e, 0);
assertEquals(obj.f, 0l);
assertEquals(obj.g, 0f);
assertEquals(obj.h, 0d);
assertEquals(obj.i, null);
assertEquals(obj.j, null);
assertEquals(obj.k, null);
assertEquals(obj.l, null);
}
@Test
public void testInt() throws Exception {
out.println("\n---");
{
record R(int x) implements Serializable {}
var r = new R(5);
byte[] OOSBytes = serialize(r);
byte[] builderBytes = SerialByteStreamBuilder
.newBuilder(R.class.getName())
.addPrimitiveField("x", int.class, 5)
.build();
var deser1 = deserialize(OOSBytes);
assertEquals(deser1, r);
var deser2 = deserialize(builderBytes);
assertEquals(deser2, deser1);
}
{
record R(int x, int y) implements Serializable {}
var r = new R(7, 8);
byte[] OOSBytes = serialize(r);
var deser1 = deserialize(OOSBytes);
assertEquals(deser1, r);
byte[] builderBytes = SerialByteStreamBuilder
.newBuilder(R.class.getName())
.addPrimitiveField("x", int.class, 7)
.addPrimitiveField("y", int.class, 8)
.build();
var deser2 = deserialize(builderBytes);
assertEquals(deser2, deser1);
builderBytes = SerialByteStreamBuilder
.newBuilder(R.class.getName())
.addPrimitiveField("y", int.class, 8) // reverse order
.addPrimitiveField("x", int.class, 7)
.build();
deser2 = deserialize(builderBytes);
assertEquals(deser2, deser1);
builderBytes = SerialByteStreamBuilder
.newBuilder(R.class.getName())
.addPrimitiveField("w", int.class, 6) // additional fields
.addPrimitiveField("x", int.class, 7)
.addPrimitiveField("y", int.class, 8)
.addPrimitiveField("z", int.class, 9) // additional fields
.build();
deser2 = deserialize(builderBytes);
assertEquals(deser2, deser1);
r = new R(0, 0);
OOSBytes = serialize(r);
deser1 = deserialize(OOSBytes);
assertEquals(deser1, r);
builderBytes = SerialByteStreamBuilder
.newBuilder(R.class.getName())
.addPrimitiveField("y", int.class, 0)
.addPrimitiveField("x", int.class, 0)
.build();
deser2 = deserialize(builderBytes);
assertEquals(deser2, deser1);
builderBytes = SerialByteStreamBuilder
.newBuilder(R.class.getName()) // no field values
.build();
deser2 = deserialize(builderBytes);
assertEquals(deser2, deser1);
}
}
@Test
public void testString() throws Exception {
out.println("\n---");
record Str(String part1, String part2) implements Serializable {}
var r = new Str("Hello", "World!");
var deser1 = deserialize(serialize(r));
assertEquals(deser1, r);
byte[] builderBytes = SerialByteStreamBuilder
.newBuilder(Str.class.getName())
.addField("part1", String.class, "Hello")
.addField("part2", String.class, "World!")
.build();
var deser2 = deserialize(builderBytes);
assertEquals(deser2, deser1);
builderBytes = SerialByteStreamBuilder
.newBuilder(Str.class.getName())
.addField("cruft", String.class, "gg")
.addField("part1", String.class, "Hello")
.addField("part2", String.class, "World!")
.addPrimitiveField("x", int.class, 13)
.build();
var deser3 = deserialize(builderBytes);
assertEquals(deser3, deser1);
}
@Test
public void testArrays() throws Exception {
out.println("\n---");
{
record IntArray(int[]ints, long[]longs) implements Serializable {}
IntArray r = new IntArray(new int[]{5, 4, 3, 2, 1}, new long[]{9L});
IntArray deser1 = deserialize(serialize(r));
assertEquals(deser1.ints(), r.ints());
assertEquals(deser1.longs(), r.longs());
byte[] builderBytes = SerialByteStreamBuilder
.newBuilder(IntArray.class.getName())
.addField("ints", int[].class, new int[]{5, 4, 3, 2, 1})
.addField("longs", long[].class, new long[]{9L})
.build();
IntArray deser2 = deserialize(builderBytes);
assertEquals(deser2.ints(), deser1.ints());
assertEquals(deser2.longs(), deser1.longs());
}
{
record StrArray(String[]stringArray) implements Serializable {}
StrArray r = new StrArray(new String[]{"foo", "bar"});
StrArray deser1 = deserialize(serialize(r));
assertEquals(deser1.stringArray(), r.stringArray());
byte[] builderBytes = SerialByteStreamBuilder
.newBuilder(StrArray.class.getName())
.addField("stringArray", String[].class, new String[]{"foo", "bar"})
.build();
StrArray deser2 = deserialize(builderBytes);
assertEquals(deser2.stringArray(), deser1.stringArray());
}
}
@Test
public void testCompatibleFieldTypeChange() throws Exception {
out.println("\n---");
{
record NumberHolder(Number n) implements Serializable {}
var r = new NumberHolder(123);
var deser1 = deserialize(serialize(r));
assertEquals(deser1, r);
byte[] builderBytes = SerialByteStreamBuilder
.newBuilder(NumberHolder.class.getName())
.addField("n", Integer.class, 123)
.build();
var deser2 = deserialize(builderBytes);
assertEquals(deser2, deser1);
}
{
record IntegerHolder(Integer i) implements Serializable {}
var r = new IntegerHolder(123);
var deser1 = deserialize(serialize(r));
assertEquals(deser1, r);
byte[] builderBytes = SerialByteStreamBuilder
.newBuilder(IntegerHolder.class.getName())
.addField("i", Number.class, 123)
.build();
var deser2 = deserialize(builderBytes);
assertEquals(deser2, deser1);
}
}
@Test
public void testIncompatibleRefFieldTypeChange() throws Exception {
out.println("\n---");
record StringHolder(String s) implements Serializable {}
var r = new StringHolder("123");
var deser1 = deserialize(serialize(r));
assertEquals(deser1, r);
byte[] builderBytes = SerialByteStreamBuilder
.newBuilder(StringHolder.class.getName())
.addField("s", Integer.class, 123)
.build();
try {
var deser2 = deserialize(builderBytes);
throw new AssertionError(
"Unexpected success of deserialization. Deserialized value: " + deser2);
} catch (InvalidObjectException e) {
// expected
}
}
@Test
public void testIncompatiblePrimitiveFieldTypeChange() throws Exception {
out.println("\n---");
record IntHolder(int i) implements Serializable {}
var r = new IntHolder(123);
var deser1 = deserialize(serialize(r));
assertEquals(deser1, r);
byte[] builderBytes = SerialByteStreamBuilder
.newBuilder(IntHolder.class.getName())
.addPrimitiveField("i", long.class, 123L)
.build();
try {
var deser2 = deserialize(builderBytes);
throw new AssertionError(
"Unexpected success of deserialization. Deserialized value: " + deser2);
} catch (InvalidClassException e) {
// expected
}
}
<T> byte[] serialize(T obj) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
return baos.toByteArray();
}
@SuppressWarnings("unchecked")
static <T> T deserialize(byte[] streamBytes)
throws IOException, ClassNotFoundException {
ByteArrayInputStream bais = new ByteArrayInputStream(streamBytes);
ObjectInputStream ois = new ObjectInputStream(bais);
return (T) ois.readObject();
}
static class SerialByteStreamBuilder {
private final ObjectOutputStream objectOutputStream;
private final ByteArrayOutputStream byteArrayOutputStream;
record NameAndType<T>(String name, Class<T>type) {}
private String className;
private final LinkedHashMap<NameAndType<?>, Object> primFields = new LinkedHashMap<>();
private final LinkedHashMap<NameAndType<?>, Object> objectFields = new LinkedHashMap<>();
private SerialByteStreamBuilder() {
try {
byteArrayOutputStream = new ByteArrayOutputStream();
objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static SerialByteStreamBuilder newBuilder(String className) {
return (new SerialByteStreamBuilder()).className(className);
}
private SerialByteStreamBuilder className(String className) {
this.className = className;
return this;
}
public <T> SerialByteStreamBuilder addPrimitiveField(String name, Class<T> type, T value) {
if (!type.isPrimitive())
throw new IllegalArgumentException("Unexpected non-primitive field: " + type);
primFields.put(new NameAndType<>(name, type), value);
return this;
}
public <T> SerialByteStreamBuilder addField(String name, Class<T> type, T value) {
if (type.isPrimitive())
throw new IllegalArgumentException("Unexpected primitive field: " + type);
objectFields.put(new NameAndType<>(name, type), value);
return this;
}
private static int getPrimitiveSignature(Class<?> cl) {
if (cl == Integer.TYPE) return 'I';
else if (cl == Byte.TYPE) return 'B';
else if (cl == Long.TYPE) return 'J';
else if (cl == Float.TYPE) return 'F';
else if (cl == Double.TYPE) return 'D';
else if (cl == Short.TYPE) return 'S';
else if (cl == Character.TYPE) return 'C';
else if (cl == Boolean.TYPE) return 'Z';
else throw new InternalError();
}
private static void writeUTF(DataOutputStream out, String str) throws IOException {
int utflen = str.length(); // assume ASCII
assert utflen <= 0xFFFF;
out.writeShort(utflen);
out.writeBytes(str);
}
private void writePrimFieldsDesc(DataOutputStream out) throws IOException {
for (Map.Entry<NameAndType<?>, Object> entry : primFields.entrySet()) {
assert entry.getKey().type() != void.class;
out.writeByte(getPrimitiveSignature(entry.getKey().type())); // prim_typecode
out.writeUTF(entry.getKey().name()); // fieldName
}
}
private void writePrimFieldsValues(DataOutputStream out) throws IOException {
for (Map.Entry<NameAndType<?>, Object> entry : primFields.entrySet()) {
Class<?> cl = entry.getKey().type();
Object value = entry.getValue();
if (cl == Integer.TYPE) out.writeInt((int) value);
else if (cl == Byte.TYPE) out.writeByte((byte) value);
else if (cl == Long.TYPE) out.writeLong((long) value);
else if (cl == Float.TYPE) out.writeFloat((float) value);
else if (cl == Double.TYPE) out.writeDouble((double) value);
else if (cl == Short.TYPE) out.writeShort((short) value);
else if (cl == Character.TYPE) out.writeChar((char) value);
else if (cl == Boolean.TYPE) out.writeBoolean((boolean) value);
else throw new InternalError();
}
}
private void writeObjectFieldDesc(DataOutputStream out) throws IOException {
for (Map.Entry<NameAndType<?>, Object> entry : objectFields.entrySet()) {
Class<?> cl = entry.getKey().type();
assert !cl.isPrimitive();
// obj_typecode
if (cl.isArray()) {
out.writeByte('[');
} else {
out.writeByte('L');
}
writeUTF(out, entry.getKey().name());
out.writeByte(TC_STRING);
writeUTF(out,
(cl.isArray() ? cl.getName() : "L" + cl.getName() + ";")
.replace('.', '/'));
}
}
private void writeObject(DataOutputStream out, Object value) throws IOException {
objectOutputStream.reset();
byteArrayOutputStream.reset();
objectOutputStream.writeUnshared(value);
out.write(byteArrayOutputStream.toByteArray());
}
private void writeObjectFieldValues(DataOutputStream out) throws IOException {
for (Map.Entry<NameAndType<?>, Object> entry : objectFields.entrySet()) {
Class<?> cl = entry.getKey().type();
assert !cl.isPrimitive();
if (cl == String.class) {
out.writeByte(TC_STRING);
writeUTF(out, (String) entry.getValue());
} else {
writeObject(out, entry.getValue());
}
}
}
private int numFields() {
return primFields.size() + objectFields.size();
}
public byte[] build() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.writeShort(STREAM_MAGIC);
dos.writeShort(STREAM_VERSION);
dos.writeByte(TC_OBJECT);
dos.writeByte(TC_CLASSDESC);
dos.writeUTF(className);
dos.writeLong(0L);
dos.writeByte(SC_SERIALIZABLE);
dos.writeShort(numFields()); // number of fields
writePrimFieldsDesc(dos);
writeObjectFieldDesc(dos);
dos.writeByte(TC_ENDBLOCKDATA); // no annotations
dos.writeByte(TC_NULL); // no superclasses
writePrimFieldsValues(dos);
writeObjectFieldValues(dos);
dos.close();
return baos.toByteArray();
} catch (IOException unexpected) {
throw new AssertionError(unexpected);
}
}
}
}

View File

@ -0,0 +1,174 @@
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
Sample runs on Ryzen 3700X:
before 8247532:
Benchmark (length) Mode Cnt Score Error Units
RecordDeserialization.deserializeClasses 10 avgt 10 8.382 : 0.013 us/op
RecordDeserialization.deserializeClasses 100 avgt 10 33.736 : 0.171 us/op
RecordDeserialization.deserializeClasses 1000 avgt 10 271.224 : 0.953 us/op
RecordDeserialization.deserializeRecords 10 avgt 10 58.606 : 0.446 us/op
RecordDeserialization.deserializeRecords 100 avgt 10 530.044 : 1.752 us/op
RecordDeserialization.deserializeRecords 1000 avgt 10 5335.624 : 44.942 us/op
after 8247532:
Benchmark (length) Mode Cnt Score Error Units
RecordDeserialization.deserializeClasses 10 avgt 10 8.681 : 0.155 us/op
RecordDeserialization.deserializeClasses 100 avgt 10 32.496 : 0.087 us/op
RecordDeserialization.deserializeClasses 1000 avgt 10 279.014 : 1.189 us/op
RecordDeserialization.deserializeRecords 10 avgt 10 8.537 : 0.032 us/op
RecordDeserialization.deserializeRecords 100 avgt 10 31.451 : 0.083 us/op
RecordDeserialization.deserializeRecords 1000 avgt 10 250.854 : 2.772 us/op
*/
package org.openjdk.bench.java.io;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
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.Warmup;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.UncheckedIOException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
/**
* A micro benchmark used to measure/compare the performance of
* de-serializing record(s) vs. classical class(es)
*/
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 10, time = 1)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
@Fork(value = 1, warmups = 0, jvmArgsAppend = "--enable-preview")
public class RecordDeserialization {
public record PointR(int x, int y) implements Serializable {}
public record LineR(PointR p1, PointR p2) implements Serializable {}
public static class PointC implements Serializable {
private final int x, y;
public PointC(int x, int y) {
this.x = x;
this.y = y;
}
}
public static class LineC implements Serializable {
private final PointC p1, p2;
public LineC(PointC p1, PointC p2) {
this.p1 = p1;
this.p2 = p2;
}
}
private byte[] lineRsBytes, lineCsBytes;
private static LineR newLineR() {
ThreadLocalRandom rnd = ThreadLocalRandom.current();
return new LineR(new PointR(rnd.nextInt(), rnd.nextInt()),
new PointR(rnd.nextInt(), rnd.nextInt()));
}
private static LineC newLineC() {
ThreadLocalRandom rnd = ThreadLocalRandom.current();
return new LineC(new PointC(rnd.nextInt(), rnd.nextInt()),
new PointC(rnd.nextInt(), rnd.nextInt()));
}
private static byte[] serialize(Object o) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
oos.close();
return baos.toByteArray();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static Object deserialize(byte[] bytes) {
try {
return new ObjectInputStream(new ByteArrayInputStream(bytes))
.readObject();
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
@Param({"10", "100", "1000"})
public int length;
@Setup(Level.Trial)
public void setup() {
LineR[] lineRs = IntStream
.range(0, length)
.mapToObj(i -> newLineR())
.toArray(LineR[]::new);
lineRsBytes = serialize(lineRs);
LineC[] lineCs = IntStream
.range(0, length)
.mapToObj(i -> newLineC())
.toArray(LineC[]::new);
lineCsBytes = serialize(lineCs);
}
@Benchmark
public Object deserializeRecords() {
return deserialize(lineRsBytes);
}
@Benchmark
public Object deserializeClasses() {
return deserialize(lineCsBytes);
}
}