Blog

Kotlinのinline関数のパフォーマンスを調べる

この記事は Kotlin Advent Calendar 2021 の5日目の記事です。

Kotlinは関数をinline化することができます。

inline化によるメリットはいくつかありますが、一つはパフォーマンスの向上でしょう。

以下のように関数をパラメータとして受け取る関数については、メモリ割り当てが重複し、実行に多少のコストがかかります。

fun <T> lock(lock: Lock, body: () -> T): T {
  lock.lock()
  try {
    return body()
  }
  finally {
    lock.unlock()
  }
}

関数をinline化するのは、その関数にinline 修飾子をつけることで実現できます。

- fun <T> lock(lock: Lock, body: () -> T): T {
+ inline fun <T> lock(lock: Lock, body: () -> T): T {

これをつけることで、コンパイル時に以下のような展開された形のコードが生成され、実行時のコストが削減されます。

// コンパイル前
lock(l) { foo() }
// コンパイル後
l.lock()
try {
  foo()
}
finally {
  l.unlock()
}

一方、コンパイル時に展開されることにより、出力される実行ファイルのサイズは増えると言われてるため、自分で作成した関数をinline関数化するかどうか悩むこともあるでしょう。

今回は、inline化することによる、実行速度、メモリ使用量の変化と実行ファイルのサイズを確認してみます。

準備

Scope関数の一つである let はnullcheck等でよく使うでしょう。

let 関数は以下のようにinline関数で定義されています。

inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

今回はこれのinline版と通常関数版を比較してみます。

inline fun <T, R> T.inlineLet(block: (T) -> R): R {
    return block(this)
}

fun <T, R> T.normalLet(block: (T) -> R): R {
    return block(this)
}

実行時間の測定には measureNanoTime 関数(これもinline関数です!)を使い、メモリ使用量はRuntime.getRuntime().totalMemory()Runtime.getRuntime().freeMemory()の差で確認します。

kotlincを使いjarファイルに出力してCLI上で実行します。 実行ファイルサイズの比較は出力されたjarファイルで行います。

以下のようにinlineLetnormalLetをそれぞれ100回呼び出したものと、直書きでインクリメントしたものを比較してみます。

fun main() {
    val time = measureNanoTime {
        val result = 0
            .inlineLet { it + 1 } // 1
            .inlineLet { it + 1 } // 2
            /* ... */
            .inlineLet { it + 1 } // 100
        println(result)
    }
    println("time: $time")
    println("memory: ${Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()}")
}
fun main() {
    val time = measureNanoTime {
        val result = 0
            .normalLet { it + 1 } // 1
            .normalLet { it + 1 } // 2
            /* ... */
            .normalLet { it + 1 } // 100
        println(result)
    }
    println("time: $time")
    println("memory: ${Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()}")
}
fun main() {
    val time = measureNanoTime {
        var result = 0
        result += 1 // 1
        result += 1 // 2
        /* ... */
        result += 1 // 100
        println(result)
    }
    println("time: $time")
    println("memory: ${Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()}")
}

実行結果

それぞれ5回ずつ計測した結果は以下の通りです。

実行時間(ns)

inlineLetnormalLet直書き
122,37021,293,250126,854
120,61720,545,971126,668
127,96919,689,613125,660
128,83619,812,922119,153
121,36919,804,250118,499

メモリ使用量(B)

inlineLetnormalLet直書き
3,928,0644,434,4643,476,496
3,928,0644,434,4643,476,496
3,928,0644,434,4643,476,496
3,928,0644,434,4643,476,496
3,928,0644,434,4643,476,480

実行ファイルサイズ(B)

inlineLetnormalLet直書き
4,564,3164,655,7974,560,993

実行ファイルサイズは意外にも通常関数よりinline関数のほうが小さくなりました。 これはletの関数が非常にシンプルなため、展開しても肥大化せず、むしろ単純化されるのだと思います。

実行時間はinline化することで100分の1になり、直書きのものとほとんど同じになりました。 また、使用メモリにも差が見られます。 Inline化することで、明らかにパフォーマンスが向上していることが確認できました。

まとめ

その他以下のようなメソッドでも試してみましたが、概ね同じような結果になりました。

inline fun safe(block: () -> Unit) {
    try {
        block()
    } catch (e: Throwable) {
        println(e)
    }
}

inline fun max(block1: () -> Int, block2: () -> Int): Int {
    val value1 = block1()
    val value2 = block2()
    return if (value1 > value2) value1 else value2
}

inline化するデメリットとして実行ファイルが肥大化することが上げられますが、よほど複雑なメソッドでなければ影響しないようでした。

実行環境にもよると思いますが、実行速度に関しては100倍以上という大きな差があったため、inline関数は積極的に使っていくのが良さそうです。

一方で、以下のような通常の関数をinline化しても、パフォーマンスに変化が見られませんでした。

inline fun increment(value: Int): Int {
    return value + 1
}

以下のようにコンパイル時にwarningも出るため、inline化するのは引数に関数を受け取るときのみにするべきでしょう。

warning: expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types

インライン化によるパフォーマンスへの予想される影響はわずかです。インライン化は、関数型のパラメーターを持つ関数に最適です。

今回の調査結果が何かの参考になれば幸いです。

人気の記事

kotlin coroutinesのFlow, SharedFlow, StateFlowを整理する

Jetpack ComposeとKotlin Coroutinesを連携させる

Layout Composableを使って複雑なレイアウトを組む【Jetpack Compose】

テスト用Dispatcherの使い分け【Kotlin Coroutines】

Flow.combineの内部実装がすごい話

Jetpack ComposeのRippleエフェクトを深堀り、カスタマイズも