mirror of
https://github.com/openjdk/jdk.git
synced 2025-08-26 22:34:27 +02:00
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:
parent
4bcd70acc0
commit
2f09989ec0
5 changed files with 972 additions and 70 deletions
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue