Kotlin:Inline Functions 教學

Photo by Connor Mollison
Photo by Connor Mollison
在 Kotlin 中傳入 lambdas 給 functions 會產生 runtime overhead。正確地使用 inline 修飾詞可以移除這些 runtime overhead 而提高效能。本文章將介紹如何使用 inline 修飾詞。

在 Kotlin 中傳入 lambdas 給 functions 會產生 runtime overhead。正確地使用 inline 修飾詞可以移除這些 runtime overhead 而提高效能。本文章將介紹如何使用 inline 修飾詞。

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 簡短的重要性。

參考

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Hans-Jurgen Mager on Unsplash
Read More

Kotlin Coroutine 教學

Kotlin 的 coroutine 是用來取代 thread。它不會阻塞 thread,而且還可以被取消。Coroutine core 會幫你管理 thread 的數量,讓你不需要自行管理,這也可以避免不小心建立過多的 thread。
Read More