【Kotlin】KFunctionを高速に呼び出す（前編）

この記事は[Kotlin Advent Calendar 2020](https://qiita.com/advent-calendar/2020/kotlin)の2日目の記事です。
本来7日目にまとめて投げるつもりでしたが、長くなりすぎたのと枠が余っていたので分割しました。

---

# 前書き
この一連の記事では、`Kotlin`のリフレクションにおける関数である`KFunction`と、それを高速に呼び出す方法についてまとめます。
また、それぞれの記事の最後にはここで紹介するノウハウを用いた`FastKFunction`という自作ライブラリ、及び関連して自分の公開しているライブラリを紹介します。

この記事では、`KFunction`に関する基礎知識と、`KFunction`を高速に呼び出す方法の内まっとうな内容について書きます。

## ベンチマークを行った環境
記事内に幾つか`JMH`のベンチマークスコアが出てきますが、計測は全て`Ryzen7 3700X`の`Windows10`環境で行っています。
ベンチマークの実行は`JMH Gradle Plugin`（`me.champeau.gradle.jmh`）で行っており、設定は以下のとおりです。

```kotlin:jmhの設定（build.gradle.kts）
jmh {
    fork = 3
    iterations = 3
    threads = 3
    warmupBatchSize = 3
    warmupIterations = 3
}
```

# KFunctionとは何か
高速な呼び出しについて説明する前に、まず`KFunction`に関する基本的な知識を説明します。

前述の通り`KFunction`とは`Kotlin`のリフレクションにおける関数で、`Java`のリフレクションにおける`Method`や`Constructor`に該当します。

## KParameterについて
`KParameter`は、`KFunction`の引数情報です。

`KParameter`からは、その引数の関数の定義上の並び順や名前といった情報が取得できます。
また、`KFunction`の呼び出し時にも`KParameter`が必要になる場合が有ります。

この情報は`KFunction`の呼び出し時などに利用します。

## KFunctionの使いどころ
`Kotlin`では`immutability`が重視されているため、コンストラクタなど何らかの関数を1度呼び出す形でオブジェクトを初期化するのが主です。
そのため、特に`Jackson`のような`Kotlin`対応のマッピングライブラリは[`KFunction`を呼び出す形でオブジェクトを初期化する](https://github.com/FasterXML/jackson-module-kotlin/blob/130f0944be72d5dc3d3c88cd76abddb6dc9359a7/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt#L171)実装になるのが自然です。

### KFunctionが高速に呼び出せると何が嬉しいのか
前述の通り、`KFunction`は`Kotlin`対応のマッピングライブラリで利用されているため、これを高速に呼び出せるとマッピング処理のコストを下げることができます。
後述する通り、`KFunction`の呼び出し速度はとても遅いパターンも有るため、これを高速化することによる効果量も期待できます。

## KFunctionの呼び出し方と動作速度の比較

### KFunctionの呼び出し方
`KFunction`には`call`と`callBy`という2種類の呼び出し方法が有ります。
これら2つを比較すると以下のようになります。

|                    | `call`                     | `callBy`                |
|--------------------|----------------------------|-------------------------|
|      **引数**      |              `vararg Any?` | `Map<KParameter, Any?>` |
| **デフォルト引数** | 非対応                     | 対応                    |
| **呼び出し速度**   | 高速                       | 低速                    |
| **その他制約**     | `vararg`に渡す引数の順番が正しくなければならない | なし |

`callBy`は抽象化やデフォルト引数への対応から[簡単なマッピングツールを作るような場面](https://qiita.com/wrongwrong/items/9e1f500606ff3880f8e8)では非常に扱いやすいですが、呼び出しの速度という意味では`call`に劣ります。

### 呼び出し速度の簡単な比較
コンストラクタに関して、普通に呼び出した場合、`call`/`callBy`で呼び出した場合、`Java`の`Constructor`で呼び出した場合のそれぞれの速度を簡単に比較するため、5引数のコンストラクタをそれぞれの方法で呼び出す`JMH`ベンチマークを作成して比較しました。
スコアは高い方が良いものです。

`callBy`が圧倒的に遅いこと、`Java`の`Constructor`の呼び出しは`call`よりも高速なことが分かります。
この差が生じる原因は、前述の通り`KFunction`が抽象化されているため、呼び出し時にオーバーヘッドが有ることです。
特に`callBy`する場合、キーごとの初期化チェックやデフォルト引数周りの確認などが入るため低速になります。

```:呼び出し速度の比較
Benchmark                                      Mode  Cnt          Score          Error  Units
CallConstructorBenchMark.javaConstructor      thrpt    9  103641734.582 ±   375122.731  ops/s
CallConstructorBenchMark.kFunctionCall        thrpt    9   83813952.159 ±  3434662.231  ops/s
CallConstructorBenchMark.kFunctionCallBy      thrpt    9   14809909.647 ±   358994.052  ops/s
CallConstructorBenchMark.normalCall           thrpt    9  527408471.072 ± 10805692.616  ops/s
```

<details>
<summary>比較用コード</summary><div>

```kotlin
import org.openjdk.jmh.annotations.Benchmark
import org.openjdk.jmh.annotations.Scope
import org.openjdk.jmh.annotations.State
import java.lang.reflect.Constructor
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.jvm.javaConstructor

@State(Scope.Benchmark)
open class CallFunctionsBenchMark {
    data class Sample(
        val arg1: Int,
        val arg2: Int,
        val arg3: Int,
        val arg4: Int,
        val arg5: Int
    )

    val function: KFunction<Sample> = ::Sample
    val argumentMap: Map<KParameter, Any?> = function.parameters.associateWith { it.index + 1 }

    val javaConstructor: Constructor<Sample> = function.javaConstructor!!

    @Benchmark
    fun normalCall(): Sample = Sample(1, 2, 3, 4, 5)

    @Benchmark
    fun kFunctionCall(): Sample = function.call(1, 2, 3, 4, 5)

    @Benchmark
    fun kFunctionCallBy(): Sample = function.callBy(argumentMap)

    @Benchmark
    fun javaConstructor(): Sample = javaConstructor.newInstance(1, 2, 3, 4, 5)
}
```
</div></details>

# KFunctionを高速に呼び出す方法
前述の比較の通り、`KFunction`は`call`で呼び出す方が高速なため、`callBy`しなくて良いと言い切れる場合は`call`だけを使うのがいいでしょう。

`callBy`呼び出しを両立したい場合には、`KParameter`の数が固定かつ`index`情報が有ることから、バケツソートの要領で引数を管理する`Map`を作るという方法が有ります。
この方法であれば、`Map<KParameter, Any?>`の構築や、初期化状況の確認、引数の並び替えを最小限のコストで行うことができるため、「完全初期化されていれば`call`呼び出し、されていなければ`callBy`呼び出し」という切り替えを容易に実装できます。
また、細かい部分として、アクセスには`index`のみ用いるため、`HashMap`等に比べて読み書きのコストも低く抑えられます。

以下は自作ライブラリ内での実装を省略・簡単化した例です。

- [FastKFunction/ArgumentBucket\.kt at ProjectMapK/FastKFunction](https://github.com/ProjectMapK/FastKFunction/blob/57d4f6b6d484ace2b3ecd151a2d9512d84503a9c/src/main/kotlin/com/mapk/fastkfunction/argumentbucket/ArgumentBucket.kt)

```kotlin:ArgumentBucket（抜粋・微改変）
class ArgumentBucket(
    private val keyList: List<KParameter>,
    val valueArray: Array<Any?>,
    private val initializationStatuses: BooleanArray
) : Map<KParameter, Any?> {
    class Entry internal constructor(
        override val key: KParameter,
        override var value: Any?
    ) : Map.Entry<KParameter, Any?>

    fun isFullInitialized(): Boolean = initializationStatuses.all { it }

    operator fun set(key: KParameter, value: Any?): Any? {
        return valueArray[key.index].apply {
            valueArray[key.index] = value
            initializationStatuses[key.index] = true
        }
    }

    // keyはインスタンスの一致と初期化状態を見る
    override fun containsKey(key: KParameter): Boolean =
        keyList[key.index] === key && initializationStatuses[key.index]
}
```

```kotlin:使い方
// 呼び出し対象関数
val function: KFunction<T>

fun callBy(bucket: ArgumentBucket): T {
    return if (bucket.isFullInitialized())
        function.call(bucket.valueArray)
    else
        function.callBy(bucket)
}
```

# 終わりに
この記事では`KFunction`に関する基礎知識と、`KFunction`を高速に呼び出す方法の内まっとうな内容について書きました。
`KFunction`を呼び出す形での高速化に関しては、恐らくこのやり方が限界だろうと思います。

後編では、まっとうじゃない所まで踏み込んで更なる高速化を行う方法について解説します。

追記: 後編を公開しました -> [【Kotlin】KFunctionを高速に呼び出す（後編） \- Qiita](https://qiita.com/wrongwrong/items/fe75bae3911eff319e68)

# 宣伝: FastKFunctionについて
最後に宣伝です。

ここで紹介したノウハウを用いた`KFunction`の高速呼び出しが行えるライブラリ、`FastKFunction`を公開しています。
「自分で作りたくはないけど`KFunction`を高速で呼び出したい」という方は是非使ってみて下さい。
スターもお待ちしております。

- [ProjectMapK/FastKFunction: A wrapper library for fast calls to KFunction\.](https://github.com/ProjectMapK/FastKFunction)

関連して、`ProjectMapK`というオーガナイゼーションで幾つかのマッピングライブラリを公開しています。
`FastKFunction`にもこれから対応していく予定ですので、こちらも利用・スターお待ちしております。

- [ProjectMapK/KMapper: Mapper Libraly for Kotlin\.](https://github.com/ProjectMapK/KMapper)
- [ProjectMapK/KRowMapper: Spring JDBC RowMapper for Kotlin\.](https://github.com/ProjectMapK/KRowMapper)