-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Use proxy-based serialization for static modules #6877
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This avoids serializing any state of serializable singletons which would be discarded anyway after deserializing (SI-10412).
lrytz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGreatTM! Are there enough existing test to ensure deserialization gives the same instance?
| * readResolve. Here it is implemented for all objects which have | ||
| * no implementation and which are marked serializable (which is true | ||
| * for all case objects.) | ||
| * readResolve. Here we use a serialization proxy that prevents |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the comment is quite right, how about
/* If you serialize a singleton you will get an additional
* instance of the singleton, unless you implement
* special serialization logic. Here we use a serialization proxy that prevents
* serialization of state and will, on deserialization by replaced by the object
* via use of readResolve. This is done for all top level objects which extend
* `java.io.Serializable` (such as case objects)
*/
| } | ||
| def extras = { | ||
| if (needsReadResolve) { | ||
| if (needsModuleSerializationProxy) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do wonder if we should only generate this method if we are generating java class files. This is not required for js and native backends is it?
| clazz.isModuleClass | ||
| && clazz.isSerializable | ||
| && !hasConcreteImpl(nme.readResolve) | ||
| && !hasConcreteImpl(nme.writeReplace) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not the correct test.
We should be checking for a matching method, not a method with this name
It should not match def writeReplace(x:Int) = ???
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We would need to rework significantly to avoid do a semantic check. We did manage to do that for Companion.apply recently, but that was far from easy. I'd suggest we take that up as a separate change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
| if (needsModuleSerializationProxy) { | ||
| // Aha, I finally decoded the original comment. | ||
| // This method should be generated as private, but apparently if it is, then | ||
| // it is name mangled afterward. (Wonder why that is.) So it's only protected. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment seems out of date - this method is (and was) private
is there some corresponding "don't mangle this name" setting somewhere
|
we also needs tests for the negative cases, where we don't generate the custom logic, e.g. if there is a custom writeReplace already, or this in not a top level object |
|
Should we do this in two steps? Publish a new STARR, then make the changes to the library? |
|
I think that there is a missing implementation for non top-level objects. They should be a singleton in their enclosing scope In impemlementation - Shouldn't they defer to a field of an outer enclosing object? from this test code |
|
Can we use M5 as the new STARR? This sounds like something that should go in before the RC cycle, if we do it. |
| @SerialVersionUID(1L) | ||
| final class ModuleSerializationProxy(cls: Class[_]) extends Serializable { | ||
| private[this] def readResolve: AnyRef = cls.getField("MODULE$").get(null) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could extend this to cache the looked-up instances:
package scala.runtime;
public final class SerializedObject {
private final Class<?> moduleClass;
private static final ClassValue<Object> instances = new ClassValue<Object>() {
@Override
protected Object computeValue(Class<?> type) {
try {
return type.getField("MODULE$").get(null);
} catch (IllegalAccessException | NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
};
public SerializedObject(Class<?> moduleClass) {
this.moduleClass = moduleClass;
}
private Object readResolve() {
return instances.get(moduleClass);
}
}The lookup in the ClassValue hashmap ought to be a fraction faster than the getField / get calls (although we'd need to benchmark to see if this matters.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would need to be classpath aware, or use WeakReference/WeakHashMap, otherwise it prevents class unloading
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. I think ClassValue is weak in all the right places, but we should validate with a test case that dynamically loads an unloads classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the performance difference:
[info] Benchmark Mode Cnt Score Error Units
[info] ReflectBenchmark.getField avgt 20 3129.835 ± 54.981 ns/op
[info] ReflectBenchmark.getFieldCached avgt 20 153.567 ± 13.023 ns/op
given:
package scala
import java.util.concurrent.TimeUnit
import org.openjdk.jmh.annotations._
import org.openjdk.jmh.infra._
object O1; object O2; object O3; object O4; object O5; object O6; object O7; object O8; object O9; object O10; object O11; object O12; object O13; object O14; object O15; object O16; object O17; object O18; object O19; object O20; object O21; object O22; object O23; object O24; object O25; object O26; object O27; object O28; object O29; object O30; object O31; object O32
@BenchmarkMode(Array(Mode.AverageTime))
@Fork(2)
@Threads(1)
@Warmup(iterations = 10)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
class ReflectBenchmark {
val classes = Array(O1.getClass, O2.getClass, O3.getClass, O4.getClass, O5.getClass, O6.getClass, O7.getClass, O8.getClass, O9.getClass, O10.getClass, O11.getClass, O12.getClass, O13.getClass, O14.getClass, O15.getClass, O16.getClass, O17.getClass, O18.getClass, O19.getClass, O20.getClass, O21.getClass, O22.getClass, O23.getClass, O24.getClass, O25.getClass, O26.getClass, O27.getClass, O28.getClass, O29.getClass, O30.getClass, O31.getClass, O32.getClass)
val cache = new ClassValue[AnyRef] {
override def computeValue(cls: Class[_]): AnyRef = {
cls.getField("MODULE$").get(null)
}
}
//////////////////////////////////////////////
@Benchmark def getField(bh: Blackhole): Unit = {
val cls = classes
var i = 0
while (i < cls.length) {
bh.consume(cls(i).getField("MODULE$").get(null))
i += 1
}
}
@Benchmark def getFieldCached(bh: Blackhole): Unit = {
val cls = classes
var i = 0
while (i < cls.length) {
bh.consume(cache.get(cls(i)))
i += 1
}
}
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good on the weak value, front, too.
scala> def cl = new java.net.URLClassLoader(Array(new java.io.File("/tmp").toURI.toURL), getClass.getClassLoader)
cl: java.net.URLClassLoader
scala> val cache = new ClassValue[AnyRef] { def computeValue(cls: Class[_]) = cl.loadClass("O$").getField("MODULE$").get(null) }
cache: ClassValue[AnyRef] = $anon$1@1de0641b
scala> for (i <- (1 to 1000000)) {val cls = cl.loadClass("O$"); println(cache.get(cls)); System.gc(); }
O$@714e861f
O$@2fdf22f7
[Unloading class O$ 0x00000007c05b3428]
[Unloading class O$ 0x00000007c05b2c28]
O$@57e03347
[Unloading class O$ 0x00000007c05b4428]
[Unloading class O$ 0x00000007c05b3c28]
O$@b8a144e
[Unloading class O$ 0x00000007c05b2c28]
[Unloading class O$ 0x00000007c05b3428]
O$@14b8a751
[Unloading class O$ 0x00000007c05b3c28]
[Unloading class O$ 0x00000007c05b4428]
O$@150d6eaf
[Unloading class O$ 0x00000007c05b3428]
[Unloading class O$ 0x00000007c05b2c28]
O$@6615237
$ scala
Welcome to Scala 2.12.4 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_172).
Type in expressions for evaluation. Or try :help.
scala> def cl = new java.net.URLClassLoader(Array(new java.io.File("/tmp").toURI.toURL), getClass.getClassLoader)
cl: java.net.URLClassLoader
scala> val cache = new ClassValue[AnyRef] { def computeValue(cls: Class[_]) = cl.loadClass("O$").getField("MODULE$").get(null) }
cache: ClassValue[AnyRef] = $anon$1@3728a578
scala> while (true) {val cls = cl.loadClass("O$"); cache.get(cls); System.gc(); print(".") }
....................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
|
Replaced by #7297 |
This avoids serializing any state of serializable singletons which
would be discarded anyway after deserializing (SI-10412).
Fixes scala/bug#10412