From 4b7d3bedf101c54f4af8c7874e7776d9e8302299 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 2 Aug 2022 17:27:28 +0200 Subject: [PATCH 1/2] ~example --- .../kotlinx/benchmarks/json/CitmBenchmark.kt | 23 ++++++-------- .../src/kotlinx/serialization/Serializers.kt | 10 ++++-- .../serialization/internal/Platform.kt | 31 ++++++++++++++++--- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/CitmBenchmark.kt b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/CitmBenchmark.kt index 1ba896684..319a8fd58 100644 --- a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/CitmBenchmark.kt +++ b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/CitmBenchmark.kt @@ -8,28 +8,23 @@ import kotlinx.serialization.json.Json.Default.encodeToString import org.openjdk.jmh.annotations.* import java.util.concurrent.* -@Warmup(iterations = 7, time = 1) -@Measurement(iterations = 7, time = 1) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) @State(Scope.Benchmark) @Fork(2) open class CitmBenchmark { - /* - * For some reason Citm is kind of de-facto standard cross-language benchmark. - * Order of magnitude: 200 ops/sec - */ - private val input = CitmBenchmark::class.java.getResource("/citm_catalog.json").readBytes().decodeToString() - private val citm = Json.decodeFromString(CitmCatalog.serializer(), input) - @Setup - fun init() { - require(citm == Json.decodeFromString(CitmCatalog.serializer(), Json.encodeToString(citm))) - } + @Serializable + data class Foo(val a: Int) + + @Serializable + object Object @Benchmark - fun decodeCitm(): CitmCatalog = Json.decodeFromString(CitmCatalog.serializer(), input) + fun objectS() = serializer() @Benchmark - fun encodeCitm(): String = Json.encodeToString(CitmCatalog.serializer(), citm) + fun dataS() = serializer() } diff --git a/core/commonMain/src/kotlinx/serialization/Serializers.kt b/core/commonMain/src/kotlinx/serialization/Serializers.kt index ff35a9f15..df75019c9 100644 --- a/core/commonMain/src/kotlinx/serialization/Serializers.kt +++ b/core/commonMain/src/kotlinx/serialization/Serializers.kt @@ -77,8 +77,7 @@ private fun SerializersModule.serializerByKTypeImpl( ): KSerializer? { val rootClass = type.kclass() val isNullable = type.isMarkedNullable - val typeArguments = type.arguments - .map { requireNotNull(it.type) { "Star projections in type arguments are not allowed, but had $type" } } + val typeArguments = mapTypeArguments(type) val result: KSerializer? = when { typeArguments.isEmpty() -> rootClass.serializerOrNull() ?: getContextual(rootClass) else -> builtinSerializer(typeArguments, rootClass, failOnMissingTypeArgSerializer) @@ -86,6 +85,13 @@ private fun SerializersModule.serializerByKTypeImpl( return result?.nullable(isNullable) } +private fun mapTypeArguments(type: KType): List { + val args = type.arguments + if (args.isEmpty()) return emptyList() + return args + .map { requireNotNull(it.type) { "Star projections in type arguments are not allowed, but had $type" } } +} + @OptIn(ExperimentalSerializationApi::class) private fun SerializersModule.builtinSerializer( typeArguments: List, diff --git a/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt b/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt index 7c16650b8..57072d0f8 100644 --- a/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt @@ -6,6 +6,10 @@ package kotlinx.serialization.internal import kotlinx.serialization.* import java.lang.reflect.* +import java.util.WeakHashMap +import java.util.concurrent.locks.ReadWriteLock +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.* import kotlin.reflect.* @Suppress("NOTHING_TO_INLINE") @@ -39,8 +43,27 @@ internal actual fun KClass.constructSerializerForGivenTypeArgs(vara return java.constructSerializerForGivenTypeArgs(*args) } +private val adhocCache = WeakHashMap, KSerializer>() +private val rwLock = ReentrantReadWriteLock() + +@Suppress("UNCHECKED_CAST") +internal fun Class.constructSerializerForGivenTypeArgs(vararg args: KSerializer): KSerializer? { + if (args.isNotEmpty()) return constructSerializer(args) + + rwLock.read { + var serializer = adhocCache[this] + if (serializer != null) return serializer.cast() + // TODO avoid var, properly cache 'null', properly handle nonempty args + serializer = constructSerializer(args)?.cast() + rwLock.write { + adhocCache[this] = serializer + } + return serializer?.cast() + } +} + @Suppress("UNCHECKED_CAST") -internal fun Class.constructSerializerForGivenTypeArgs(vararg args: KSerializer): KSerializer? { +private fun Class.constructSerializer(args: Array>): KSerializer? { if (isEnum && isNotAnnotated()) { return createEnumSerializer() } @@ -64,7 +87,7 @@ internal fun Class.constructSerializerForGivenTypeArgs(vararg args: return polymorphicSerializer() } -private fun Class.isNotAnnotated(): Boolean { +private fun Class.isNotAnnotated(): Boolean { /* * For annotated enums search serializer directly (or do not search at all?) */ @@ -72,7 +95,7 @@ private fun Class.isNotAnnotated(): Boolean { getAnnotation(Polymorphic::class.java) == null } -private fun Class.polymorphicSerializer(): KSerializer? { +private fun Class.polymorphicSerializer(): KSerializer? { /* * Last resort: check for @Polymorphic or Serializable(with = PolymorphicSerializer::class) * annotations. @@ -87,7 +110,7 @@ private fun Class.polymorphicSerializer(): KSerializer? { return null } -private fun Class.interfaceSerializer(): KSerializer? { +private fun Class.interfaceSerializer(): KSerializer? { /* * Interfaces are @Polymorphic by default. * Check if it has no annotations or `@Serializable(with = PolymorphicSerializer::class)`, From 4e79de9145b61a432d4103d2f42dd10a90ebd097 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 2 Aug 2022 17:48:47 +0200 Subject: [PATCH 2/2] Add baseline benchmark --- .../kotlinx/benchmarks/json/CitmBenchmark.kt | 23 ++++++---- .../json/LookupOverheadBenchmark.kt | 45 +++++++++++++++++++ 2 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt diff --git a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/CitmBenchmark.kt b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/CitmBenchmark.kt index 319a8fd58..1ba896684 100644 --- a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/CitmBenchmark.kt +++ b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/CitmBenchmark.kt @@ -8,23 +8,28 @@ import kotlinx.serialization.json.Json.Default.encodeToString import org.openjdk.jmh.annotations.* import java.util.concurrent.* -@Warmup(iterations = 5, time = 1) -@Measurement(iterations = 5, time = 1) +@Warmup(iterations = 7, time = 1) +@Measurement(iterations = 7, time = 1) @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) @State(Scope.Benchmark) @Fork(2) open class CitmBenchmark { + /* + * For some reason Citm is kind of de-facto standard cross-language benchmark. + * Order of magnitude: 200 ops/sec + */ + private val input = CitmBenchmark::class.java.getResource("/citm_catalog.json").readBytes().decodeToString() + private val citm = Json.decodeFromString(CitmCatalog.serializer(), input) - @Serializable - data class Foo(val a: Int) - - @Serializable - object Object + @Setup + fun init() { + require(citm == Json.decodeFromString(CitmCatalog.serializer(), Json.encodeToString(citm))) + } @Benchmark - fun objectS() = serializer() + fun decodeCitm(): CitmCatalog = Json.decodeFromString(CitmCatalog.serializer(), input) @Benchmark - fun dataS() = serializer() + fun encodeCitm(): String = Json.encodeToString(CitmCatalog.serializer(), citm) } diff --git a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt new file mode 100644 index 000000000..689fe3ceb --- /dev/null +++ b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt @@ -0,0 +1,45 @@ +package kotlinx.benchmarks.json + +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(val a: T) + + private val data = """{"a":""}""" + + @Serializable + object Object + + @Benchmark + fun dataReified() = Json.decodeFromString(data) + + @Benchmark + fun dataPlain() = Json.decodeFromString(Holder.serializer(), data) + + @Benchmark + fun genericReified() = Json.decodeFromString>(data) + + @Benchmark + fun genericPlain() = Json.decodeFromString(Generic.serializer(String.serializer()), data) + + @Benchmark + fun objectReified() = Json.decodeFromString("{}") + + @Benchmark + fun objectPlain() = Json.decodeFromString(Object.serializer(), "{}") +}