Kotlinでインライン関数を使用する場合


105

インライン関数を使用するとパフォーマンスが向上し、生成されたコードが大きくなる可能性があることはわかっていますが、いつ使用するのが正しいかわかりません。

lock(l) { foo() }

パラメータの関数オブジェクトを作成して呼び出しを生成する代わりに、コンパイラは次のコードを発行できます。(出典

l.lock()
try {
  foo()
}
finally {
  l.unlock()
}

しかし、非インライン関数用にkotlinによって作成された関数オブジェクトがないことがわかりました。どうして?

/**non-inline function**/
fun lock(lock: Lock, block: () -> Unit) {
    lock.lock();
    try {
        block();
    } finally {
        lock.unlock();
    }
}

7
これには2つの主な使用例があります。1つは特定のタイプの高次関数であり、もう1つは具体化された型パラメーターです。インライン関数のドキュメンテーションはこれらをカバーします:kotlinlang.org/docs/reference/inline-functions.html
zsmb13

2
@ zsmb13ありがとうございます。「パラメーターの関数オブジェクトを作成して呼び出しを生成する代わりに、コンパイラーが次のコードを
発行

2
私はその例をtbhのどちらにも入れません。
filthy_wizard 2018年

回答:


279

タイプのラムダ() -> Unit(パラメータなし、戻り値なし)を取る高次関数を作成し、次のように実行するとします。

fun nonInlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

Java用語では、これは次のようなものに変換されます(簡略化されています!):

public void nonInlined(Function block) {
    System.out.println("before");
    block.invoke();
    System.out.println("after");
}

Kotlinから呼び出すと...

nonInlined {
    println("do something here")
}

Function内部では、のインスタンスがここで作成され、ラムダ内のコードをラップします(これも簡単です)。

nonInlined(new Function() {
    @Override
    public void invoke() {
        System.out.println("do something here");
    }
});

したがって、基本的には、この関数を呼び出してラムダを渡すと、常にFunctionオブジェクトのインスタンスが作成されます。


一方、inlineキーワードを使用する場合:

inline fun inlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

次のように呼び出すと:

inlined {
    println("do something here")
}

Functionインスタンスは作成されません。代わりに、blockインライン関数内の呼び出しの周りのコードが呼び出しサイトにコピーされるため、バイトコードで次のようなものが得られます。

System.out.println("before");
System.out.println("do something here");
System.out.println("after");

この場合、新しいインスタンスは作成されません。


19
そもそもFunctionオブジェクトラッパーを使用する利点は何ですか?つまり、なぜすべてがインライン化されていないのですか?
Arturs Vancans 2017年

14
この方法は、あなたはまた、任意の変数などに保存し、周りのパラメータとして関数を渡すことができます
zsmb13

6
@ zsmb13によるすばらしい説明
Yajairo87

2
できます。それらを使用して複雑なことを行うと、最終的にnoinlineおよびcrossinlineキーワードについて知りたいと思います。ドキュメントを参照してください。
zsmb13

2
ドキュメントは、デフォルトでインライン化したくない理由を示しています。インライン化により 、生成されたコードが大きくなる可能性があります。ただし、妥当な方法で(つまり、大きな関数のインライン化を回避して)実行すると、特にループ内の「メガモーフィック」な呼び出しサイトでパフォーマンスが向上します。
CorayThan

43

追加しよう:「使用しない場合inline

1)引数として他の関数を受け入れない単純な関数がある場合、それらをインライン化しても意味がありません。IntelliJは警告を出します:

「...」のインライン化によるパフォーマンスへの影響はわずかです。インライン化は、関数型のパラメーターを持つ関数に最適です

2)「関数型のパラメーターを持つ」関数がある場合でも、コンパイラーがインライン化が機能しないことを通知する場合があります。この例を考えてみましょう:

inline fun calculateNoInline(param: Int, operation: IntMapper): Int {
    val o = operation //compiler does not like this
    return o(param)
}

このコードはエラーでコンパイルされません:

「...」内のインラインパラメータ「operation」の不正な使用。'noinline'修飾子をパラメーター宣言に追加します。

その理由は、コンパイラーがこのコードをインライン化できないためです。operationがオブジェクトにラップされていない場合(これinlineを回避したいために暗黙的に示されます)、変数にどのように割り当てることができますか?この場合、コンパイラは引数の作成を提案しますnoinlineinline単一のnoinline関数を持つ関数があることは意味をなさない、それをしないでください。ただし、関数型のパラメーターが複数ある場合は、必要に応じてそれらのいくつかをインライン化することを検討してください。

だからここにいくつかの提案されたルールがあります:

  • あなたはできるすべての機能型パラメータを直接呼び出すかに渡されたときにインライン他のインライン機能
  • ^の場合はインライン化する必要があります。
  • あなたはできません関数パラメータは、関数内の変数に代入されているときにインライン化
  • あなたはすべきであるあなたの機能タイプのパラメータの少なくとも一つは、使用をインライン化できるのであればインライン展開を検討しnoinline、他人のために。
  • あなたはべきではないインライン巨大な機能は、生成されたバイトコードを考えます。関数の呼び出し元のすべての場所にコピーされます。
  • もう1つの使用例は、reified型パラメーターで、これを使用する必要がありますinlineこちらをお読みください

4
技術的には、ラムダ式を使用しない関数をインライン化することはできますか?..ここでの利点は、関数呼び出しのオーバーヘッドがその場合に回避されることです.. Scalaのような言語がこれを許可します。 INGの
不正-1

