この記事はSwift/Kotlin愛好会 Advent Calendar 2020の4日目の記事になりました1。
前書き
Kotlin Advent Calendar 2020にて宣伝させて頂いた自作のKFunction高速呼び出しライブラリFastKFunctionについて、使い方と呼び出し性能面の説明をしていきます。
- ProjectMapK/FastKFunction: A wrapper library for fast calls to KFunction.
- 【Kotlin】KFunctionを高速に呼び出す(前編) - Qiita
- 【Kotlin】KFunctionを高速に呼び出す(後編) - Qiita
このライブラリはJitPackで公開しており、MavenやGradleといったビルドツールから利用できます。
ベンチマークを行った環境
記事内に幾つかJMHのベンチマークスコアが出てきますが、計測は全てRyzen7 3700XのWindows10環境で行っています。
ベンチマークの実行はJMH Gradle Plugin(me.champeau.gradle.jmh)で行っており、設定は以下のとおりです。
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(+ 必要ならインスタンスパラメータ)」から初期化できます。
例えばインスタンス有りだと以下のようになります。
val fastKFunction = FastKFunction.of(instance::method, instance)
val singleArgFastKFunction = SingleArgFastKFunction.of(instance::method, instance)
インスタンスパラメータは以下のような場面で必要になります。
-
KFunctionがINSTANCEやEXTENSION_RECEIVERパラメータを要求している- 主にクラスから
KFunctionを取得した場合
- 主にクラスから
-
KFunctionをより高速に呼び出したい- ※コンストラクタ、トップレベル関数、
objectに定義した関数はインスタンス無しで高速呼び出し可能
- ※コンストラクタ、トップレベル関数、
FastKFunctionの呼び出し方について
FastKFunctionは以下3通りの呼び出し方を提供しています。
// 可変長引数
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を利用する場合、以下のように呼び出すことができます。
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引数のみ渡すことができます。
abstract fun call(arg: Any?): T
ベンチマークによる比較
ベンチマーク全体はGitHubに上げてあります。
以下はコンストラクタを対象にしたFastKFunctionのベンチマーク全体です。
コンストラクタ向けベンチマークコード
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)
}
また、ベンチマークの生データもこちらに上げてあります。
ただし、拡張関数とインスタンス関数に関してはベンチマークにおける処理内容が他と異なっているため、単純に比較はできません。
ベンチマークは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はインスタンスパラメータを渡さなくても呼び出せるもののみとします。
コンストラクタ
| 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 |
コンパニオンオブジェクトに定義した関数(ファクトリメソッド)
| 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 |
トップレベル関数
| 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はインスタンスパラメータを渡さなくても呼び出せるもののみとします。
コンストラクタ
| 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 |
コンパニオンオブジェクトに定義した関数(ファクトリメソッド)
| 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 |
トップレベル関数
| 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のインスタンス自動取得やvalueParametersとArgumentBucketによる呼び出しの扱いやすさから、通常利用する範囲では使いやすさの面でもKFunctionより良いものができたんじゃないかなと思っています2。
直近はアドカレに全力を尽くすつもりですが、年末年始〜は自作のマッピングライブラリであるKMapperやKRowMapperにFastKFunctionを組み込んだりしていくつもりです。
スター等々お待ちしております。
- 【Kotlin】KFunctionを高速に呼び出す(前編) - Qiita
- 【Kotlin】KFunctionを高速に呼び出す(後編) - Qiita
- ProjectMapK/KMapper: Object to Object mapper Libraly for Kotlin.
- ProjectMapK/KRowMapper: Spring JDBC RowMapper for Kotlin.
- ProjectMapK/FastKFunction: A wrapper library for fast calls to KFunction.
-
Kotlin Advent Calendar 2020の方に投稿しようか迷いましたが、同じネタで3記事埋めるのは流石にどうかと思ったのでこちらに投げます……。 ↩
-
この優位性は
KFunctionよりも利用の制約を強めていることが最大の理由なので、KFunctionを貶めるような意図は有りません。 ↩





