LoginSignup
1

More than 1 year has passed since last update.

posted at

updated at

Organization

【Kotlin】KFunctionを高速に呼び出す(後編)

この記事はKotlin Advent Calendar 2020の7日目の記事です。
【Kotlin】KFunctionを高速に呼び出す(前編)の続きです。


前書き

この一連の記事では、Kotlinのリフレクションにおける関数であるKFunctionと、それを高速に呼び出す方法についてまとめます。
また、それぞれの記事の最後にはこのノウハウを用いたFastKFunctionという自作ライブラリを紹介します。

ベンチマークを行った環境

記事内に幾つかJMHのベンチマークスコアが出てきますが、計測は全てRyzen7 3700XWindows10環境で行っています。
ベンチマークの実行はJMH Gradle Pluginme.champeau.gradle.jmh)で行っており、設定は以下のとおりです。

jmhの設定(build.gradle.kts)
jmh {
    fork = 3
    iterations = 3
    threads = 3
    warmupBatchSize = 3
    warmupIterations = 3
}

KFunctionを更に高速に呼び出す方法

前回はKFunctioncall呼び出ししやすくすることによる高速化手法を紹介しました。

素直にKFunctionを呼び出す形での高速化に関しては、恐らくこれが限界だろうと思います。
一方、call呼び出しをしたとしてもJavaConstructor/Methodを直接呼び出す速度には及びませんでした。

この状況で更に高速化するには以下2つの方法が有りますが、今回は前者について紹介します。

  • call呼び出しする時になるべくJavaConstructor/Methodを直接呼び出す
  • callする時にspread演算子を使っているなら)Javaのクラスでラップしてspread演算子の利用を避ける

なお、関連する内容は黒魔術に足を突っ込んでいる感が大きく、一般的にはここまでやらなくていいと思っています。

高速化呼び出しに関する制約

ここで紹介する方法の中にはKFunctionだけで高速呼び出しができないような場合も有ります。

KFunctionは、インスタンスからメソッドリファレンスで取得した場合、呼び出し時にインスタンスを渡す必要はありません。
一方、static関数でないJavaMethodを呼び出す場合にはインスタンスが必要となります。
ここで、KFunctionからインスタンスを取得する方法は、自分が知る限り一部の例外を除いて有りません。

以上の通り、KFunctionの高速呼び出しにインスタンスも必要になる場合はKFunctionだけで高速呼び出しができないことが有ります。

KFunctionからConstructor/Methodを取得する

KFunctionにはjavaConstructorjavaMethodというプロパティが用意されており、KFunctionの実体に合わせてそれぞれConstructor/Methodを取得することができます。

// 呼び出し対象関数
val function: KFunction<T>

val javaConstructor: Constructor<T>? = function.javaConstructor
val javaMethod: Method? = function.javaMethod

それぞれの状況での高速な呼び出し方法

JavaConstructor/Methodを直接呼び出すには、KFunctionの定義元ごとに幾つかの条件が有りますが、大まかに以下3パターンに分けられます。

  1. コンストラクタ
  2. トップレベル関数
  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など、Kotlinobjectに定義された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

それぞれのスコア

コンストラクタの場合

コンストラクタ (2).png

トップレベル関数の場合

トップレベル関数 (3).png

トップレベル拡張関数の場合

トップレベル拡張関数 (1).png

インスタンス関数の場合

インスタンス関数 (1).png

オブジェクトに定義されたインスタンス関数の場合

オブジェクトに定義されたインスタンス関数.png

終わりに

この記事では、KFunctionからJavaConstructor/Methodを取得し直接呼び出すことによる高速化方法と、どれくらい高速化するかについてまとめました。
ライブラリを作るように、包括的にやる中でこの方法を取り込む場合はオーバーヘッドが出たりして必ずしもこのスコア通りに高速化する訳ではありませんが、それでも効果が有ることを示せたと思います。

書いた後で「スプレッド演算子のコストがとても重いからJavaのクラスでラップして使わないようにすると高速化する」ということを知ったり、書けなかった工夫も幾つか有りますが、ある程度綺麗にまとめられたかなと思います。
ここまでやらかしてでも高速性を追求してみたい方のお役に立てれば幸いです。

宣伝: FastKFunctionについて

最後に宣伝です。

ここで紹介したノウハウを用いたKFunctionの高速呼び出しが行えるライブラリ、FastKFunctionを公開しています。
「自分で作りたくはないけどKFunctionを高速で呼び出したい」という方は是非使ってみて下さい。
スターもお待ちしております。

関連して、ProjectMapKというオーガナイゼーションで幾つかのマッピングライブラリを公開しています。
FastKFunctionにもこれから対応していく予定ですので、こちらも利用・スターお待ちしております。


  1. --no-daemonしている理由は次の記事の通りです 【JMH】JMH Gradle PluginはWindows 10で正常に動作しない【Gradle】 - Qiita 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1