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

@ -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);
}
}
}
}