Kotlin Inline classのパフォーマンスを計測する

こんにちは。ASKULのほかほかごはんです。 今回の記事はKotlin 1.3で追加されたInline classと data classとのパフォーマンス比較についてまとめたものになります。 なお、Inline classは現時点でExperimentalなのでご注意ください。

初めに Value Objectの利点とコストについて

Value ObjectはDDDの中でも導入が手軽で効果も大きいためLOHACOのアプリケーション開発でも登場する機会が増えています。

// 簡単な例
data class ItemPrice(private val value: Int) {
    operator fun plus(itemPrice: ItemPrice) =
        ItemPrice(itemPrice.rawValue() + rawValue())
        
    fun rawValue(): Int = value
    
    companion object {
        val ZERO = ItemPrice(0)
    }
}

しかしながら、Value Objectを使用した際のコストが気になるケースもあります。

// itemPriceのCollectionを合算する
val totalPrice =
    fold(ItemPrice.ZERO) { total, itemPrice ->
        total + itemPrice
    }

合計を算出するためにfoldを利用していますが、計算過程でloop分のObjectを生成してしまいます。 Collectionが大きくなるほどその生成コストは無視しがたいものになっていきます。

Inline classについて

Inline classは上述の問題を解決するために導入されました。 インライン展開してくれるので、ItemPriceはIntに置き換えられます。そのためオブジェクト生成コストを気にする必要がありません。 こいつは便利 ;-)

計測してみる

実際のところどの程度早くなるのか気になったので計測してみることにしました。 計測にはJMHを利用しています。

実行環境

Software Version
OS MacOS Mojave 10.14.4
Java 1.8.0_192-b12
Kotlin 1.3.21
Gradle 4.10
JMH Gradle Plugin 0.4.8

計測対象

  1. data class
  2. inline class
  3. primitive

テストコード

// ItemPriceInline classはItemPriceをdata classからInline classにしたものです
@State(Scope.Thread)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
open class LoaderBenchMark {
    private val randomValues = List(100000) { Random.nextInt(1, 100) }

    private val itemPrices = randomValues.map { ItemPrice(it) }

    private val itemPriceInlines = randomValues.map { ItemPriceInline(it) }

    private val intPrices = randomValues

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    fun sumItemPrice(): ItemPrice =
        itemPrices.fold(ItemPrice.ZERO) { total, itemPrice ->
            total + itemPrice
        }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    fun sumItemPriceInline(): ItemPriceInline =
        itemPriceInlines.fold(ItemPriceInline.ZERO) { total, itemPriceInline ->
            total + itemPriceInline
        }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    fun sumInt(): Int =
        intPrices.fold(0) { total, price -> total + price }
}

結果

Benchmark                           Mode  Cnt  Score   Error  Units
LoaderBenchMark.sumInt              avgt   25  0.065 ± 0.005  ms/op
LoaderBenchMark.sumItemPrice        avgt   25  0.278 ± 0.006  ms/op
LoaderBenchMark.sumItemPriceInline  avgt   25  0.064 ± 0.001  ms/op

data classと比較してinline classがより高速であることがわかりました。 予想通り、primitiveなIntの場合と同様の速度を出しています。すごいぞInline class。

実運用に向けて

すぐにでも利用したいのですが、Inline classにはいくつかの制約があります。

  1. init blockを持てない
  2. backing fieldsを持てない
  3. primary constructorを公開しなければならない

以上の制約から、Value Objectに業務ルールを表現することができません。

inline class ItemPriceInline(
    private val value: Int?
) {
    // init blockでvalidationしたいがコンパイルエラーになる :(
    init {
        if (value == null || value < 0 || value > 100000000) {
            throw IllegalArgumentException("異常な金額: $value")
        }
    }
    
    // price の accessor
    fun rawValue() = value!!
    
    companion object {
        // static constructorを定義しても利用者はItemPriceInline(-10) を呼べる :(
        fun valueOf(price: Int?): ItemPriceInline {
            if (price == null || price < 0 || price > 100000000) {
                throw IllegalArgumentException("異常な金額: $price")
            }
            return ItemPriceInline(price)
        }
    }
}

現時点でInline classをValue Objectとして活用する場合、validateを必要としないシンプルなケースに限定されそうです。
オブジェクト生成時のvalidationについては議論が続いていますので期待して待ちましょう。

まとめ

実際に測定することでInline classの可能性を言葉でなく心で理解することができました。 Experimentalなので本番利用は慎重に行いましょう。Have a nice Kotlin!

ASKUL Engineering BLOG

2021 © ASKUL Corporation. All rights reserved.