在 Kotlin 中傳入 lambdas 給 functions 會產生 runtime overhead。正確地使用 inline
修飾詞可以移除這些 runtime overhead 而提高效能。本文章將介紹如何使用 inline
修飾詞。
Table of Contents
Inline Functions
使用 lambdas 時會產生 runtime overhead。以下的程式碼中,我們呼叫 compute() 10 次,每次都會傳入一個 lambda 給 compute()。
fun compute(x: Int, block: (Int) -> Int): Int { return block(x) } fun main() { var times = 0 var total = 0 for (n in 0 until 10) { total += compute(n) { times++ it * it } } println(times) }
以下是 Kotlin 產生出來的 Java 程式碼。每次在呼叫 compute() 時,會建立一個匿名的 Function1 物件。此外,因為 Function1 物件需要存取外面的 times
變數,因此它還要 capture 一個 closure。所以每次呼叫 compute() 時都會產生這兩個 runtime overhead。若 compute() 不是常常被呼叫,我們可以忽略這些小小的 runtime overhead。但如果在一個 loop 裡一直呼叫compute(),這些累積起來的 runtime overhead 可能會影響效能。
public class MainKt { public static int compute(int x, @NotNull Function1 block) { return ((Number) block.invoke(x)).intValue(); } public static void main() { final Ref.IntRef times = new Ref.IntRef(); times.element = 0; int total = 0; int n = 0; for (byte var3 = 10; n < var3; ++n) { total += compute(n, new Function1() { public Object invoke(Object var1) { return this.invoke(((Number) var1).intValue()); } public int invoke(int it) { int var10001 = times.element++; return it * it; } }); } n = times.element; System.out.println(n); } public static void main(String[] var0) { main(); } }
而 inline
修飾詞就是 Kotlin 提出的解決方案。以下程式碼中,我們在 compute() 前面加上 inline
修飾詞。
inline fun compute(x: Int, block: (Int) -> Int): Int { return block(x) } fun main() { var times = 0 var total = 0 for (n in 0 until 10) { total += compute(n) { times++ it * it } } println(times) }
以下是 Kotlin 產生出來的 Java 程式碼。我們發現 compute() 依然有被宣告出來,但是沒有被呼叫。在 loop 裡面呼叫 compute() 的程式碼,被取代為 lambda 的內容。因此,inline
修飾詞幫我們移除了呼叫 compute() 、建立 Function1 物件、和 capturing 一個 closure 等等 runtime overhead 而提高效能。
public class MainKt { public static int compute(int x, @NotNull Function1 block) { return ((Number) block.invoke(x)).intValue(); } public static void main() { int times = 0; int total = 0; int n = 0; for (byte var3 = 10; n < var3; ++n) { ++times; int var9 = n * n; total += var9; } System.out.println(times); } public static void main(String[] var0) { main(); } }
然而,inline
修飾詞也可能會增長 generated code。另外,要避免 inline function 的程式碼過長。因為如果 loop 中的程式碼過長,會造成 megamorphic 而降低效能。
Non-Local Returns
Non-local returns 指的是在 lambdas 中,那些沒有指定 label 的 return
。Non-local returns 是用來離開 enclosing function。如下程式碼中,nonInlineLoop() 是一般的 function,在 lambda 中,只可以用 return@nonInlineLoop
來離開 nonInlineLoop(),而不能用 return
來離開 main()。
fun nonInlineLoop(array: List<Int>, block: (Int) -> Unit) { for (element in array) { block(element) } } fun main() { nonInlineLoop(listOf(1, 2, 3)) { if (it % 2 == 0) { return@nonInlineLoop // OK } println(it) } nonInlineLoop(listOf(1, 2, 3)) { if (it % 2 == 0) { return // ERROR: A non-local return is not allowed } println(it) } }
但是如果是 inline functions 的話,我們就可以在 lambdas 中用 non-local returns 來離開 enclosing function。如下程式碼中,我們可以用 return@inlineLoop
來離開 inlineLoop(),也可以用 return
來離開 main()。
inline fun inlineLoop(array: List<Int>, block: (Int) -> Unit) { for (element in array) { block(element) } } fun main() { inlineLoop(listOf(1, 2, 3)) { if (it % 2 == 0) { return@inlineLoop // OK } println(it) } inlineLoop(listOf(1, 2, 3)) { if (it % 2 == 0) { return // OK: A non-local return is allowed } println(it) } }
noinline 修飾詞
Kotlin 會自動 inline 一個 inline function 的所有 lambda 參數。但如果你不希望某個參數被 inline 的話,可以使用 noinline
修飾詞,如下。
inline fun compute(x: Int, block1: (Int) -> Int, noinline block2: (Int) -> Int): Int { ... }
corssinline 修飾詞
當你傳給 inline function 的 lambda 不是直接在此 inline function 裡直接被呼叫,而是在其他的 execution context 被呼叫,如 local object 或 nested function 的話,編譯會發生錯誤。因為在這種情況下,該 lambda 不可使用 non-local returns 來離開 enclosing function。
我們可以使用 crossinline
修飾詞來指示該 lambda 不可以使用 non-local returns。這樣就可以成功地編譯,如下程式碼。
interface Compute { fun exe(x: Int): Int } inline fun loop(array: List<Int>, crossinline computeBlock: (Int) -> Int, block: (Int) -> Unit) { for (element in array) { val compute = object : Compute { override fun exe(x: Int): Int = computeBlock(x) } block(compute.exe(element)) } } fun main() { loop(listOf(1, 2, 3), { it * it }) { if (it % 2 == 0) { return@loop } println(it) } }
reified 修飾詞
當 inline function 是一個 generic function 時,我們可以用 reified
修飾詞來修飾 type 參數。這樣的話,我們就可以對 type 參數使用 reflection。
inline fun <reified T> String.numeric(): T { return when (T::class) { Int::class -> this.toInt() as T Long::class -> this.toLong() as T Float::class -> this.toFloat() as T Double::class -> this.toDouble() as T else -> throw UnsupportedOperationException() } } fun main() { val i: Int = "1".numeric() println(i) val d: Double = "3.14".numeric() println(d) }
Inline Properties
在 Kotlin 中,在 class 裡宣告一個 property 時,Kotlin 會對它產生一個 backing field,一個 getter 和一個 setter。我們可以用 inline 修飾詞指示不要產生 backing fields。
我們可以將 inline
修飾詞加在 getter 或 setter 前,也可以加在 property 前。
class Person(var name: String) { inline var displayName: String get() = name set(value) { field = value // ERROR: No backing field name = value } var firstName: String get() = name inline set(value) { field = value // ERROR: No backing field name = value } val lastName: String inline get() = name }
結語
其實在寫 Kotin 程式時,我們呼叫了很多 inline functions。因為 Kotlin 的標準 functions 很多其實是 inline functions,如 let()、apply()、forEach()、filter() 等等。它們讓我們可以使用 functional programming,卻不降低效能。但是也如同本文章所提到,過長的 lambda 會造成 megamorphic 而降低效能。所以,在讀完本文章之後,除了知道要如何建立 inline functions,也要知道保持 lambdas 簡短的重要性。
參考
- Inline functions, Kotlin Docs.