この記事は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
を貶めるような意図は有りません。 ↩