この記事はKotlin Advent Calendar 2020の7日目の記事です。
【Kotlin】KFunctionを高速に呼び出す(前編)の続きです。
前書き
この一連の記事では、Kotlin
のリフレクションにおける関数であるKFunction
と、それを高速に呼び出す方法についてまとめます。
また、それぞれの記事の最後にはこのノウハウを用いたFastKFunction
という自作ライブラリを紹介します。
ベンチマークを行った環境
記事内に幾つかJMH
のベンチマークスコアが出てきますが、計測は全てRyzen7 3700X
のWindows10
環境で行っています。
ベンチマークの実行はJMH Gradle Plugin
(me.champeau.gradle.jmh
)で行っており、設定は以下のとおりです。
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
を取得することができます。
// 呼び出し対象関数
val function: KFunction<T>
val javaConstructor: Constructor<T>? = function.javaConstructor
val javaMethod: Method? = function.javaMethod
それぞれの状況での高速な呼び出し方法
Java
のConstructor
/Method
を直接呼び出すには、KFunction
の定義元ごとに幾つかの条件が有りますが、大まかに以下3パターンに分けられます。
- コンストラクタ
- トップレベル関数
- インスタンス関数
拡張関数に関しては、2と3にそれぞれ含まれます。
以下、それぞれに関する呼び出し方についてまとめます。
コンストラクタの場合
コンストラクタの場合はjavaConstructor
をそのまま呼び出すことで高速な呼び出しができます。
data class Foo(val arg1: Int, ...)
// 呼び出し対象関数
val function: KFunction<Foo> = ::Foo
val javaConstructor: Constructor<Foo> = function.javaConstructor!!
val result = javaConstructor.newInstance(/* 引数 */)
トップレベル関数の場合
トップレベル関数の場合、実体はstatic
メソッドになるため、javaMethod
をインスタンス無しで呼び出すことで高速な呼び出しができます。
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
が必要になる点が異なります。
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はタイプを持たないためキャストが必要
インスタンス関数の場合
インスタンス関数の場合、呼び出しにインスタンスが必要になります。
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
のインスタンスを持ってくることができるため、KFunction
だけからも高速呼び出しが可能です。
// 呼び出し対象関数
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
が必要になります。
// 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
の呼び出しに関しては、取得方法によってスコアに差が出る場合も有るため、そのような内容に関してはインスタンスからメソッドリファレンスで取得した場合に絞ってスコアを出します。
インスタンスに定義された拡張関数はインスタンスからメソッドリファレンスで取得できないため、今回のベンチマークからは外しています。
ベンチマークの詳細
ソースコード
今回のベンチマークで呼び出し対象としたコードです。
呼び出し対象関数群
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
)
ベンチマークのコード全体は以下リポジトリのsrc/jmh
配下に書かれていて、gradle jmh --no-daemon
で実行できます1。
ただし、全体の実行には2時間少しかかるのでご注意ください。
結果の生データ
記事とは直接関係しない余計なスコアも含まれていますが、結果の生データは以下の通りです。
生データ
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
それぞれのスコア
コンストラクタの場合
トップレベル関数の場合
トップレベル拡張関数の場合
インスタンス関数の場合
オブジェクトに定義されたインスタンス関数の場合
終わりに
この記事では、KFunction
からJava
のConstructor
/Method
を取得し直接呼び出すことによる高速化方法と、どれくらい高速化するかについてまとめました。
ライブラリを作るように、包括的にやる中でこの方法を取り込む場合はオーバーヘッドが出たりして必ずしもこのスコア通りに高速化する訳ではありませんが、それでも効果が有ることを示せたと思います。
書いた後で「スプレッド演算子のコストがとても重いからJava
のクラスでラップして使わないようにすると高速化する」ということを知ったり、書けなかった工夫も幾つか有りますが、ある程度綺麗にまとめられたかなと思います。
ここまでやらかしてでも高速性を追求してみたい方のお役に立てれば幸いです。
宣伝: FastKFunctionについて
最後に宣伝です。
ここで紹介したノウハウを用いたKFunction
の高速呼び出しが行えるライブラリ、FastKFunction
を公開しています。
「自分で作りたくはないけどKFunction
を高速で呼び出したい」という方は是非使ってみて下さい。
スターもお待ちしております。
関連して、ProjectMapK
というオーガナイゼーションで幾つかのマッピングライブラリを公開しています。
FastKFunction
にもこれから対応していく予定ですので、こちらも利用・スターお待ちしております。
- ProjectMapK/KMapper: Mapper Libraly for Kotlin.
- ProjectMapK/KRowMapper: Spring JDBC RowMapper for Kotlin.
-
--no-daemon
している理由は次の記事の通りです 【JMH】JMH Gradle PluginはWindows 10で正常に動作しない【Gradle】 - Qiita ↩