こんにちは。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 |
計測対象
- data class
- inline class
- 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にはいくつかの制約があります。
- init blockを持てない
- backing fieldsを持てない
- 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!