Kotlin字節碼解析-3 函數內聯

1. 背景

在JAVA8中,Lamdba表達式通過invokedynamic指令實現的,通過invokedynamic可以避免編譯期硬編碼生成內部匿名類的實現,而是由JIT在運行時才產生相應的接入點代碼,顯著減少靜態生成的類和字節碼大小。

由於Kotlin支持JAVA6,Kotlin對於Lamdba表達式的支持不得不通過編譯期硬編碼的方式實現,導致大量的內部匿名類,爲了避免此問題,Kotlin引入inline、noinline、crossinline等關鍵字,以減少額外生成的匿名類數以及函數執行的時間開銷。

本文將分析inline、noinline、crossinline關鍵字的字節碼實現。

2. inline關鍵字

fun main() {
    fun3{
        println("lambda1")
    }
}

fun fun3(funparam: () -> Unit) {
    println("fun1 starting")
    funparam()
    println("fun1 end")
}

上文是一個簡單的Lamdba的調用,正如文中所描述的,編譯器會針對Lamdba中的邏輯自動生成一個內部匿名類,外部代碼通過調用此內部匿名類的invoke方法執行Lamdba邏輯。

通過下面的例程,我們看看Kotlin的內聯是如何操作的。

fun main() {
    fun4{
        println("lambda1")
    }
}

inline fun fun4(funparam: () -> Unit) {
    println("fun1 starting")
    funparam()
    println("fun1 end")
}

執行結果如下:

fun1 starting
lambda1
fun1 end

反編譯字節碼如下:

public final class T4Kt {
  public static final void main();
    Code:
       0: iconst_0
       1: istore_0
       2: ldc           #11                 // String fun1 starting
       4: astore_1
       5: iconst_0
       6: istore_2
       7: getstatic     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;
      10: aload_1
      11: invokevirtual #23                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      14: iconst_0
      15: istore_3
      16: ldc           #25                 // String lambda1
      18: astore        4
      20: iconst_0
      21: istore        5
      23: getstatic     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;
      26: aload         4
      28: invokevirtual #23                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      31: nop
      32: ldc           #27                 // String fun1 end
      34: astore_1
      35: iconst_0
      36: istore_2
      37: getstatic     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;
      40: aload_1
      41: invokevirtual #23                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      44: nop
      45: return

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #9                  // Method main:()V
       3: return
}

使用inline關鍵字修飾fun4後,編譯結果中沒有了內部匿名類,Lambda表達式邏輯、fun4就均被內聯到public static final void main()方法中 ,程序佔用資源減少並且執行效率有提升。

3. 非局部返回

由於方法內聯後,內聯函數函數體和Lambda邏輯都會直接替代具體的調用,如果Lambda邏輯中包含return語句,則導致非局部返回。

fun main() {
    fun5{
        println("lambda1")
        return
    }
}

inline fun fun5(funparam: () -> Unit) {
    println("fun1 starting")
    funparam()
    println("fun1 end")
}

執行結果如下:

fun1 starting
lambda1

我們注意到,最後的"fun1 end"預計沒有被執行。

  public static final void main();
    Code:
       0: iconst_0
       1: istore_0
       2: ldc           #11                 // String fun1 starting
       4: astore_1
       5: iconst_0
       6: istore_2
       7: getstatic     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;
      10: aload_1
      11: invokevirtual #23                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      14: iconst_0
      15: istore_3
      16: ldc           #25                 // String lambda1
      18: astore        4
      20: iconst_0
      21: istore        5
      23: getstatic     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;
      26: aload         4
      28: invokevirtual #23                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      31: return

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #9                  // Method main:()V
       3: return

從字節碼中也可以看出println(“fun1 end”)語句根本就沒有被執行。

非局部返回尤其在循環控制中顯得特別有用,比如Kotlin的forEach接口,它接收的就是一個Lambda參數,由於它也是一個內聯函數,所以可以直接在它調用的Lambda中執行return退出上一層的程序。

4. crossinline

雖然某些場景下非局部返回可能非常有用,但還是可能存在危險,我們可以通過crossline關鍵字防止非局部返回的發生。

fun main() {
    fun5{
        println("lambda1")
        return
    }
}

inline fun fun5(crossinline funparam: () -> Unit) {
    println("fun1 starting")
    funparam()
    println("fun1 end")
}

執行結果是’return’ is not allowed here

5. 總結

出於Kotlin語言定位的考量,Kotlin當前採用方法內聯對Lambda帶來的額外開銷進行優化,而不是JAVA7引入的invokedynamic。

Kotlin的方法內聯是編譯器硬編碼的方式,與JIT的運行動態的方法內聯不同,也是不同層次的優化,應當注意區分。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章