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的运行动态的方法内联不同,也是不同层次的优化,应当注意区分。

猜你喜欢

转载自blog.csdn.net/a860MHz/article/details/90755079