Skip to content

TreeJsonDecoder with useAlternativeNames=true is slow even if no alternative names exist #3193

Description

@Snipesy

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions