【Kotlin】KFunction.callByを高速化した話

この記事は[Kotlinのカレンダー \| Advent Calendar 2022 \- Qiita](https://qiita.com/advent-calendar/2022/kotlin)の2日目の記事です。

`Kotlin`リポジトリに対して出した下記のPRについて解説していきます。
内容は`kotlin-reflect`内の重要な関数である`KFunction.callBy`を高速化するというものです。
`Kotlin 1.8.20`でリリース予定です。

https://github.com/JetBrains/kotlin/pull/4840

# このPRの意義
`KFunction.callBy`は、`Kotlin`のリフレクションで、デフォルト引数を有効にして関数呼び出しを行うための関数です[^reflect-full]。

[^reflect-full]: 実行には[kotlin\-reflect](https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect)が必要です。

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-callable/call-by.html

正攻法で`Kotlin`用にリフレクションでの関数呼び出しを実装したい場合、`KFunction.callBy`の呼び出しは避けられません。
実際、[簡単に検索してみる](https://github.com/search?q=%22callBy%28%22+lang%3AKotlin&type=code&ref=advsearch)だけで、以下のような有名フレームワークでの利用例が見つかります。

- [ktor](https://github.com/ktorio/ktor/blob/f101c78f5b0c7ded59c126c6de0e3cb9c60d0f9c/ktor-server/ktor-server-plugins/ktor-server-sessions/jvm/src/io/ktor/server/sessions/SessionSerializerReflection.kt#L110)
- [Exposed](https://github.com/JetBrains/Exposed/blob/c88153fc750602b697d317a849422edd5a83a311/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt#L1063)
- [jackson\-module\-kotlin](https://github.com/FasterXML/jackson-module-kotlin/blob/bbd40807bcfe97b5b089952e785b7aabf3c2f125/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ValueCreator.kt#L43)

つまり、この関数を高速化出来れば、`Kotlin`を利用する多くのシステムで処理コストを下げることが出来ます。

# どれ位高速化するか
以下のコメント時点での簡単な`JMH`ベンチマークによる性能比較を紹介します。
正確に言うとこのベンチマーク時点から変更されている部分も有りますが、大幅にスコアがブレるような変更は無く、これから入る予定も有りません。

https://github.com/JetBrains/kotlin/pull/4840#issuecomment-1300708748

ベンチマーク全体は以下にアップロードしています。

https://github.com/k163377/kfunction-call-by-benchmark

## ベンチマーク内容
以下のような、全引数にデフォルト引数が設定されたトップレベル関数から取得した`KFunction`について、一定時間内にどれだけ`callBy`できるか（秒間何回のペースで呼び出せたか）を比較しています。

```kotlin
fun __five(a0: Int = 0, a1: Int = 1, a2: Int = 2, a3: Int = 3, a4: Int = 4) {}
```

引数としては、空`Map`（= 全引数でデフォルト引数利用）の場合と、全ての引数を与えた場合を比較しています。

## ベンチマーク結果
5引数の場合の結果は以下の通りです。
`Score`は高いほど良いです。
![5引数での呼び出しスコア比較.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280230/e2538343-a80a-a223-8e31-e0b24752bd7d.png)

特に全引数でデフォルト引数を利用した場合に大きくスコアが伸びている（約4.6倍）ことが分かります。
また、デフォルト引数を利用しない場合にも、スコアが伸びています（約1.2倍）。
勿論この検証は限定的な内容であり、全ての場合での高速化を保証するものではありませんが、いくつかの計測結果を見る限り少なくとも引数が1つ以上の場合は高速化していそうでした。

この部分の生データは以下の通りです。

```:生データ（抜粋）
# 1.7.20
Benchmark                          Mode  Cnt         Score         Error  Units
Measurement.fiveWithDefault       thrpt    4    427439.340 ±   18559.512  ops/s
Measurement.fiveWithoutDefault    thrpt    4   2025561.126 ±   78028.024  ops/s

# 最適化後
Benchmark                          Mode  Cnt         Score         Error  Units
Measurement.fiveWithDefault       thrpt    4   1948375.781 ± 199051.047  ops/s
Measurement.fiveWithoutDefault    thrpt    4   2461809.238 ± 123859.545  ops/s
```

# どのような最適化を行ったか
ここから先は完全に`Kotlin`内部の実装の話になります。
内容は、以下の記事にあるような、`Kotlin`のデフォルト引数の仕組みを理解している前提となっています（この部分を分かりやすく解説できればと思いましたが書き切れませんでした、、、）。

https://developers-jp.googleblog.com/2020/11/dont-argue-with-default-arguments.html

最適化前（`1.7.21`時点）の当該関数は以下です。

https://github.com/JetBrains/kotlin/blob/e7f77e9d35483cf3d428f5c9a2eecc98d6cbb3aa/core/reflection.jvm/src/kotlin/reflect/jvm/internal/KCallableImpl.kt#L115-L175

## 用語解説
この先で用いる用語に関する解説です。
一部この記事独自の表現が含まれます。

- マスク
  - デフォルト引数の利用の有無を管理するビットマスク
- マーカー
  - デフォルト引数に関連する関数であることを示すため、`JVM`上の引数末尾に加えられる引数
  - 実行時には`null`を渡す
- ダミー引数
  - デフォルト引数を利用する際に仮で渡す（最終的にデフォルト引数で置き換えられる）引数
  - オブジェクト型の場合は`null`、プリミティブ型なら`0`（非`null`の値）を渡す
    - `vararg`の場合、対応する空配列を渡す

## 最適化内容
主に以下2点の高速化を行いました。
また、この他にも細かな修正を行なっています。

### 引数管理を配列化
最適化前までは引数を`ArrayList`で管理していましたが、最終的に要求される配列のサイズは一定であるため、最初から配列で管理するようにしました。
これによって`ArrayList`関連の計算コストと消費メモリ量が多少削減されました。

### 事前計算可能な処理のキャッシュ
最適化前までは、プリミティブ型及び`vararg`に対応するダミー引数の取得は呼び出し毎に行われていましたが、これらのダミー引数は引数毎に一定であるため、事前に計算・キャッシュして使い回す形に修正を行いました。
この処理はかなり高コストだったため、今回行った最適化の中では最大の効果が有りました。

また、最適化前はマスクやマーカーの追加処理を最後に行っていましたが、これらも最初からキャッシュに含めることで、呼び出しの度に必要な処理の量を減らしました。

# 後書き
この記事では自分の取り組んできた`KFunction.callBy`の高速化についてまとめました。

冒頭で解説した通り、`KFunction.callBy`は様々なフレームワーク・ライブラリで利用されている重要な関数です。
一方、この関数の呼び出しが非常に重たいことは前々から気になっていたので、自分が最適化に携われたことが嬉しいです。
一ユーザーとしてもこの最適化が方々に取り込まれるのを心待ちにしています。

今年はこの他にも`kotlin-reflect`の`value class`関連の修正にも携わることができ、`Kontlibute`という意味では充実した1年でした。

https://wrongwrong163377.hatenablog.com/entry/2022/04/10/162758

この他にも`kotlin-reflect`絡みの最適化案は有るので、来年もいい感じに`Kontlibute`していけたらと思います。