Skip to content
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

Implemented serializers caching for lookup #2015

Merged
merged 7 commits into from Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,69 @@
/*
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.benchmarks.json
shanshin marked this conversation as resolved.
Show resolved Hide resolved

import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.json.*
import org.openjdk.jmh.annotations.*
import java.util.concurrent.*

@Warmup(iterations = 7, time = 1)
@Measurement(iterations = 7, time = 1)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Fork(2)
open class LookupOverheadBenchmark {

@Serializable
class Holder(val a: String)

@Serializable
class Generic<T>(val a: T)

@Serializable
class DoubleGeneric<T1, T2>(val a: T1, val b: T2)

@Serializable
class PentaGeneric<T1, T2, T3, T4, T5>(val a: T1, val b: T2, val c: T3, val d: T4, val e: T5)

private val data = """{"a":""}"""
private val doubleData = """{"a":"","b":0}"""
private val pentaData = """{"a":"","b":0,"c":1,"d":true,"e":" "}"""

@Serializable
object Object

@Benchmark
fun dataReified() = Json.decodeFromString<Holder>(data)

@Benchmark
fun dataPlain() = Json.decodeFromString(Holder.serializer(), data)

@Benchmark
fun genericReified() = Json.decodeFromString<Generic<String>>(data)

@Benchmark
fun genericPlain() = Json.decodeFromString(Generic.serializer(String.serializer()), data)

@Benchmark
fun doubleGenericReified() = Json.decodeFromString<DoubleGeneric<String, Int>>(doubleData)

@Benchmark
fun doubleGenericPlain() = Json.decodeFromString(DoubleGeneric.serializer(String.serializer(), Int.serializer()), doubleData)

@Benchmark
fun pentaGenericReified() = Json.decodeFromString<PentaGeneric<String, Int, Long, Boolean, Char>>(pentaData)

@Benchmark
fun pentaGenericPlain() = Json.decodeFromString(PentaGeneric.serializer(String.serializer(), Int.serializer(), Long.serializer(), Boolean.serializer(), Char.serializer()), pentaData)

@Benchmark
fun objectReified() = Json.decodeFromString<Object>("{}")

@Benchmark
fun objectPlain() = Json.decodeFromString(Object.serializer(), "{}")
}
2 changes: 1 addition & 1 deletion build.gradle
Expand Up @@ -173,7 +173,7 @@ subprojects {
afterEvaluate { // Can be applied only when the project is evaluated
animalsniffer {
sourceSets = [sourceSets.main]
annotation = "kotlinx.serialization.json.internal.SuppressAnimalSniffer"
annotation = (name == "kotlinx-serialization-core")? "kotlinx.serialization.internal.SuppressAnimalSniffer" : "kotlinx.serialization.json.internal.SuppressAnimalSniffer"
shanshin marked this conversation as resolved.
Show resolved Hide resolved
}
dependencies {
signature 'net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature'
Expand Down
123 changes: 78 additions & 45 deletions core/commonMain/src/kotlinx/serialization/Serializers.kt
Expand Up @@ -66,9 +66,8 @@ public fun SerializersModule.serializer(type: KType): KSerializer<Any?> =
* Returns `null` if serializer cannot be created (provided [type] or its type argument is not serializable and is not registered in [this] module).
*/
@OptIn(ExperimentalSerializationApi::class)
public fun SerializersModule.serializerOrNull(type: KType): KSerializer<Any?>? {
return serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = false)
}
public fun SerializersModule.serializerOrNull(type: KType): KSerializer<Any?>? =
serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = false)

