0
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

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

この記事はSwift/Kotlin愛好会 Advent Calendar 2020の4日目の記事になりました1

前書き

Kotlin Advent Calendar 2020にて宣伝させて頂いた自作のKFunction高速呼び出しライブラリFastKFunctionについて、使い方と呼び出し性能面の説明をしていきます。

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

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

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

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(+ 必要ならインスタンスパラメータ)」から初期化できます。
例えばインスタンス有りだと以下のようになります。

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

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

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

FastKFunctionの呼び出し方について

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

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

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

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引数のみ渡すことができます。

SingleArgFastKFunctionの呼び出しインターフェース
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: SingleArgFastKFunctioncall呼び出し(1引数の関数のみ)
  • FastKFunction(call): FastKFunctioncall呼び出し
  • FastKFunction(callBy): FastKFunctioncallBy呼び出し(複数引数の関数のみ)
  • KFunction(call): KFunctioncall呼び出し
  • KFunction(callBy): KFunctioncallBy呼び出し

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

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

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

FastKFunction

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

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

コンストラクタ

コンストラクタ (1).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

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

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

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

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

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/SingleArgFastKFunctionKFunction比で高速かつ、JavaConstructorMethodに匹敵する結果となりました。
どころか多くの結果でJavaConstructorMethodよりよい成績になっています。

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

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

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

終わりに

この記事では自作のライブラリFastKFunctionの使い方と、KFunctionに対する速度面での優位性を示しました。
また、ここまでの内容ではアピールしていませんが、objectのインスタンス自動取得valueParametersArgumentBucketによる呼び出しの扱いやすさから、通常利用する範囲では使いやすさの面でもKFunctionより良いものができたんじゃないかなと思っています2

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

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


  1. Kotlin Advent Calendar 2020の方に投稿しようか迷いましたが、同じネタで3記事埋めるのは流石にどうかと思ったのでこちらに投げます……。 

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
0
Help us understand the problem. What are the problem?