【自作ライブラリ】FastKFunctionはどれ位速いのか【Kotlin】

この記事は[Swift/Kotlin愛好会 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/love_swift_kotlin)の4日目の記事になりました[^cal]。

[^cal]: [Kotlin Advent Calendar 2020](https://qiita.com/advent-calendar/2020/kotlin)の方に投稿しようか迷いましたが、同じネタで3記事埋めるのは流石にどうかと思ったのでこちらに投げます……。

# 前書き
[Kotlin Advent Calendar 2020](https://qiita.com/advent-calendar/2020/kotlin)にて宣伝させて頂いた自作の`KFunction`高速呼び出しライブラリ`FastKFunction`について、使い方と呼び出し性能面の説明をしていきます。

- [ProjectMapK/FastKFunction: A wrapper library for fast calls to KFunction\.](https://github.com/ProjectMapK/FastKFunction)
- [【Kotlin】KFunctionを高速に呼び出す（前編） \- Qiita](https://qiita.com/wrongwrong/items/f7b15d54956191f471d1)
- [【Kotlin】KFunctionを高速に呼び出す（後編） \- Qiita](https://qiita.com/wrongwrong/items/fe75bae3911eff319e68)

このライブラリは`JitPack`で公開しており、`Maven`や`Gradle`といったビルドツールから利用できます。

- [ProjectMapK / FastKFunction](https://jitpack.io/#ProjectMapK/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

    failOnError = true
    isIncludeTests = false

    resultFormat = "CSV"
}
```

# FastKFunctionの使い方について
まず、`FastKFunction`の使い方について説明します。

`FastKFunction`は主に以下2クラスを提供しています

- `FastKFunction`: `KFunction`を高速に呼び出すためのラッパー
- `SingleArgFastKFunction`: 関数が引数1つのみ要求する場合にのみ利用可能な、より高速なラッパー

これらのクラスは「`KFunction`（+ 必要ならインスタンスパラメータ）」から初期化できます。
例えばインスタンス有りだと以下のようになります。

```kotlin
val fastKFunction = FastKFunction.of(instance::method, instance)
val singleArgFastKFunction = SingleArgFastKFunction.of(instance::method, instance)
```

インスタンスパラメータは以下のような場面で必要になります。

- `KFunction`が`INSTANCE`や`EXTENSION_RECEIVER`パラメータを要求している
  - 主にクラスから`KFunction`を取得した場合
- `KFunction`をより高速に呼び出したい
  - ※コンストラクタ、トップレベル関数、`object`に定義した関数はインスタンス無しで高速呼び出し可能

## FastKFunctionの呼び出し方について
`FastKFunction`は以下3通りの呼び出し方を提供しています。

```kotlin:FastKFunctionの呼び出しインターフェース
// 可変長引数
abstract fun call(vararg args: Any?): T
// Collection
abstract fun callByCollection(args: Collection<Any?>): T
// ArgumentBucket
abstract fun callBy(bucket: ArgumentBucket): T
```

`ArgumentBucket`は`FastKFunction`が独自に実装した`mutableMap<KParameter, Any?>`に近いクラスで、`Kotlin`のデフォルト引数に対応しつつ高速な呼び出しを行うためのものです。
`ArgumentBucket`を利用する場合、以下のように呼び出すことができます。

```kotlin:FastKFunctionを用いた簡単なマッピングコード
data class Sample(
    val arg1: Int,
    val arg2: Int = 0,
    val arg3: String? = null
)

val fastKFunction: FastKFunction<Sample> = FastKFunction.of(::Sample)
val valueParameters: List<KParameter> = fastKFunction.valueParameters

fun map(src: Map<String, Any?>): Sample {
    val argumentBucket: ArgumentBucket = fastKFunction.generateBucket()

    return argumentBucket.apply { 
        valueParameters.forEach {
            if (src.containsKey(it.name!!)) this[it] = src.getValue(it.name!!)
        }
    }.let { fastKFunction.callBy(it) }
}
```

サンプルコードの通り、`ArgumentBucket`を用いた呼び出しの場合デフォルト引数を利用することもできます。

## SingleArgFastKFunctionの呼び出し方について
`SingleArgFastKFunction`の呼び出しインターフェースは以下の通りです。
名前通り1引数のみ渡すことができます。

```kotlin:SingleArgFastKFunctionの呼び出しインターフェース
abstract fun call(arg: Any?): T
```

# ベンチマークによる比較
ベンチマーク全体は[GitHubに上げてあります](https://github.com/ProjectMapK/FastKFunction/tree/3b8687da712319a49e4f58a38edbb016cc0c41b7)。

以下はコンストラクタを対象にした`FastKFunction`のベンチマーク全体です。
<details>
<summary>コンストラクタ向けベンチマークコード</summary><div>

```kotlin
import com.mapk.fastkfunction.FastKFunction
import com.mapk.fastkfunction.argumentbucket.ArgumentBucket
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 CallConstructorBenchmark {
    private val function: KFunction<Constructor5> = ::Constructor5
    private val argumentMap: Map<KParameter, Any?> = function.parameters.associateWith { it.index + 1 }

    private val javaConstructor: Constructor<Constructor5> = function.javaConstructor!!
    private val fastKFunction: FastKFunction<Constructor5> = FastKFunction.of(function, null)
    private val collection: Collection<Int> = listOf(1, 2, 3, 4, 5)
    private val argumentBucket: ArgumentBucket = fastKFunction.generateBucket()
        .apply { (0 until 5).forEach { this[it] = it + 1 } }

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

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

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

    @Benchmark
    fun javaConstructor(): Constructor5 = javaConstructor.newInstance(1, 2, 3, 4, 5)

    @Benchmark
    fun fastKFunctionCall(): Constructor5 = fastKFunction.call(1, 2, 3, 4, 5)

    @Benchmark
    fun fastKFunctionCallByCollection(): Constructor5 = fastKFunction.callByCollection(collection)

    @Benchmark
    fun fastKFunctionCallBy(): Constructor5 = fastKFunction.callBy(argumentBucket)
}
```
</div></details>

また、ベンチマークの生データも[こちら](https://docs.google.com/spreadsheets/d/1DJhf8KX1-BAxCGor5cZdlO3626AZbKeet-rkk26XGAE/edit?usp=sharing)に上げてあります。
ただし、拡張関数とインスタンス関数に関してはベンチマークにおける処理内容が他と異なっているため、単純に比較はできません。

ベンチマークは`JMH`で行い、`Score Error`に関しては、ここでは簡単のため無視します。

比較対象は以下の通りで、並びは期待される実行速度順です。

- `Constructor`/`Method`: `Java`のリフレクションのコンストラクタ/メソッドの呼び出し
- `SingleArgFastKFunction`: `SingleArgFastKFunction`の`call`呼び出し（1引数の関数のみ）
- `FastKFunction(call)`: `FastKFunction`の`call`呼び出し
- `FastKFunction(callBy)`: `FastKFunction`の`callBy`呼び出し（複数引数の関数のみ）
- `KFunction(call)`: `KFunction`の`call`呼び出し
- `KFunction(callBy)`: `KFunction`の`callBy`呼び出し

スコアは`ops/s`と`倍率`で表記します。
`ops/s`は1秒当たりどれだけ操作が行えるか、`倍率`は呼び出し速度が最遅となる`KFunction.callBy`を基準にどれだけ高速に処理を行えているかです。
また、両方とも小数点以下2桁を四捨五入して表記します。

この記事で紹介するベンチマーク対象は、マッピングライブラリなどで呼び出す機会が多いと思われる以下の3関数とします。

- コンストラクタ
- コンパニオンオブジェクトに定義した関数（ファクトリメソッド）
- トップレベル関数

## FastKFunction
ベンチマーク対象の引数の数は5で統一します。

また、呼び出しは引数全てを指定した（= デフォルト引数を利用しない）状態でのもので、`KFunction`はインスタンスパラメータを渡さなくても呼び出せるもののみとします。

### コンストラクタ
![コンストラクタ (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280230/1229388e-847d-5adf-d37e-ef13bb9f2bd3.png)

|                       | ops/s       | 倍率 |
|-----------------------|------------:|-----|
| Constructor           |`104267558.4`|`6.7`|
| FastKFunction(call)   |`102948283.4`|`6.6`|
| FastKFunction(callBy) |`105609306.2`|`6.8`|
| KFunction(call)       | `77096714.2`|`5.0`|
| KFunction(callBy)     | `15519730.2`|`1`  |

### コンパニオンオブジェクトに定義した関数（ファクトリメソッド）
![コンパニオンオブジェクトに定義した関数.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280230/1c81035b-107f-2468-59cf-77319554c8f1.png)

|                       | ops/s       | 倍率 |
|-----------------------|------------:|-----|
| Method                |`102145026.0`|`6.5`|
| FastKFunction(call)   |`105252947.3`|`6.7`|
| FastKFunction(callBy) |`112420267.4`|`7.1`|
| KFunction(call)       | `83548738.0`|`5.3`|
| KFunction(callBy)     | `15781275.3`|`1`  |

### トップレベル関数
![トップレベル関数 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280230/870f88d8-cab4-36e6-7921-1cdfc954fb89.png)


|                       | ops/s       | 倍率 |
|-----------------------|------------:|-----|
| Method                |`113634719.1`|`7.1`|
| FastKFunction(call)   |`113663007.2`|`7.1`|
| FastKFunction(callBy) |`113788711.0`|`7.1`|
| KFunction(call)       | `91435909.0`|`5.7`|
| KFunction(callBy)     | `16030918.0`|`1`  |

## SingleArgFastKFunction
呼び出しは引数全てを指定した（= デフォルト引数を利用しない）状態でのもので、`KFunction`はインスタンスパラメータを渡さなくても呼び出せるもののみとします。

### コンストラクタ
![コンストラクタ (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280230/22aff686-0fbd-4e8b-61f2-34c505ae399f.png)

|                        | ops/s       | 倍率 |
|------------------------|------------:|-----|
| Constructor            |`360415771.3`|`8.3`|
| SingleArgFastKFunction |`382129250.6`|`8.8`|
| FastKFunction          |`356409526.0`|`8.2`|
| KFunction(call)        |`216949612.3`|`5.0`|
| KFunction(callBy)      | `43532534.3`|`1`  |

### コンパニオンオブジェクトに定義した関数（ファクトリメソッド）
![コンパニオンオブジェクトに定義した関数 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280230/2c9047b0-4fb7-3bc6-292d-7f70627800a5.png)

|                        | ops/s       | 倍率 |
|------------------------|------------:|-----|
| Method                 |`322787172.0`|`7.6`|
| SingleArgFastKFunction |`379662407.1`|`9.0`|
| FastKFunction          |`320499676.4`|`7.5`|
| KFunction(call)        |`188408189.4`|`4.4`|
| KFunction(callBy)      | `42569076.9`|`1`  |

### トップレベル関数
![トップレベル関数 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280230/2084ebad-4476-2f24-d079-8cc66a366cf6.png)

|                        | ops/s       | 倍率 |
|------------------------|------------:|-----|
| Method                 |`349518702.4`|`8.0`|
| SingleArgFastKFunction |`386224539.2`|`8.8`|
| FastKFunction          |`320677801.1`|`7.3`|
| KFunction(call)        |`191457147.2`|`4.4`|
| KFunction(callBy)      | `43824053.0`|`1`  |

## 考察
スコアを見ていると、いずれも`FastKFunction`/`SingleArgFastKFunction`が`KFunction`比で高速かつ、`Java`の`Constructor`や`Method`に匹敵する結果となりました。
どころか多くの結果で`Java`の`Constructor`や`Method`よりよい成績になっています。

`FastKFunction.callBy`の方が`Java`の`Constructor`や`Method`より高速なのは、後者が可変長引数で呼び出しているため場合引数の配列を一々作っているのに対し、前者は`ArgumentBucket`をそのまま使いまわしているためだと考えられます。

`SingleArgFastKFunction`に関しては正直よくわかりません。
ソースを見てもベンチマークを見ても`SingleArgFastKFunction`の方がオーバーヘッドが大きそうに見えるんですが……。

何はともあれ、誤差の範囲と強弁できなくもありませんし、少なくとも確実に変と言えるほど悪い結果にはなっていないのでヨシとします。

# 終わりに
この記事では自作のライブラリ`FastKFunction`の使い方と、`KFunction`に対する速度面での優位性を示しました。
また、ここまでの内容ではアピールしていませんが、[`object`のインスタンス自動取得](https://qiita.com/wrongwrong/items/b2ba1c2030ca1c44f4b6)や`valueParameters`と`ArgumentBucket`による呼び出しの扱いやすさから、通常利用する範囲では使いやすさの面でも`KFunction`より良いものができたんじゃないかなと思っています[^hosoku]。

[^hosoku]: この優位性は`KFunction`よりも利用の制約を強めていることが最大の理由なので、`KFunction`を貶めるような意図は有りません。

直近はアドカレに全力を尽くすつもりですが、年末年始〜は自作のマッピングライブラリである`KMapper`や`KRowMapper`に`FastKFunction`を組み込んだりしていくつもりです。

スター等々お待ちしております。

- [【Kotlin】KFunctionを高速に呼び出す（前編） \- Qiita](https://qiita.com/wrongwrong/items/f7b15d54956191f471d1)
- [【Kotlin】KFunctionを高速に呼び出す（後編） \- Qiita](https://qiita.com/wrongwrong/items/fe75bae3911eff319e68)
- [ProjectMapK/KMapper: Object to Object mapper Libraly for Kotlin\.](https://github.com/ProjectMapK/KMapper)
- [ProjectMapK/KRowMapper: Spring JDBC RowMapper for Kotlin\.](https://github.com/ProjectMapK/KRowMapper)
- [ProjectMapK/FastKFunction: A wrapper library for fast calls to KFunction\.](https://github.com/ProjectMapK/FastKFunction)