TL;DR
-
moshi 1.12
から条件付きでKotlin
のクラスへのデシリアライズが高速化した - 条件は以下の通り
-
moshi-kotlin-codegen
を利用していて、デフォルト値が有りうるクラスを、引数全てが設定されたJSON
からデシリアライズする場合 -
moshi-kotlin
を利用していて、デフォルト値を用いず(= プロパティが全て揃った状態で)デシリアライズする場合
-
本文
moshi 1.12.0
でKotlin
のdata class
へのデシリアライズが条件付きで高速化したので、比較・解説していきます。
高速化する条件
高速化に関する変更はmoshi-kotlin-codegen
とmoshi-kotlin
それぞれに入っています。
この内moshi-kotlin-codegen
が高速化する条件は限定的なものですが、moshi-kotlin
が高速化する条件は緩く、多くの場合高速化すると思われます。
moshi-kotlin-codegenでのデシリアライズが高速化する条件
moshi-kotlin-codegen
でのデシリアライズが高速化するのは、デフォルト値が有りうるクラスを、引数全てが設定されたJSON
からデシリアライズする場合です。
例えば以下のようなクラスをデシリアライズするとします。
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Sample(val foo: Int, val bar: Int = 0)
このクラスを、foo
, bar
それぞれのプロパティを持つJSON
からデシリアライズすると高速化します。
{ "foo": 1, "bar": 1 }
以下のように、bar
がプロパティに存在しない(= デフォルト値が用いられる)場合は高速化しません。
{ "foo": 1 }
また、以下のように、そもそもデフォルト値が設定されていない場合も高速化しません。
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Sample(val foo: Int, val bar: Int)
moshi-kotlinでのデシリアライズが高速化する条件
moshi-kotlin
でのデシリアライズが高速化するのは、デフォルト値を用いずにデシリアライズを行う場合です。
例えば以下のようなクラスをデシリアライズするとします。
data class Sample(val foo: Int, val bar: Int)
このクラスを、foo
, bar
それぞれのプロパティを持つJSON
からデシリアライズすると高速化します。
{ "foo": 1, "bar": 1 }
moshi-kotlin
の場合、デフォルト値が設定されているかに関係なく、引数全てを揃えてデシリアライズした場合高速化します。
どれ位高速化するか
3引数・デフォルト値有りの条件でJMH
のベンチマークを作成し、同じベンチマークをmoshi 1.11.0
とmoshi 1.12.0
で実行して比較を行いました。
ベンチマークに用いたコードは以下の通りです。
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Codegen(
val foo: Int,
val bar: Int,
val baz: Int = 0
)
data class Reflect(
val foo: Int,
val bar: Int,
val baz: Int = 0
)
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.wrongwrong.dto.Codegen
import com.wrongwrong.dto.Reflect
import org.openjdk.jmh.annotations.*
import java.util.concurrent.ThreadLocalRandom
@State(Scope.Benchmark)
open class BenchMark {
private val moshiCodegenAdapter: JsonAdapter<Codegen> =
Moshi.Builder().build().adapter(Codegen::class.java)
private val moshiReflectAdapter: JsonAdapter<Reflect> =
Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build().adapter(Reflect::class.java)
private var targetJsonString: String? = null
@Setup(Level.Iteration)
fun setupTarget() {
val i = ThreadLocalRandom.current().nextInt(5)
// CodegenとReflectの内容は同じため、同じJSONからデシリアライズして比較できる
targetJsonString = moshiCodegenAdapter.toJson(Codegen(i, i + 1, i + 2))
}
@Benchmark
fun codegen() = moshiCodegenAdapter.fromJson(targetJsonString!!)
@Benchmark
fun reflect() = moshiReflectAdapter.fromJson(targetJsonString!!)
}
補足
JMH
にの実行に関する設定を含めたbuild.gradle.kts
は以下の通りです。
build.gradle.kts
plugins {
kotlin("jvm") version "1.4.32"
kotlin("kapt") version "1.4.32"
id("me.champeau.gradle.jmh") version "0.5.3"
}
group = "com.wrongwrong"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
jmhImplementation(kotlin("stdlib-jdk8"))
val moshiVersion = "1.11.0"
jmhImplementation("com.squareup.moshi:moshi:${moshiVersion}")
jmhImplementation("com.squareup.moshi:moshi-kotlin:${moshiVersion}")
kaptJmh("com.squareup.moshi:moshi-kotlin-codegen:${moshiVersion}")
}
tasks {
compileKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}
compileJmhKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}
compileTestKotlin {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "1.8"
}
}
// https://qiita.com/wrongwrong/items/16fa10a7f78a31830ed8
jmhJar {
exclude("META-INF/versions/9/module-info.class")
}
}
jmh {
warmupForks = 2
warmupBatchSize = 3
warmupIterations = 3
warmup = "1s"
fork = 2
batchSize = 3
iterations = 2
timeOnIteration = "1500ms"
failOnError = true
isIncludeTests = false
resultFormat = "CSV"
}
その他ベンチマークの作り方は以下を参照ください。
ベンチマーク結果
ベンチマーク結果は以下のようになりました。
codegen
はmoshi-kotlin-codegen
、reflect
はmoshi-kotlin
のスコアで、スコアは高いほど良いです。
Benchmark Mode Cnt Score Error Units
BenchMark.codegen thrpt 4 1018336.030 ± 55897.668 ops/s
BenchMark.reflect thrpt 4 949243.442 ± 106377.587 ops/s
Benchmark Mode Cnt Score Error Units
BenchMark.codegen thrpt 4 896153.959 ± 107707.744 ops/s
BenchMark.reflect thrpt 4 754958.982 ± 33874.740 ops/s
このベンチマークから見るそれぞれの高速化幅は以下の通りです。
-
codegen
-> 1018336.030/896153.959 = 1.1倍 -
reflect
-> 949243.442/754958.982 = 1.3倍
高速化の原理
moshi-kotlin-codegen
とmoshi-kotlin
での高速化の原理をそれぞれ解説します。
moshi-kotlin-codegenが高速化した原理
Kotlin
のデフォルト引数を用いた呼び出しは、実体としてはデフォルト引数に関する処理を含むコンストラクタを呼び出す形で行われます。
この辺りの詳細は以下の資料を参照ください。
今回の高速化は、引数が揃っている場合に通常のコンストラクタを呼び出す処理を生成されるJsonAdapter
に追加し、デフォルト引数に関する処理をスキップすることで達成されています。
moshi-kotlinが高速化した原理
moshi-kotlin
のマッピングはkotlin-reflect
のKFunction
を呼び出すことで行われていますが、引数が全て揃っていた場合の呼び出しをcall
で行うようにすることで達成されています。
終わりに
moshi-kotlin
の高速化は、自分の出したPRによって行われたものです。
moshi-kotlin-codegen
の高速化は、このPRのアイデアを元に行われたものです。
このPRは自分にとって初めて有名OSSに取り込んでもらえたソースコードのPRになったので、嬉しくて解説記事を書いてみた次第です。