Describe the bug
While working on ohs-foundation/kotlin-fhir#83 I discovered using TreeJsonDecoder (which is used when polymorphic serialization fails the hot path) the performance of kotlinx serialization is extremely poor (roughly 2x to 4x slower than useAlternativeNames=false)
To Reproduce
@Serializable
data class WideClass(
val f00: String? = null,
val f01: String? = null,
val f02: String? = null,
val f03: String? = null,
val f04: String? = null,
val f05: String? = null,
val f06: String? = null,
val f07: String? = null,
val f08: String? = null,
val f09: String? = null,
val f10: String? = null,
val f11: String? = null,
val f12: String? = null,
val f13: String? = null,
val f14: String? = null,
val f15: String? = null,
val f16: String? = null,
val f17: String? = null,
val f18: String? = null,
val f19: String? = null,
val f20: String? = null,
val f21: String? = null,
val f22: String? = null,
val f23: String? = null,
val f24: String? = null,
val f25: String? = null,
val f26: String? = null,
val f27: String? = null,
val f28: String? = null,
val f29: String? = null,
)
fun main() {
// 5 of 30 fields populated. JSON has 5 keys; descriptor has 30 slots; 25 absent → 25 slow-path
// invocations per decode under default `useAlternativeNames = true`.
val sample =
"""
{"f00":"a","f01":"b","f02":"c","f03":"d","f04":"e"}
"""
.trimIndent()
val withAlternates = Json { useAlternativeNames = true }
val withoutAlternates = Json { useAlternativeNames = false }
val element = withAlternates.parseToJsonElement(sample)
val iterations = 5_000_000
// Warm both Json instances + JIT.
repeat(50_000) {
withAlternates.decodeFromJsonElement(WideClass.serializer(), element)
withoutAlternates.decodeFromJsonElement(WideClass.serializer(), element)
}
val withTime = measureTime {
repeat(iterations) {
withAlternates.decodeFromJsonElement(WideClass.serializer(), element)
}
}
val withoutTime = measureTime {
repeat(iterations) {
withoutAlternates.decodeFromJsonElement(WideClass.serializer(), element)
}
}
println()
println("════════════════════════════════════════════════════════")
println(" JsonTreeDecoder slow-path reproducer")
println("════════════════════════════════════════════════════════")
println(" Class: WideClass (30 nullable String fields, 0 @JsonNames)")
println(" JSON input: 5 keys present, 25 slots absent")
println(" Iterations: $iterations")
println()
println(" useAlternativeNames=true (default): ${withTime} = ${withTime.inWholeNanoseconds / iterations} ns/op")
println(" useAlternativeNames=false (skip slow): ${withoutTime} = ${withoutTime.inWholeNanoseconds / iterations} ns/op")
println()
val ratio = withTime.inWholeNanoseconds.toDouble() / withoutTime.inWholeNanoseconds.toDouble()
println(" Slowdown factor: %.2fx".format(ratio))
println("════════════════════════════════════════════════════════")
}
Expected behavior
Serialization should be fast :)
Actual behavior
Serialization is slow! :(
On my M3 Max Macbook pro useAlternativeNames = true is 3x slower than useAlternativeNames = false. Almost the entire time is spent doing O(n*m) lookups here:
|
value.keys.find { deserializationNamesMap[it] == index }?.let { |
Even though the model declares no alternative names. While it is easy to set useAlternativeNames=false, the default in Kotlin is true... And I think this is something that can easilly be cached.
Describe the bug
While working on ohs-foundation/kotlin-fhir#83 I discovered using TreeJsonDecoder (which is used when polymorphic serialization fails the hot path) the performance of kotlinx serialization is extremely poor (roughly 2x to 4x slower than
useAlternativeNames=false)To Reproduce
Expected behavior
Serialization should be fast :)
Actual behavior
Serialization is slow! :(
On my M3 Max Macbook pro
useAlternativeNames = trueis 3x slower thanuseAlternativeNames = false. Almost the entire time is spent doing O(n*m) lookups here:kotlinx.serialization/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt
Line 254 in 6956af2
Even though the model declares no alternative names. While it is easy to set
useAlternativeNames=false, the default in Kotlin is true... And I think this is something that can easilly be cached.