3
@ rogue-one Kotlinは、今回のインライン化を禁止していません。言語の作者は、パフォーマンスの利点は取るに足らないものである可能性が高いと単純に主張しています。小さなメソッドは、特に頻繁に実行される場合、JIT最適化中にJVMによってインライン化される可能性があります。inline有害になる可能性があるもう1つのケースは、さまざまな条件分岐など、インライン関数で機能パラメーターが複数回呼び出される場合です。これが原因で、関数の引数のすべてのバイトコードが複製されているケースに遭遇しました。
マイク・ヒル

5

インライン修飾子を使用する場合の最も重要なケースは、util関数のような関数をパラメーター関数で定義する場合です。コレクションまたは文字列処理(のようなfiltermapまたはjoinToString)、または単にスタンドアロン機能が完璧な例です。

これが、インライン修飾子がほとんどライブラリ開発者にとって重要な最適化である理由です。彼らはそれがどのように機能するか、そしてその改善とコストは何かを知っているべきです。関数型パラメーターを使用して独自のutil関数を定義する場合は、プロジェクトでインライン修飾子を使用する必要があります。

関数型パラメーターと具体化型パラメーターがなく、非ローカルの戻り値が必要ない場合は、おそらくインライン修飾子を使用しないでください。これが、Android StudioまたはIDEA IntelliJに関する警告が表示される理由です。

また、コードサイズの問題があります。大規模な関数をインライン化すると、バイトコードがすべての呼び出しサイトにコピーされるため、バイトコードのサイズが大幅に増加する可能性があります。このような場合、関数をリファクタリングして、コードを通常の関数に抽出できます。


4

高次関数は非常に役立ち、reusabilityコードを本当に改善できます。ただし、それらの使用に関する最大の懸念の1つは効率です。ラムダ式はクラス(多くの場合匿名クラス)にコンパイルされ、Javaでのオブジェクト作成は負荷の高い操作です。関数をインライン化することで、すべての利点を維持しながら、高次関数を効果的に使用できます。

ここにインライン関数があります

関数がとしてマークされている場合、inlineコードのコンパイル中に、コンパイラーはすべての関数呼び出しを関数の実際の本体で置き換えます。また、引数として提供されたラムダ式は、実際の本体に置き換えられます。それらは関数としてではなく、実際のコードとして扱われます。

つまり、-インライン->呼び出されるのではなく、コンパイル時に関数の本体コードに置き換えられます...

Kotlinでは、関数を別の関数(いわゆる高階関数)のパラメーターとして使用する方が、Javaより自然に感じられます。

ただし、ラムダの使用にはいくつかの欠点があります。それらは匿名クラス(したがってオブジェクト)であるため、メモリが必要です(アプリの全体的なメソッド数が増えることもあります)。これを回避するために、メソッドをインライン化できます。

fun notInlined(getString: () -> String?) = println(getString())

inline fun inlined(getString: () -> String?) = println(getString())

上記の例から:-これら2つの関数はまったく同じことを行います-getString関数の結果を出力します。1つはインライン化され、もう1つはインライン化されません。

逆コンパイルされたJavaコードを確認すると、メソッドが完全に同一であることがわかります。これは、inlineキーワードがコードを呼び出しサイトにコピーするようコンパイラーに指示するためです。

ただし、以下のように関数タイプを別の関数に渡す場合:

//Compile time error… Illegal usage of inline function type ftOne...
 inline fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/
 }

これを解決するには、関数を次のように書き換えます。

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/}

以下のような高次関数があるとします。

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/}

ここで、ラムダパラメーターが1つだけあり、それを別の関数に渡す場合、コンパイラーはinlineキーワードを使用しないように指示します。したがって、上記の関数を次のように書き換えることができます。

fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/
}

:-キーワードnoinlineも削除する必要がありました。インライン関数にのみ使用できるためです。

このような関数があるとしましょう ->

fun intercept() {
    // ...
    val start = SystemClock.elapsedRealtime()
    val result = doSomethingWeWantToMeasure()
    val duration = SystemClock.elapsedRealtime() - start
    log(duration)
    // ...}

これは問題なく機能しますが、関数のロジックの大部分が測定コードで汚染されているため、同僚が現在の作業を行うのが難しくなっています。:)

インライン関数がこのコードにどのように役立つかを次に示します。

      fun intercept() {
    // ...
    val result = measure { doSomethingWeWantToMeasure() }
    // ...
    }

     inline fun <T> measure(action: () -> T) {
    val start = SystemClock.elapsedRealtime()
    val result = action()
    val duration = SystemClock.elapsedRealtime() - start
    log(duration)
    return result
    }

これで、測定コードの行をスキップすることなく、intercept()関数の主な目的を読み取ることに集中できます。また、他の場所でそのコードを再利用するオプションも利用できます。

インラインでは、measure(myLamda)のようにラムダを渡すのではなく、クロージャー({...})内でラムダ引数を使用して関数を呼び出すことができます。


2

単純なケースとして、suspendブロックを受け取るutil関数を作成する場合があります。このことを考慮。

fun timer(block: () -> Unit) {
    // stuff
    block()
    //stuff
}

fun logic() { }

suspend fun asyncLogic() { }

fun main() {
    timer { logic() }

    // This is an error
    timer { asyncLogic() }
}

この場合、タイマーはサスペンド機能を受け入れません。それを解決するために、それを同様に一時停止させたくなるかもしれません

suspend fun timer(block: suspend () -> Unit) {
    // stuff
    block()
    // stuff
}

ただし、コルーチン/サスペンド関数自体からのみ使用できます。次に、これらのユーティリティの非同期バージョンと非非同期バージョンを作成します。インラインにすれば問題はなくなります。

inline fun timer(block: () -> Unit) {
    // stuff
    block()
    // stuff
}

fun main() {
    // timer can be used from anywhere now
    timer { logic() }

    launch {
        timer { asyncLogic() }
    }
}

これは、エラー状態のkotlinプレイグラウンドです。タイマーをインライン化して解決します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.