@OptIn(ExperimentalSerializationApi::class)
private fun SerializersModule.serializerByKTypeImpl(
Expand All @@ -79,54 +78,47 @@ private fun SerializersModule.serializerByKTypeImpl(
val isNullable = type.isMarkedNullable
val typeArguments = type.arguments
.map { requireNotNull(it.type) { "Star projections in type arguments are not allowed, but had $type" } }
val result: KSerializer<Any>? = when {
typeArguments.isEmpty() -> rootClass.serializerOrNull() ?: getContextual(rootClass)
else -> builtinSerializer(typeArguments, rootClass, failOnMissingTypeArgSerializer)
}?.cast()
return result?.nullable(isNullable)
}

@OptIn(ExperimentalSerializationApi::class)
private fun SerializersModule.builtinSerializer(
typeArguments: List<KType>,
rootClass: KClass<Any>,
failOnMissingTypeArgSerializer: Boolean
): KSerializer<out Any>? {
val serializers = if (failOnMissingTypeArgSerializer)
typeArguments.map(::serializer)
else {
typeArguments.map { serializerOrNull(it) ?: return null }
val cachedSerializer = if (typeArguments.isEmpty()) {
findCachedSerializer(rootClass, isNullable)
} else {
val cachedResult = findParametrizedCachedSerializer(rootClass, typeArguments, isNullable)
if (failOnMissingTypeArgSerializer) {
cachedResult.getOrNull()
} else {
// return null if error occurred - serializer for parameter(s) was not found
cachedResult.getOrElse { return null }
}
}
// Array is not supported, see KT-32839
return when (rootClass) {
Collection::class, List::class, MutableList::class, ArrayList::class -> ArrayListSerializer(serializers[0])
HashSet::class -> HashSetSerializer(serializers[0])
Set::class, MutableSet::class, LinkedHashSet::class -> LinkedHashSetSerializer(serializers[0])
HashMap::class -> HashMapSerializer(serializers[0], serializers[1])
Map::class, MutableMap::class, LinkedHashMap::class -> LinkedHashMapSerializer(
serializers[0],
serializers[1]
cachedSerializer?.let { return it }

// slow path to find contextual serializers in serializers module
val contextualSerializer: KSerializer<out Any?>? = if (typeArguments.isEmpty()) {
getContextual(rootClass)
} else {
val serializers = serializersForParameters(typeArguments, failOnMissingTypeArgSerializer) ?: return null
// first, we look among the built-in serializers, because the parameter could be contextual
rootClass.parametrizedSerializerOrNull(typeArguments, serializers) ?: getContextual(
rootClass,
serializers
)
Map.Entry::class -> MapEntrySerializer(serializers[0], serializers[1])
Pair::class -> PairSerializer(serializers[0], serializers[1])
Triple::class -> TripleSerializer(serializers[0], serializers[1], serializers[2])
else -> {
if (isReferenceArray(rootClass)) {
return ArraySerializer<Any, Any?>(typeArguments[0].classifier as KClass<Any>, serializers[0]).cast()
}
val args = serializers.toTypedArray()
rootClass.constructSerializerForGivenTypeArgs(*args)
?: reflectiveOrContextual(rootClass, serializers)
}
}
return contextualSerializer?.cast<Any>()?.nullable(isNullable)
}

@OptIn(ExperimentalSerializationApi::class)
internal fun <T : Any> SerializersModule.reflectiveOrContextual(
kClass: KClass<T>,
typeArgumentsSerializers: List<KSerializer<Any?>>
): KSerializer<T>? {
return kClass.serializerOrNull() ?: getContextual(kClass, typeArgumentsSerializers)
/**
* Returns null only if `failOnMissingTypeArgSerializer == false` and at least one parameter serializer not found.
*/
internal fun SerializersModule.serializersForParameters(
typeArguments: List<KType>,
failOnMissingTypeArgSerializer: Boolean
): List<KSerializer<Any?>>? {
val serializers = if (failOnMissingTypeArgSerializer) {
typeArguments.map { serializer(it) }
} else {
typeArguments.map { serializerOrNull(it) ?: return null }
}
return serializers
}

/**
Expand Down Expand Up @@ -179,6 +171,47 @@ public fun <T : Any> KClass<T>.serializer(): KSerializer<T> = serializerOrNull()
public fun <T : Any> KClass<T>.serializerOrNull(): KSerializer<T>? =
compiledSerializerImpl() ?: builtinSerializerOrNull()

internal fun KClass<Any>.parametrizedSerializerOrNull(
types: List<KType>,
serializers: List<KSerializer<Any?>>
): KSerializer<out Any>? {
// builtin first because some standard parametrized interfaces (e.g. Map) must use builtin serializer but not polymorphic
return builtinParametrizedSerializer(types, serializers) ?: compiledParametrizedSerializer(serializers)
}


private fun KClass<Any>.compiledParametrizedSerializer(serializers: List<KSerializer<Any?>>): KSerializer<out Any>? {
return constructSerializerForGivenTypeArgs(*serializers.toTypedArray())
}

@OptIn(ExperimentalSerializationApi::class)
private fun KClass<Any>.builtinParametrizedSerializer(
typeArguments: List<KType>,
serializers: List<KSerializer<Any?>>,
): KSerializer<out Any>? {
// Array is not supported, see KT-32839
return when (this) {
Collection::class, List::class, MutableList::class, ArrayList::class -> ArrayListSerializer(serializers[0])
HashSet::class -> HashSetSerializer(serializers[0])
Set::class, MutableSet::class, LinkedHashSet::class -> LinkedHashSetSerializer(serializers[0])
HashMap::class -> HashMapSerializer(serializers[0], serializers[1])
Map::class, MutableMap::class, LinkedHashMap::class -> LinkedHashMapSerializer(
serializers[0],
serializers[1]
)
Map.Entry::class -> MapEntrySerializer(serializers[0], serializers[1])
Pair::class -> PairSerializer(serializers[0], serializers[1])
Triple::class -> TripleSerializer(serializers[0], serializers[1], serializers[2])
else -> {
if (isReferenceArray(this)) {
ArraySerializer(typeArguments[0].classifier as KClass<Any>, serializers[0])
} else {
null
}
}
}
}

private fun <T : Any> KSerializer<T>.nullable(shouldBeNullable: Boolean): KSerializer<T?> {
if (shouldBeNullable) return nullable
return this as KSerializer<T?>
Expand Down
74 changes: 74 additions & 0 deletions core/commonMain/src/kotlinx/serialization/SerializersCache.kt
@@ -0,0 +1,74 @@
/*
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization

import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.internal.cast
import kotlinx.serialization.internal.createCache
import kotlinx.serialization.internal.createParametrizedCache
import kotlinx.serialization.modules.EmptySerializersModule
import kotlin.native.concurrent.ThreadLocal
import kotlin.reflect.KClass
import kotlin.reflect.KType


/**
* Cache for non-null non-parametrized and non-contextual serializers.
*/
@ThreadLocal
private val SERIALIZERS_CACHE = createCache { it.serializerOrNull() }

/**
* Cache for nullable non-parametrized and non-contextual serializers.
*/
@ThreadLocal
private val SERIALIZERS_CACHE_NULLABLE = createCache<Any?> { it.serializerOrNull()?.nullable?.cast() }

/**
* Cache for non-null parametrized and non-contextual serializers.
*/
@ThreadLocal
private val PARAMETRIZED_SERIALIZERS_CACHE = createParametrizedCache { clazz, types ->
val serializers = EmptySerializersModule().serializersForParameters(types, true)!!
clazz.parametrizedSerializerOrNull(types, serializers)
}

/**
* Cache for nullable parametrized and non-contextual serializers.
*/
@ThreadLocal
private val PARAMETRIZED_SERIALIZERS_CACHE_NULLABLE = createParametrizedCache<Any?> { clazz, types ->
val serializers = EmptySerializersModule().serializersForParameters(types, true)!!
clazz.parametrizedSerializerOrNull(types, serializers)?.nullable?.cast()
}

/**
* Find cacheable serializer in the cache.
* If serializer is cacheable but missed in cache - it will be created, placed into the cache and returned.
*/
internal fun findCachedSerializer(clazz: KClass<Any>, isNullable: Boolean): KSerializer<Any?>? {
return if (!isNullable) {
SERIALIZERS_CACHE.get(clazz)?.cast()
} else {
SERIALIZERS_CACHE_NULLABLE.get(clazz)
}
}

/**
* Find cacheable parametrized serializer in the cache.
* If serializer is cacheable but missed in cache - it will be created, placed into the cache and returned.
*/
internal fun findParametrizedCachedSerializer(
clazz: KClass<Any>,
types: List<KType>,
isNullable: Boolean
): Result<KSerializer<Any?>?> {
return if (!isNullable) {
@Suppress("UNCHECKED_CAST")
PARAMETRIZED_SERIALIZERS_CACHE.get(clazz, types) as Result<KSerializer<Any?>?>
} else {
PARAMETRIZED_SERIALIZERS_CACHE_NULLABLE.get(clazz, types)
}
}
Expand Up @@ -130,6 +130,18 @@ internal expect fun BooleanArray.getChecked(index: Int): Boolean

internal expect fun <T : Any> KClass<T>.compiledSerializerImpl(): KSerializer<T>?

/**
* Create serializers cache for non-parametrized and non-contextual serializers.
* The activity and type of cache is determined for a specific platform and a specific environment.
*/
internal expect fun <T> createCache(factory: (KClass<*>) -> KSerializer<T>?): SerializerCache<T>

/**
* Create serializers cache for parametrized and non-contextual serializers. Parameters also non-contextual.
* The activity and type of cache is determined for a specific platform and a specific environment.
*/
internal expect fun <T> createParametrizedCache(factory: (KClass<Any>, List<KType>) -> KSerializer<T>?): ParametrizedSerializerCache<T>

internal expect fun <T : Any, E : T?> ArrayList<E>.toNativeArrayImpl(eClass: KClass<T>): Array<E>

/**
Expand All @@ -145,3 +157,24 @@ internal expect fun Any.isInstanceOf(kclass: KClass<*>): Boolean
internal inline fun <T, K> Iterable<T>.elementsHashCodeBy(selector: (T) -> K): Int {
return fold(1) { hash, element -> 31 * hash + selector(element).hashCode() }
}

/**
* Cache class for non-parametrized and non-contextual serializers.
*/
internal interface SerializerCache<T> {
/**
* Returns cached serializer or `null` if serializer not found.
*/
fun get(key: KClass<Any>): KSerializer<T>?
}

/**
* Cache class for parametrized and non-contextual serializers.
*/
internal interface ParametrizedSerializerCache<T> {
/**
* Returns successful result with cached serializer or `null` if root serializer not found.
* If no serializer was found for the parameters, then result contains an exception.
*/
fun get(key: KClass<Any>, types: List<KType> = emptyList()): Result<KSerializer<T>?>
}
16 changes: 16 additions & 0 deletions core/jsMain/src/kotlinx/serialization/internal/Platform.kt
Expand Up @@ -20,6 +20,22 @@ internal actual fun BooleanArray.getChecked(index: Int): Boolean {
internal actual fun <T : Any> KClass<T>.compiledSerializerImpl(): KSerializer<T>? =
this.constructSerializerForGivenTypeArgs() ?: this.js.asDynamic().Companion?.serializer() as? KSerializer<T>

internal actual fun <T> createCache(factory: (KClass<*>) -> KSerializer<T>?): SerializerCache<T> {
return object: SerializerCache<T> {
override fun get(key: KClass<Any>): KSerializer<T>? {
return factory(key)
}
}
}

internal actual fun <T> createParametrizedCache(factory: (KClass<Any>, List<KType>) -> KSerializer<T>?): ParametrizedSerializerCache<T> {
return object: ParametrizedSerializerCache<T> {
override fun get(key: KClass<Any>, types: List<KType>): Result<KSerializer<T>?> {
return kotlin.runCatching { factory(key, types) }
}
}
}

internal actual fun <T : Any, E : T?> ArrayList<E>.toNativeArrayImpl(eClass: KClass<T>): Array<E> = toTypedArray()

internal actual fun Any.isInstanceOf(kclass: KClass<*>): Boolean = kclass.isInstance(this)
Expand Down