7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【moshi】moshi 1.12.0でデシリアライズが高速化したため比較・解説する【Kotlin】

Last updated at Posted at 2021-04-10

TL;DR

  • moshi 1.12から条件付きでKotlinのクラスへのデシリアライズが高速化した
  • 条件は以下の通り
    • moshi-kotlin-codegenを利用していて、デフォルト値が有りうるクラスを、引数全てが設定されたJSONからデシリアライズする場合
    • moshi-kotlinを利用していて、デフォルト値を用いず(= プロパティが全て揃った状態で)デシリアライズする場合

本文

moshi 1.12.0Kotlindata classへのデシリアライズが条件付きで高速化したので、比較・解説していきます。

高速化する条件

高速化に関する変更はmoshi-kotlin-codegenmoshi-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.0moshi 1.12.0で実行して比較を行いました。
ベンチマークに用いたコードは以下の通りです。

codegen用
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class Codegen(
    val foo: Int,
    val bar: Int,
    val baz: Int = 0
)
reflect用
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"
}

その他ベンチマークの作り方は以下を参照ください。

ベンチマーク結果

ベンチマーク結果は以下のようになりました。

codegenmoshi-kotlin-codegenreflectmoshi-kotlinのスコアで、スコアは高いほど良いです。

1.12.0
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
1.11.0
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-codegenmoshi-kotlinでの高速化の原理をそれぞれ解説します。

moshi-kotlin-codegenが高速化した原理

Kotlinのデフォルト引数を用いた呼び出しは、実体としてはデフォルト引数に関する処理を含むコンストラクタを呼び出す形で行われます。
この辺りの詳細は以下の資料を参照ください。

今回の高速化は、引数が揃っている場合に通常のコンストラクタを呼び出す処理を生成されるJsonAdapterに追加し、デフォルト引数に関する処理をスキップすることで達成されています。

moshi-kotlinが高速化した原理

moshi-kotlinのマッピングはkotlin-reflectKFunctionを呼び出すことで行われていますが、引数が全て揃っていた場合の呼び出しをcallで行うようにすることで達成されています。

終わりに

moshi-kotlinの高速化は、自分の出したPRによって行われたものです。

moshi-kotlin-codegenの高速化は、このPRのアイデアを元に行われたものです。

このPRは自分にとって初めて有名OSSに取り込んでもらえたソースコードのPRになったので、嬉しくて解説記事を書いてみた次第です。

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?