⏱️

Kotlinでロジックの速度計測する時に便利な関数を作った

2021/07/02に公開
2

しかじろうだよ。
みなさん計測してますか。
なにかロジックを実装してその速度を計測したい時ありますよね。僕はあります。

一般的な計測方法

王道な方法として以下の実装でできます。

val start = System.nanoTime()
// 計測したい処理
execute()
val end = System.nanoTime()
Log.d("Performance", "process time ${end - start}")

とりあえずこれで動くのですが、 System.nanoTime() を色んな所で呼ぶのでちょっと煩雑です。
もうちょっとスッキリ書けないものでしょうか。

ちょっと便利な計測方法

そこでKotlinは便利な拡張関数があります。

val time = measureNanoTime {
    // 計測したい処理
    execute()
}
Log.d("Performance", "process time ${time}")

計測したい処理をラムダで囲むだけです。かなりスッキリ書けます。内部的には上で書いた一般的な計算方法と同じです。
ただこのやり方だとスコープで囲まれてしまうため、処理の中で変数を作って別の場所で使いたい場合は使いづらいです。

val time = measureNanoTime {
    val hoge = execute()
}
Log.d("Performance", "process time ${time}")
// hogeを渡せない。困った。
call(hoge)

もうちょっと程よい計測方法はないでしょうか。

ちょうどいい感じの計測方法

関数とラムダを組み合わせて、以下のように呼び出せる関数を作りました。

fun measure(): (msg: String) -> Unit {
    val start = System.nanoTime()
    return { msg ->
        Log.d("Performance", "%,13d $msg".format(System.nanoTime() - start))
    }
}

measture() を呼び出したタイミングで計測を開始します。
戻り値としてラムダが返ってくるので、それを計測終了後に呼び出します。
System.nanoTime() を隠蔽できてるし、スコープがないので変数を自由に取り回せます。

val stop = measure()
// 計測したい処理
val hoge = execute()
stop("process time")
// hogeを渡せる。嬉しい!
call(hoge)

ちなみにログの時間を揃えて見やすくしてます。こういう地味な対応がとても便利です。

D/Performance:    27,455,940 copy time

もっといい感じの計測方法

yy_yankさんよりアドバイスコメントいただきました。
measureNanoTimeにはcontractがあるので

val hoge
val time = measureNanoTime {
     hoge = execute()
}
call(hoge)

こういうふうに書けるようです。contractでこんなことできるの知らなかった。勉強になりました。僕の提案したスタイルは使わなくて良さそうです。
https://zenn.dev/shikajiro/articles/2f58c3aa07cdff#comment-fb479156b327e6

さらにいい感じの計測方法

上記のアドバイスを参考に、下記を実現する仕組みを作りました。

  • デバッグのときのみ計測する
  • ログ出力も一緒に実行

作った関数はこんな感じ

@OptIn(ExperimentalContracts::class)
inline fun measure(msg: String, block: () -> Unit) {
    // contractを設定することで、block内でval変数への代入が可能になる
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }

    //PERFORMANCEとDEBUGを有効にしている場合のみ計測する
    if (BuildConfig.PERFORMANCE && BuildConfig.DEBUG) {
        val time = measureNanoTime {
            block()
        }

        Log.d("Performance", "%,13dns $msg".format(time))
    } else {
        // 計測しない場合は処理をそのまま実行する
        block()
    }
}

実行はこんな感じ

val hoge: String // 型は任意
measure("process time") {
    // 計測したい処理
    hoge = execute()
}
// hogeを渡せる。嬉しい!
call(hoge)

さらにいい感じの関数ができました。

実際に使ってる実装をgistに載せてますのでよかったらどうぞ。

Discussion