Introduce reflection cache in KClassImpl and KPackageImpl

Most of KClassImpl operations should be cached, but creating a lazy value for
each operation would significantly increase memory footprint of a KClassImpl
instance. Therefore we decide to store all lazy values under a single lazy
value named 'data' which is stored in KClassImpl. There's a minor overhead of
indirection on any operation now, however it'll allow us to substantially
increase reflection performance by caching everything we can
This commit is contained in:
Alexander Udalov
2016-08-17 15:56:08 +03:00
parent f6b471ac01
commit b9067e6462
4 changed files with 119 additions and 64 deletions
@@ -20,28 +20,48 @@ import junit.framework.TestCase
import org.jetbrains.kotlin.codegen.forTestCompile.ForTestCompileRuntime
import org.jetbrains.kotlin.load.java.JvmAbi
import java.lang.reflect.Field
import java.lang.reflect.Modifier
import kotlin.reflect.jvm.javaField
class ReflectionCodeSanityTest : TestCase() {
fun testNoDelegatedPropertiesInKClassAndKProperties() {
val classLoader = ForTestCompileRuntime.runtimeAndReflectJarClassLoader()
private lateinit var classLoader: ClassLoader
val classesToCheck = linkedSetOf<Class<*>>()
override fun setUp() {
super.setUp()
classLoader = ForTestCompileRuntime.runtimeAndReflectJarClassLoader()
}
override fun tearDown() {
ReflectionCodeSanityTest::classLoader.javaField!!.set(this, null)
super.tearDown()
}
private fun loadClass(name: String): Class<*> =
classLoader.loadClass("kotlin.reflect.jvm.internal.$name")
private fun collectClassesWithSupers(vararg names: String): Set<Class<*>> {
val result = linkedSetOf<Class<*>>()
fun addClassToCheck(klass: Class<*>) {
if (classesToCheck.add(klass)) {
@Suppress("UNNECESSARY_SAFE_CALL") // https://youtrack.jetbrains.com/issue/KT-9294
klass.superclass?.let { addClassToCheck(it) }
if (result.add(klass)) {
klass.superclass?.let(::addClassToCheck)
}
}
fun addClass(name: String) = addClassToCheck(classLoader.loadClass("kotlin.reflect.jvm.internal.$name"))
for (name in names) {
addClassToCheck(loadClass(name))
}
addClass("KClassImpl")
addClass("KMutableProperty0Impl")
addClass("KMutableProperty1Impl")
addClass("KMutableProperty2Impl")
return result
}
fun testNoDelegatedPropertiesInKClassAndKProperties() {
val badFields = linkedSetOf<Field>()
for (klass in classesToCheck) {
for (klass in collectClassesWithSupers(
"KClassImpl",
"KMutableProperty0Impl",
"KMutableProperty1Impl",
"KMutableProperty2Impl"
)) {
badFields.addAll(klass.declaredFields.filter { it.name.endsWith(JvmAbi.DELEGATED_PROPERTY_NAME_SUFFIX) })
}
@@ -54,4 +74,35 @@ class ReflectionCodeSanityTest : TestCase() {
badFields.joinToString("\n"))
}
}
fun testMaxAllowedFields() {
// The following classes are instantiated a lot in Kotlin applications and thus they should be optimized as best as possible.
// This test checks that these classes have not more fields than a predefined small number, which can usually be calculated as
// the number of constructor parameters (number of objects needed to initialize an instance) + 1 for 'data', the reflection cache.
val classesWithMaxAllowedFields = linkedMapOf(
"KClassImpl" to 2, // jClass, data
"KPackageImpl" to 3 // jClass, moduleName, data
)
val badClasses = linkedMapOf<Class<*>, Collection<Field>>()
for ((className, maxAllowedFields) in classesWithMaxAllowedFields) {
val klass = loadClass(className)
val fields = generateSequence(klass) { it.superclass }
.flatMap { it.declaredFields.asSequence() }
.filterNot { Modifier.isStatic(it.modifiers) }
.toList()
if (fields.size > maxAllowedFields) {
badClasses[klass] = fields
}
}
if (badClasses.isNotEmpty()) {
fail("Some classes in reflection.jvm contain more fields than it is allowed. Please optimize storage in these classes:\n\n" +
badClasses.entries.joinToString("\n") { entry ->
val (klass, fields) = entry
"$klass has ${fields.size} fields but max allowed = ${classesWithMaxAllowedFields[klass.simpleName]}:\n" +
fields.joinToString("\n") { " $it" }
})
}
}
}
@@ -35,18 +35,36 @@ import kotlin.reflect.*
internal class KClassImpl<T : Any>(override val jClass: Class<T>) :
KDeclarationContainerImpl(), KClass<T>, KClassifierImpl, KAnnotatedElementImpl {
private val descriptor_ = ReflectProperties.lazySoft {
val classId = classId
private inner class Data : KDeclarationContainerImpl.Data() {
val descriptor: ClassDescriptor by ReflectProperties.lazySoft {
val classId = classId
val moduleData = data().moduleData
val descriptor =
if (classId.isLocal) moduleData.deserialization.deserializeClass(classId)
else moduleData.module.findClassAcrossModuleDependencies(classId)
val descriptor =
if (classId.isLocal) moduleData.deserialization.deserializeClass(classId)
else moduleData.module.findClassAcrossModuleDependencies(classId)
descriptor ?: reportUnresolvedClass()
descriptor ?: reportUnresolvedClass()
}
@Suppress("UNCHECKED_CAST")
val objectInstance: T? by ReflectProperties.lazy {
val descriptor = descriptor
if (descriptor.kind != ClassKind.OBJECT) return@lazy null
val field = if (descriptor.isCompanionObject && !CompanionObjectMapping.isMappedIntrinsicCompanionObject(descriptor)) {
jClass.enclosingClass.getDeclaredField(descriptor.name.asString())
}
else {
jClass.getDeclaredField(JvmAbi.INSTANCE_FIELD)
}
field.get(null) as T
}
}
override val descriptor: ClassDescriptor
get() = descriptor_()
private val data = ReflectProperties.lazy { Data() }
override val descriptor: ClassDescriptor get() = data().descriptor
override val annotated: Annotated get() = descriptor
@@ -70,7 +88,6 @@ internal class KClassImpl<T : Any>(override val jClass: Class<T>) :
return emptyList()
}
@Suppress("UNCHECKED_CAST")
override fun getProperties(name: Name): Collection<PropertyDescriptor> =
(memberScope.getContributedVariables(name, NoLookupLocation.FROM_REFLECTION) +
staticScope.getContributedVariables(name, NoLookupLocation.FROM_REFLECTION))
@@ -123,22 +140,7 @@ internal class KClassImpl<T : Any>(override val jClass: Class<T>) :
jClass?.let { KClassImpl(it) }
}
@Suppress("UNCHECKED_CAST")
private val objectInstance_ = ReflectProperties.lazy {
val descriptor = descriptor
if (descriptor.kind != ClassKind.OBJECT) return@lazy null
val field = if (descriptor.isCompanionObject && !CompanionObjectMapping.isMappedIntrinsicCompanionObject(descriptor)) {
jClass.enclosingClass.getDeclaredField(descriptor.name.asString())
}
else {
jClass.getDeclaredField(JvmAbi.INSTANCE_FIELD)
}
field.get(null) as T
}
override val objectInstance: T?
get() = objectInstance_()
override val objectInstance: T? get() = data().objectInstance
override fun isInstance(value: Any?): Boolean {
// TODO: use Kotlin semantics for mutable/read-only collections once KT-11754 is supported (see TypeIntrinsics)
@@ -31,14 +31,13 @@ import kotlin.jvm.internal.ClassBasedDeclarationContainer
import kotlin.reflect.KotlinReflectionInternalError
internal abstract class KDeclarationContainerImpl : ClassBasedDeclarationContainer {
// Note: this is stored here on a soft reference to prevent GC from destroying the weak reference to it in the moduleByClassLoader cache
private val moduleData_ = ReflectProperties.lazySoft {
jClass.getOrCreateModule()
protected abstract inner class Data {
// This is stored here on a soft reference to prevent GC from destroying the weak reference to it in the moduleByClassLoader cache
val moduleData: RuntimeModuleData by ReflectProperties.lazySoft {
jClass.getOrCreateModule()
}
}
val moduleData: RuntimeModuleData
get() = moduleData_()
protected open val methodOwner: Class<*>
get() = jClass
@@ -16,10 +16,7 @@
package kotlin.reflect.jvm.internal
import org.jetbrains.kotlin.descriptors.ConstructorDescriptor
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.load.java.lazy.descriptors.LazyJavaPackageFragment
import org.jetbrains.kotlin.load.java.structure.reflect.classId
@@ -31,27 +28,33 @@ import org.jetbrains.kotlin.serialization.deserialization.descriptors.Deserializ
import kotlin.reflect.KCallable
internal class KPackageImpl(override val jClass: Class<*>, val moduleName: String) : KDeclarationContainerImpl() {
private val descriptor = ReflectProperties.lazySoft {
with(moduleData) {
packageFacadeProvider.registerModule(moduleName)
module.getPackage(jClass.classId.packageFqName)
private inner class Data : KDeclarationContainerImpl.Data() {
val descriptor: PackageViewDescriptor by ReflectProperties.lazySoft {
with(moduleData) {
packageFacadeProvider.registerModule(moduleName)
module.getPackage(jClass.classId.packageFqName)
}
}
val methodOwner: Class<*> by ReflectProperties.lazy {
val facadeName = ReflectKotlinClass.create(jClass)?.classHeader?.multifileClassName
// We need to check isNotEmpty because this is the value read from the annotation which cannot be null.
// The default value for 'xs' is empty string, as declared in kotlin.Metadata
// TODO: do not read ReflectKotlinClass multiple times, obtain facade name from descriptor
if (facadeName != null && facadeName.isNotEmpty()) {
jClass.classLoader.loadClass(facadeName.replace('/', '.'))
}
else {
jClass
}
}
}
override val methodOwner: Class<*> by lazy(LazyThreadSafetyMode.PUBLICATION) {
val facadeName = ReflectKotlinClass.create(jClass)?.classHeader?.multifileClassName
// We need to check isNotEmpty because this is the value read from the annotation which cannot be null.
// The default value for 'xs' is empty string, as declared in kotlin.Metadata
// TODO: do not read ReflectKotlinClass multiple times, obtain facade name from descriptor
if (facadeName != null && facadeName.isNotEmpty()) {
jClass.classLoader.loadClass(facadeName.replace('/', '.'))
}
else {
jClass
}
}
private val data = ReflectProperties.lazy { Data() }
internal val scope: MemberScope get() = descriptor().memberScope
override val methodOwner: Class<*> get() = data().methodOwner
private val scope: MemberScope get() = data().descriptor.memberScope
override val members: Collection<KCallable<*>>
get() = getMembers(scope, declaredOnly = false, nonExtensions = true, extensions = true).filter { member ->