Kotlinのインライン関数

1概要

Kotlinでは、関数はhttps://en.wikipedia.org/wiki/First-class__function[first-class citizens]なので、他の通常の型と同じように関数を渡したり返したりすることができます。ただし、実行時にこれらの関数を表現すると、いくつかの制限やパフォーマンス上の問題が発生することがあります。

このチュートリアルでは、最初にラムダとジェネリックについて関係のないと思われる2つの問題を列挙します。それから Inline Functions を導入した後、それらがこれらの懸念の両方にどのように対処できるかを見ていきましょう。

2楽園でのトラブル

2.1. コトリンのラムダのオーバーヘッド

Kotlinの一流市民であるという機能の利点の1つは、1つの動作を他の機能に渡すことができるということです。関数をラムダとして渡すことで、意図をより簡潔で優雅な方法で表現できますが、それはストーリーの一部に過ぎません。

ラムダの暗い面を探るために、 filter コレクションへの拡張関数を宣言することによって、輪を作り直そう。

fun <T> Collection<T>.filter(predicate: (T) -> Boolean): Collection<T> =//Omitted

それでは、上記の関数がどのようにJavaにコンパイルされるのかを見てみましょう。パラメータとして渡されている predicate 関数に注目してください。

public static final <T> Collection<T> filter(Collection<T>, kotlin.jvm.functions.Function1<T, Boolean>);

Function1 インターフェースを使用して predicate がどのように処理されるかに注意してください。

さて、これをKotlinで呼ぶと:

sampleCollection.filter { it == 1 }

ラムダコードをラップするために、次のようなものが生成されます。

filter(sampleCollection, new Function1<Integer, Boolean>() {
  @Override
  public Boolean invoke(Integer param) {
    return param == 1;
  }
});
  • high -order function を宣言するたびに、これらの特別な Function __型のインスタンスが少なくとも1つ作成されます 。

なぜKotlinはhttp://wiki.jvmlangsummit.com/images/1/1e/2011 Goetz Lambda.pdf[ __invokedynamicを使う代わりにこれをやらないのですか?簡単に言うと、KotlinはJava 6との互換性を保つためのものであり、 invokedynamic__はJava 7まで利用できません。**

しかし、これで終わりではありません。ご想像のとおり、型のインスタンスを作成するだけでは不十分です。

Kotlinラムダにカプセル化された操作を実際に実行するためには、高階関数 - この場合 filter - は、新しいインスタンスで invoke という名前の特別なメソッドを呼び出す必要があります。 余分な呼び出しのために、結果はより多くのオーバーヘッドです。

つまり、要約すると、関数にラムダを渡すと、次のようになります。

  1. 特殊タイプの少なくとも1つのインスタンスが作成され、

ヒープ 。追加のメソッド呼び出しが常に発生します

  • もう1つインスタンスを割り当て、もう1つ仮想メソッドを呼び出しています。

2.2. クロージャ

前述したように、ラムダを関数に渡すと、Javaの anonymous inner classes と同様に、関数型のインスタンスが作成されます。

後者と同様に、 ラムダ式はその closure 、つまり外側のスコープで宣言された変数にアクセスできます。 ラムダがクロージャから変数を取得すると、Kotlinは取得したラムダコードとともにその変数を格納します。

  • ラムダが変数を取り込むと、余分なメモリ割り当てがさらに悪化します。JVMは、起動のたびに関数型インスタンスを作成します。

キャプチャされないラムダの場合、これらの関数型のインスタンスは1つだけです。

これについてどのように確信がありますか。各collection要素に関数を適用するための関数を宣言することで、別の輪を作り直そう

fun <T> Collection<T>.each(block: (T) -> Unit) {
    for (e in this) block(e)
}

おかしな話ですが、ここでは各collection要素に乱数を掛けます

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    val random = random()

    numbers.each { println(random **  it) }//capturing the random variable
}

そして javap を使ってバイトコードを覗いてみると:

