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

この記事は[Kotlin Advent Calendar 2020](https://qiita.com/advent-calendar/2020/kotlin)の7日目の記事です。
[【Kotlin】KFunctionを高速に呼び出す（前編）](https://qiita.com/wrongwrong/items/f7b15d54956191f471d1)の続きです。

---

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

## ベンチマークを行った環境
記事内に幾つか`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`を`call`呼び出ししやすくすることによる高速化手法を紹介しました。

素直に`KFunction`を呼び出す形での高速化に関しては、恐らくこれが限界だろうと思います。
一方、`call`呼び出しをしたとしても`Java`の`Constructor`/`Method`を直接呼び出す速度には及びませんでした。

この状況で更に高速化するには以下2つの方法が有りますが、今回は前者について紹介します。

- `call`呼び出しする時になるべく`Java`の`Constructor`/`Method`を直接呼び出す
- （`call`する時に`spread`演算子を使っているなら）`Java`のクラスでラップして`spread`演算子の利用を避ける

なお、関連する内容は黒魔術に足を突っ込んでいる感が大きく、一般的にはここまでやらなくていいと思っています。

## 高速化呼び出しに関する制約
ここで紹介する方法の中には`KFunction`だけで高速呼び出しができないような場合も有ります。

`KFunction`は、インスタンスからメソッドリファレンスで取得した場合、呼び出し時にインスタンスを渡す必要はありません。
一方、`static`関数でない`Java`の`Method`を呼び出す場合にはインスタンスが必要となります。
ここで、`KFunction`からインスタンスを取得する方法は、自分が知る限り一部の例外を除いて有りません。

以上の通り、`KFunction`の高速呼び出しにインスタンスも必要になる場合は`KFunction`だけで高速呼び出しができないことが有ります。

## KFunctionからConstructor/Methodを取得する
`KFunction`には`javaConstructor`と`javaMethod`というプロパティが用意されており、`KFunction`の実体に合わせてそれぞれ`Constructor`/`Method`を取得することができます。

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

val javaConstructor: Constructor<T>? = function.javaConstructor
val javaMethod: Method? = function.javaMethod
```

# それぞれの状況での高速な呼び出し方法
`Java`の`Constructor`/`Method`を直接呼び出すには、`KFunction`の定義元ごとに幾つかの条件が有りますが、大まかに以下3パターンに分けられます。

1. コンストラクタ
2. トップレベル関数
3. インスタンス関数

拡張関数に関しては、2と3にそれぞれ含まれます。

以下、それぞれに関する呼び出し方についてまとめます。

## コンストラクタの場合
コンストラクタの場合は`javaConstructor`をそのまま呼び出すことで高速な呼び出しができます。

```kotlin
data class Foo(val arg1: Int, ...)

// 呼び出し対象関数
val function: KFunction<Foo> = ::Foo

val javaConstructor: Constructor<Foo> = function.javaConstructor!!
val result = javaConstructor.newInstance(/* 引数 */)
```

## トップレベル関数の場合
トップレベル関数の場合、実体は`static`メソッドになるため、`javaMethod`をインスタンス無しで呼び出すことで高速な呼び出しができます。

```kotlin
fun foo(arg1: Int, ...): T = /* 略 */

// 呼び出し対象関数
val function: KFunction<T> = ::foo

val javaMethod: Method = function.javaMethod!!
val result = javaMethod.invoke(null, /* 引数 */) as T // Methodはタイプを持たないためキャストが必要
```

### トップレベル拡張関数の場合
トップレベル拡張関数の場合、`static`メソッドになる点は同じですが、パラメータとして`EXTENSION_RECEIVER`が必要になる点が異なります。

```kotlin
fun Foo.bar(arg1: Int, ...): T = /* 略 */

// EXTENSION_RECEIVER
val foo: Foo = /* Fooのインスタンス */
// 呼び出し対象関数
val function: KFunction<T> = foo::bar

val javaMethod: Method = function.javaMethod!!
val result = javaMethod.invoke(null, foo, /* 引数 */) as T // Methodはタイプを持たないためキャストが必要
```

## インスタンス関数の場合
インスタンス関数の場合、呼び出しにインスタンスが必要になります。

```kotlin
val foo: Foo = /* Fooのインスタンス */
// 呼び出し対象関数
val function: KFunction<T> = foo::bar

val javaMethod: Method = function.javaMethod!!
val result = javaMethod.invoke(foo, /* 引数 */) as T // Methodはタイプを持たないためキャストが必要
```

### objectに定義された関数の場合
`companion object`など、`Kotlin`の`object`に定義された`Method`の場合、[`Method`から`object`のインスタンスを持ってくることができる](https://qiita.com/wrongwrong/items/b2ba1c2030ca1c44f4b6)ため、`KFunction`だけからも高速呼び出しが可能です。

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

val javaMethod: Method = function.javaMethod!!
val declaringObject = javaMethod.declaringClass.kotlin.objectInstance!!

val result = javaMethod.invoke(declaringObject, /* 引数 */) as T // Methodはタイプを持たないためキャストが必要
```

### インスタンスに定義された拡張関数の場合
インスタンスに定義された拡張関数の場合、インスタンスに加えてパラメータとして`EXTENSION_RECEIVER`が必要になります。

```kotlin
// EXTENSION_RECEIVER
val foo: Foo = /* Fooのインスタンス */
// 呼び出し対象関数
val function: KFunction<T> = foo::bar
// Foo.bar関数を定義しているクラスのインスタンス
val baz: Baz = /* barのインスタンス */

val javaMethod: Method = function.javaMethod!!
val result = javaMethod.invoke(baz, foo, /* 引数 */) as T // Methodはタイプを持たないためキャストが必要
```

# ベンチマーク
高速呼び出しすることでどれ位高速化するかを示すため、以下にそれぞれの呼び出しに関するベンチマーク結果を載せます。

## 注意点
呼び出している関数は処理内容が異なるものも含まれているため、スコアは同じ括りの内容に関する比較にのみ有効です。

`KFunction`の呼び出しに関しては、取得方法によってスコアに差が出る場合も有るため、そのような内容に関してはインスタンスからメソッドリファレンスで取得した場合に絞ってスコアを出します。

インスタンスに定義された拡張関数はインスタンスからメソッドリファレンスで取得できないため、今回のベンチマークからは外しています。

## ベンチマークの詳細
### ソースコード
今回のベンチマークで呼び出し対象としたコードです。

<details>
<summary>呼び出し対象関数群</summary><div>

```kotlin
data class Constructor5(
    val arg1: Int,
    val arg2: Int,
    val arg3: Int,
    val arg4: Int,
    val arg5: Int
) {
    fun instanceFun5(arg1: Int, arg2: Int, arg3: Int, arg4: Int, arg5: Int) = Constructor5(
        this.arg1 + arg1,
        this.arg2 + arg2,
        this.arg3 + arg3,
        this.arg4 + arg4,
        this.arg5 + arg5
    )

    companion object {
        fun companionObjectFun5(
            arg1: Int,
            arg2: Int,
            arg3: Int,
            arg4: Int,
            arg5: Int
        ) = Constructor5(arg1, arg2, arg3, arg4, arg5)
    }
}

fun topLevelFun5(
    arg1: Int,
    arg2: Int,
    arg3: Int,
    arg4: Int,
    arg5: Int
) = Constructor5(arg1, arg2, arg3, arg4, arg5)

fun Constructor5.topLevelExtensionFun5(arg1: Int, arg2: Int, arg3: Int, arg4: Int, arg5: Int) = Constructor5(
    this.arg1 + arg1,
    this.arg2 + arg2,
    this.arg3 + arg3,
    this.arg4 + arg4,
    this.arg5 + arg5
)
```
</div></details>

ベンチマークのコード全体は以下リポジトリの`src/jmh`配下に書かれていて、`gradle jmh --no-daemon`で実行できます[^no-daemon]。
ただし、全体の実行には2時間少しかかるのでご注意ください。

- [k163377/FastKFunction at advent\_calendar](https://github.com/k163377/FastKFunction/tree/advent_calender)

[^no-daemon]: `--no-daemon`している理由は次の記事の通りです [【JMH】JMH Gradle PluginはWindows 10で正常に動作しない【Gradle】 \- Qiita](https://qiita.com/wrongwrong/items/78b7a8d72b7693650c05)

### 結果の生データ
記事とは直接関係しない余計なスコアも含まれていますが、結果の生データは以下の通りです。

<details>
<summary>生データ</summary><div>

```
Benchmark                                                                               Mode  Cnt          Score          Error  Units
CallConstructorBenchmark.fastKFunctionCall                                             thrpt    9   85052573.766 ± 10135836.867  ops/s
CallConstructorBenchmark.fastKFunctionCallBy                                           thrpt    9   97982704.063 ±   727311.047  ops/s
CallConstructorBenchmark.javaConstructor                                               thrpt    9  104106898.981 ±   513441.664  ops/s
CallConstructorBenchmark.kFunctionCall                                                 thrpt    9   82064272.976 ±  7502561.790  ops/s
CallConstructorBenchmark.kFunctionCallBy                                               thrpt    9   15426701.690 ±   387936.791  ops/s
CallConstructorBenchmark.normalCall                                                    thrpt    9  539215159.610 ±  2730821.696  ops/s
CallInstanceMethodBenchmark.fastKFunctionWithInstanceCall                              thrpt    9   87384543.240 ±  7278754.272  ops/s
CallInstanceMethodBenchmark.fastKFunctionWithInstanceCallBy                            thrpt    9   98649583.249 ±   611586.904  ops/s
CallInstanceMethodBenchmark.fastKFunctionWithoutInstanceCall                           thrpt    9   72715463.637 ±  5736837.740  ops/s
CallInstanceMethodBenchmark.fastKFunctionWithoutInstanceCallBy                         thrpt    9   75080816.237 ±   523158.603  ops/s
CallInstanceMethodBenchmark.javaMethod                                                 thrpt    9   96602286.980 ±  8052988.952  ops/s
CallInstanceMethodBenchmark.kFunctionCall                                              thrpt    9   78468535.140 ±  6653819.249  ops/s
CallInstanceMethodBenchmark.kFunctionCallBy                                            thrpt    9   15502674.154 ±   397163.021  ops/s
CallInstanceMethodBenchmark.normalCall                                                 thrpt    9  401915746.475 ±  2120160.044  ops/s
CallObjectMethodBenchmark.fastKFunctionByMethodReferenceWithInstanceCall               thrpt    9   90185483.584 ±  3927354.323  ops/s
CallObjectMethodBenchmark.fastKFunctionByMethodReferenceWithInstanceCallBy             thrpt    9   97556887.857 ±   388612.159  ops/s
CallObjectMethodBenchmark.fastKFunctionByMethodReferenceWithoutInstanceCall            thrpt    9   91550100.076 ±   512383.591  ops/s
CallObjectMethodBenchmark.fastKFunctionByMethodReferenceWithoutInstanceCallBy          thrpt    9   88524021.847 ± 11371527.505  ops/s
CallObjectMethodBenchmark.fastKFunctionByReflectionWithInstanceCall                    thrpt    9   91522667.716 ±   434637.722  ops/s
CallObjectMethodBenchmark.fastKFunctionByReflectionWithInstanceCallBy                  thrpt    9   92706863.702 ± 12240766.452  ops/s
CallObjectMethodBenchmark.fastKFunctionByReflectionWithoutInstanceCall                 thrpt    9   87643733.604 ± 10361398.433  ops/s
CallObjectMethodBenchmark.fastKFunctionByReflectionWithoutInstanceCallBy               thrpt    9   97654075.423 ±   304503.876  ops/s
CallObjectMethodBenchmark.functionByMethodReferenceCall                                thrpt    9   82706039.555 ±   444529.463  ops/s
CallObjectMethodBenchmark.functionByMethodReferenceCallBy                              thrpt    9   15796093.171 ±   228602.752  ops/s
CallObjectMethodBenchmark.javaMethod                                                   thrpt    9  101449027.309 ±   495643.624  ops/s
CallObjectMethodBenchmark.normalCall                                                   thrpt    9  536079767.211 ±  1157563.556  ops/s
CallTopLevelExtensionFunBenchmark.fastKFunctionByMethodReferenceWithInstanceCall       thrpt    9   43825314.870 ±  1679136.313  ops/s
CallTopLevelExtensionFunBenchmark.fastKFunctionByMethodReferenceWithInstanceCallBy     thrpt    9   44766675.224 ±  3944829.919  ops/s
CallTopLevelExtensionFunBenchmark.fastKFunctionByMethodReferenceWithoutInstanceCall    thrpt    9   38669399.996 ±  2046228.488  ops/s
CallTopLevelExtensionFunBenchmark.fastKFunctionByMethodReferenceWithoutInstanceCallBy  thrpt    9   39998505.538 ±  2233768.115  ops/s
CallTopLevelExtensionFunBenchmark.fastKFunctionFromClassCall                           thrpt    9   43704874.150 ±   718792.734  ops/s
CallTopLevelExtensionFunBenchmark.fastKFunctionFromClassCallBy                         thrpt    9   45983654.167 ±  2273278.593  ops/s
CallTopLevelExtensionFunBenchmark.functionByMethodReferenceCall                        thrpt    9   40063994.092 ±  3396635.812  ops/s
CallTopLevelExtensionFunBenchmark.functionByMethodReferenceCallBy                      thrpt    9   12807720.087 ±   472068.412  ops/s
CallTopLevelExtensionFunBenchmark.javaMethod                                           thrpt    9  102103993.994 ±  7014535.924  ops/s
CallTopLevelExtensionFunBenchmark.normalCall                                           thrpt    9  402199156.209 ±  1631181.425  ops/s
CallTopLevelFunBenchmark.fastKFunctionCall                                             thrpt    9   76069475.472 ± 11176352.425  ops/s
CallTopLevelFunBenchmark.fastKFunctionCallBy                                           thrpt    9   73204493.212 ±  6711746.415  ops/s
CallTopLevelFunBenchmark.javaMethod                                                    thrpt    9  107878131.754 ± 11422459.814  ops/s
CallTopLevelFunBenchmark.kFunctionCall                                                 thrpt    9   94344375.947 ±   720958.628  ops/s
CallTopLevelFunBenchmark.kFunctionCallBy                                               thrpt    9   15943463.689 ±   227926.568  ops/s
CallTopLevelFunBenchmark.normalCall                                                    thrpt    9  536198749.612 ±  1800497.188  ops/s
```
</div></details>

## それぞれのスコア

### コンストラクタの場合
![コンストラクタ (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280230/5d34399e-234d-e643-cf3f-e5b96e09eede.png)

### トップレベル関数の場合
![トップレベル関数 (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280230/3d078450-2d50-76c0-5d8b-55c481134d13.png)

### トップレベル拡張関数の場合
![トップレベル拡張関数 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280230/b41df051-5c8c-7dbb-5cf0-ebd1412b2f94.png)

### インスタンス関数の場合
![インスタンス関数 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280230/90317195-6c99-d98c-2a43-2ac095e09536.png)

### オブジェクトに定義されたインスタンス関数の場合
![オブジェクトに定義されたインスタンス関数.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280230/73c8f6da-1afe-eeef-bc85-998ce285d0bc.png)

# 終わりに
この記事では、`KFunction`から`Java`の`Constructor`/`Method`を取得し直接呼び出すことによる高速化方法と、どれくらい高速化するかについてまとめました。
ライブラリを作るように、包括的にやる中でこの方法を取り込む場合はオーバーヘッドが出たりして必ずしもこのスコア通りに高速化する訳ではありませんが、それでも効果が有ることを示せたと思います。

書いた後で「スプレッド演算子のコストがとても重いから`Java`のクラスでラップして使わないようにすると高速化する」ということを知ったり、書けなかった工夫も幾つか有りますが、ある程度綺麗にまとめられたかなと思います。
ここまでやらかしてでも高速性を追求してみたい方のお役に立てれば幸いです。

# 宣伝: 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)