Kotlinのクロスインラインとnoinlineの違い
1. 概要
インライン化は、多くのコンパイラがコードのパフォーマンスを最適化するために使用する最も古いトリックの1つです。 したがって、当然のことながら、パフォーマンスを向上させ、フットプリントを小さくするために、Kotlinもこのトリックを利用しています。
このチュートリアルでは、Kotlinでのラムダ関数のインライン化の2つの結果、noinlineとcrossinlineについて説明します。
2. クイックインラインリフレッシャー
Kotlinのインライン関数は、ラムダ式ごとに余分なメモリ割り当てや不要なメソッド呼び出しを回避するのに役立ちます。 たとえば、この簡単な例では、次のようになります。
inline fun execute(action: () -> Unit) {
action()
}
Kotlinは、 execute()関数への呼び出しと呼び出しサイトのラムダ関数の両方をインライン化します。 したがって、この関数を別の関数から呼び出すと、次のようになります。
fun main() {
execute {
print("Hello ")
print("World")
}
}
その場合、インライン結果は次のようになります。
fun main() {
print("Hello ")
print("World")
}
上に示したように、 execute()メソッド呼び出しの兆候はなく、もちろん、ラムダの兆候もありません。 したがって、インライン化により、使用する割り当てが少なくなり、アプリケーションのパフォーマンスが向上します。
3. noinline効果
デフォルトでは、 inlineキーワードは、メソッド呼び出しと、呼び出しサイトで渡されたすべてのラムダ関数をインライン化するようにコンパイラーに指示します。
inline fun executeAll(action1: () -> Unit, action2: () -> Unit) {
// omitted
}
上記の例では、Kotlinは2つのラムダ関数(action1とaction2)とともに executeAll()メソッド呼び出しをインライン化します。
場合によっては、何らかの理由で、渡されたラムダ関数の一部をインライン化から除外したい場合があります。 その場合、 noinline 修飾子を使用して、マークされたラムダ関数をインライン化から除外できます。
inline fun executeAll(action1: () -> Unit, noinline action2: () -> Unit) {
action1()
action2()
}
上記の例では、Kotlinは executeAll()メソッド呼び出しとaction1ラムダをインライン化します。 ただし、 noinline 修飾子があるため、action2ラムダ関数では同じことはできません。
基本的に、Kotlinは次のコードをコンパイルすることが期待できます。
fun main() {
executeAll({ print("Hello") }, { print(" World") })
}
次のようなものに:
fun main() {
print("Hello")
val action2 = { print(" World") }
action2()
}
上に示したように、 executeAll()メソッド呼び出しの兆候はありません。 さらに、最初のラムダ関数は明らかにインライン化されています。 ただし、2番目のラムダ関数はインライン化されずにそのまま存在します。
3.1. バイトコード表現
先ほど、Kotlinはmain関数でインライン関数呼び出しを次のようにコンパイルすると述べました。
fun main() {
print("Hello")
val action2 = { print(" World") }
action2()
}
このメンタルモデルは詳細をよりよく理解するのに役立ちますが、Kotlinは元のコードから別のKotlinまたはJavaコードを生成しません。 実際の詳細を確認するには、この場合、Kotlinがバイトコードを生成する方法を確認する必要があります。
これを行うには、Kotlinコードをコンパイルし、javapを使用してバイトコードを確認します。
>> kotlinc Inlines.kt
>> javap -c -p com.baeldung.crossinline.InlinesKt
// omitted
public static final void main();
Code:
0: getstatic #41 // Field com/baeldung/crossinline/InlinesKt$main$2.INSTANCE:LInlinesKt$main$2;
3: checkcast #18 // class kotlin/jvm/functions/Function0
6: astore_0. // storing the lambda
7: iconst_0
8: istore_1
9: iconst_0
10: istore_2
11: ldc #43 // String Hello
13: astore_3
14: iconst_0
15: istore 4
17: getstatic #49 // Field java/lang/System.out:Ljava/io/PrintStream;
20: aload_3
21: invokevirtual #55 // Method java/io/PrintStream.print:(Ljava/lang/Object;)V
24: nop
25: aload_0 // the lambda at index 5
26: invokeinterface #22, 1 // InterfaceMethod kotlin/jvm/functions/Function0.invoke:()LObject;
31: pop
32: nop
33: return
// use -v flag to see the following line
InnerClasses:
static final #37; // class com/baeldung/crossinline/InlinesKt$main$2
上記のバイトコードから、いくつかのポイントを理解できます。
- executeAll()メソッドの呼び出しを表す invokestatic がないため、Kotlinはこのメソッド呼び出しを確実にインライン化しました
- インデックス11から21は、 System.out.print( “Hello”)への直接呼び出しを表すため、最初のラムダ関数もインライン化されます
- インデックス5では、Kotlinの Function0 のサブタイプ(インデックス3)である com / baeldung / crossinline / InlinesKt $ main $2シングルトンインスタンスを取得しています。 Kotlinは、引数なしでラムダ関数をコンパイルし、UnitはFunction0として型を返します。 インデックス26では、この Function0でinvoke()メソッドを呼び出しています。これは、基本的に、インライン化されていないラムダ関数を呼び出すことと同じです。
4. クロスインライン効果
Kotlinでは、通常の非修飾リターンを使用して、名前付き関数、無名関数、またはインライン関数を終了することしかできません。 ラムダを終了するには、ラベルを使用する必要があります( return @ label のように)。 ラムダで通常のreturnを使用することはできません。これは、囲んでいる関数を終了するためです。
fun foo() {
val f = {
println("Hello")
return // won't compile
}
}
ここで、Kotlinコンパイラは、ラムダ内でreturnを使用して囲んでいる関数を終了することを許可しません。 このようなreturnは、非ローカルreturnと呼ばれます。
ラムダは呼び出しサイトでインライン化されるため、インライン関数で非ローカル制御フローを使用できます。
inline fun foo(f: () -> Unit) {
f()
}
fun main() {
foo {
println("Hello World")
return
}
}
ラムダを終了している場合でも、ラムダ自体はmain関数にインライン化されています。 したがって、この return ステートメントは、ラムダではなく、main関数で直接発生します。 これが、インライン関数内で通常のリターンを使用できる理由です。
これを考えると、ラムダ関数をインライン関数から非インライン関数に渡すとどうなりますか? それをチェックしよう:
inline fun foo(f: () -> Unit) {
bar { f() }
}
fun bar(f: () -> Unit) {
f()
}
ここでは、fラムダをインライン関数から非インライン関数に渡します。
Kotlinが許可した場合、これがどのような問題を引き起こすかを見てみましょう。
4.1. 問題
Kotlinが上記の機能を許可した場合、実際には呼び出しサイトで非ローカルreturnを使用できます。
fun main() {
foo {
println("Hello World")
return
}
}
foo はインライン関数であるため、Kotlinは呼び出しサイトでインライン関数を使用します。 ラムダについても同じことが言えます。 したがって、1日の終わりに、Kotlinはmain関数を次のようなものにコンパイルします。
fun main() {
bar {
println("Hello World")
return // root cause
}
}
Kotlinがここにいくつかの呼び出しをインライン化したにもかかわらず、バー関数の呼び出しはそのままでした。 したがって、Kotlinがインライン関数fooに対して非ローカルreturnを許可した場合、関数のラムダで非ローカル制御フローを使用しないというルールに最終的に違反します。バー。
それがこのルールの背後にある理由です。 したがって、要約すると、次の3つの場合に非ローカル制御フローを使用できます。
- 通常の名前付き関数
- 匿名関数
- インライン関数は、ラムダを直接呼び出すか、別のインライン関数に渡す場合にのみ機能します
4.2. ソリューション
ラムダ関数で非ローカル制御フローを使用しないことがわかっている場合があります。 同時に、インライン関数の利点を活用したい場合もあります。
このような状況では、インライン関数のラムダパラメーターをcrossinline修飾子でマークできます。
inline fun foo(crossinline f: () -> Unit) {
bar { f() }
}
fun bar(f: () -> Unit) {
f()
}
crossinline 修飾子を使用すると、上記のコードがコンパイルされます。 ただし、コールサイトで非ローカルreturnを使用することはできません。
fun main() {
foo {
println("Hello World")
return // won't compile
}
}
これが、 crossinline の存在目的全体です。つまり、ラムダで非ローカル制御フローを使用する機能を失いながら、インライン関数の効率から利益を得ることができます。
4.3. noinline対。 クロスインライン
驚いたことに、 noinline 修飾子を使用して、fooおよびbar関数をコンパイルすることもできます。
inline fun foo(noinline f: () -> Unit) {
bar { f() }
}
fun bar(f: () -> Unit) {
f()
}
これは確かにコンパイルされます。 ただし、インライン関数の効率と非ローカルリターンを使用する機能の両方が失われます。 Kotlinでさえ、コンパイル中にこの事実について警告します。
>> kotlinc Inlines.kt
Inlines.kt:12:1: warning: expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types
inline fun foo(noinline f: () -> Unit) {
^
ここで、Kotlinコンパイラは、すべてのラムダが noinline でマークされている場合、インライン関数からそれほど多くの利益を得ることができないと明示的に述べています。
5. 結論
この記事では、noinline修飾子とcrossinline修飾子の違いを評価しました。 より具体的には、前者を使用すると、一部のラムダパラメーターをインライン化から除外できます。
さらに、インライン関数から非インライン関数にラムダを渡すときにnoinlineを使用できます。 ただし、この目的でこの修飾子を使用すると、インライン化の効率が低下します。 同様に、 crossinline 修飾子は同じシナリオに適用できますが、大きな違いが1つあります。それでもインライン化の優位性から利益を得ることができます。
いつものように、すべての例はGitHubでから入手できます。