>> javap -c MainKt
public final class MainKt {
  public static final void main();
    Code:
     //Omitted
      51: new           #29                //class MainKt$main$1
      54: dup
      55: fload__1
      56: invokespecial #33                //Method MainKt$main$1."<init>":(F)V
      59: checkcast     #35                //class kotlin/jvm/functions/Function1
      62: invokestatic  #41                //Method CollectionsKt.each:(Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)V
      65: return

次に、インデックス51から、JVMが各呼び出しに対して _MainKt $ main $ _ 1内部クラスの新しいインスタンスを作成することを確認できます。また、インデックス56は、Kotlinが確率変数をどのように取得するかを示しています。これは、キャプチャされた各変数がコンストラクタの引数として渡されるため、メモリのオーバーヘッドが発生することを意味します。

2.3. 消去タイプ

JVM上のジェネリックスに関しては、最初から楽園ではありませんでした。とにかく、Kotlinは実行時にジェネリック型情報を消去します。つまり、 ジェネリッククラスのインスタンスは実行時にその型パラメータを保持しません

たとえば、 List <Int> List <String>のようないくつかのコレクションを宣言すると、実行時に取得できるのは raw __List __だけになります。約束されているように、これは以前の問題とは無関係のようですが、インライン関数が両方の問題に対する共通の解決策であることがわかります。

3インライン関数

3.1. ラムダのオーバーヘッドを取り除く

ラムダを使用すると、追加のメモリ割り当てと追加の仮想メソッド呼び出しにより、ランタイムオーバーヘッドが発生します。そのため、ラムダを使用せずに同じコードを直接実行している場合、実装はより効率的になります。

抽象化と効率のどちらかを選ぶ必要がありますか?**

  • 結局のところ、Kotlinのインライン関数では両方を持つことができます! 私たちの素敵でエレガントなラムダを書くことができます そしてコンパイラはインラインの直接コードを生成します。それについて:

inline fun <T> Collection<T>.each(block: (T) -> Unit) {
    for (e in this) block(e)
}
  • インライン関数を使用する場合、コンパイラは関数本体をインライン化します。

つまり、関数が呼び出される場所に本体を直接代入します。** デフォルトでは、コンパイラは関数自体と渡されたラムダの両方のコードをインライン化します。

たとえば、コンパイラは次のように変換します。

val numbers = listOf(1, 2, 3, 4, 5)
numbers.each { println(it) }

のようなものに:

val numbers = listOf(1, 2, 3, 4, 5)
for (number in numbers)
    println(number)
  • インライン関数を使用するとき、追加のオブジェクト割り当てや追加の仮想メソッド呼び出しはありません。

ただし、インライン関数によって生成コードがかなり大きくなる可能性があるため、特に長い関数についてはインライン関数を使いすぎないでください。

3.2. インラインなし

デフォルトでは、インライン関数に渡されたすべてのラムダもインライン化されます。しかし、いくつかのラムダを noinline キーワードでマークしてインライン化から除外することができます。

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { ... }

3.3. インライン具体化

先に見たように、Kotlinは実行時にジェネリック型情報を消去しますが、インライン関数の場合、この制限を避けることができます。つまり、コンパイラはインライン関数のジェネリック型情報を具体化することができます。

やらなければいけないことは、typeパラメータを __reified __キーワードでマークすることです。

inline fun <reified T> Any.isA(): Boolean = this is T

inline reified がないと、https://www.baeldung.com/kotlin-generics[Kotlin Generics]の記事で詳しく説明しているように、 isA 関数はコンパイルできません。

4. 制限

一般的に、 ラムダが直接呼び出されるか、別のインライン関数に渡される場合に限り、ラムダパラメータを持つ関数をインライン化できます。 それ以外の場合、コンパイラはコンパイラエラーによるインライン展開を防ぎます。

たとえば、Kotlin標準ライブラリの replace 関数を見てみましょう。

inline fun CharSequence.replace(regex: Regex, noinline transform: (MatchResult) -> CharSequence): String =
    regex.replace(this, transform)//passing to a normal function

上記のスニペットは、ラムダ、 __transform、 を通常の関数 replace 、したがって noinline__に渡します。

5. 結論

この記事では、Kotlinのラムダパフォーマンスと型の消去に関する問題について調べました。次に、インライン関数を導入した後、これらがどのように両方の問題に対処できるかを見ました。

ただし、特に生成されるバイトコードサイズが大きくなる可能性があるために関数本体が大きすぎる場合、また途中でいくつかのJVM最適化が失われる可能性がある場合は、これらの関数を使いすぎないようにする必要があります。

いつものように、サンプルコードはhttps://github.com/eugenp/tutorials/tree/master/core-kotlin[GitHubプロジェクト]から入手できます。