您当前的位置: 首页 >  jvm
  • 0浏览

    0关注

    674博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Kotlin (JVM) 语言层面的性能开销 —— Lambda 表达式.

沙漠一只雕得儿得儿 发布时间:2021-12-01 11:06:34 ,浏览量:0

Ref: 

  • https://medium.com/@yangweigbh/how-kotlin-optimize-lambda-under-the-hood-3f7eb286736c
  • https://medium.com/@yangweigbh/how-kotlin-lambda-capture-variable-ef90e11e531d

本文从 JVM 字节码的层面,简易分析了 Kotlin 的 Lambda 表达式的基本原理。理解其底层的实现原理有利于写出更高效率的代码。

本文基本上全文参考了上述两篇博客,文中示例全部使用 Kotlin 1.4 进行了重新验证.

Lambda 表达式是 Java 8 提供的一项重大改变,其实现原理是依赖于编译器在编译阶段将 Lambda 表达式编译成匿名内部类,在这点上可以认为,Java 8 的 Lambda 表达式是本质为匿名内部类的语法糖.

Kotlin 自诞生起便天然地支持 Lambda 表达式,其背后的实现原理是否与 Java 相同呢?下面例举几个不同语法形式的 Kotlin Labmda 函数,通过反编译其字节码,来看看其背后的实现原理,以及存在的性能开销。

- Kotlin Lambda 表达式的性能问题

与 Java 8 Lambda 的实现原理类似,Kotlin 在 JVM 上对 Lambda 的支持也是通过编译器将 Lambda 表达式编译为内部类来实现的,可以使用 Android Studio 的 KotlinBytecode 工具 decompile 字节码进行查看. 其实可以认为,JVM 上的 Labmda 实现应该都会是同样的原理. 但是在使用 Kotlin Labmda 时,针对不同的语法,编译器还是会有不同的处理,这其中的差异会有一定的性能隐患: 1) 最简易的 Lambda 的表达式 new Thread{ print("hello") } 编译后,实际上会生成一个实现 Runnable 接口的静态内部类,如下: static class RunnableImp implement Runnable{ RunnableImp INSTANCE; @Override public void run(){ print("hello); } static { INSTANCE = new RunnableImp(); } } 该静态类是单例的,我们今后每次执行该表达式,都会直接复用,并不会重新创建,因此具有性能优化的效果. 2) Lambda 表达式中访问外部作用域的成员的情形 val msg = "hello" new Thread{ print(msg) } 不同于情形1,这种情形下,因为内部类访问了外部成员,因此每次都需要创建新的内部类,因此并不会有跟情形1一样的优化效果. 3) // Kotlin object expression new Thread(object : Runnable { override fun run() { println("hello world") } }) 其对应的反编译Java代码为 new Thread((Runnable)new Runnable { override public void run() { println("hello world") } }) 不习惯 Lambda 的同学,可能还是习惯与使用 Object expression 这样类似于 Java 语法匿名内部类的写法,但是实际上,这种使用方式是存在性能损耗的,因为它并不会使用如 情形1 那种形式的复用内部类的性能优化手段,而是每次都重新创建,因此性能上肯定不如情形1,因此在语法层面,如非必要,则尽量避免使用 object expression 形式的语法. 4) Kotlin Lambda 中可以修改其访问到的外部成员 Java 的匿名内部类和 Lambda 表达式,若有访问到外部成员变量,则要求该变量必须为 final,其中原因此处不再赘述。因此,在内部类中或者 Lambda 中若要修改该外部变量是不可能的,真有这样的需求,通常的做法也是创建新的引用来对其进行操作. // java public void sayHi(){ final String msg = "hello"; new Thread(()->{ // 编译失败 msg += "world"; System.out.println(msg); }); } 不同于 Java ,Kotlin "似乎"移除了这样的限制,我们在 Lambda 中竟然可以修改外部作用域的变量. // kotlin fun sayHi() { var msg = "hello" Thread { // 通过编译 msg += "world" println(msg) } } 其实不然,反编译上述代码的字节码为 Java 代码如下: static class RunnableImp implement Runnable{ private final ObjectRef $msg; public RunnableImp(ObjectRef $msg){ this.$msg = $msg; } @Override public void run(){ String var1 = Intrinsics.stringPlus((String)this.$msg.element, " world"); boolean var2 = false; System.out.println(var1); } } 可以其使用了一个 ObjectRef 持有了外部的引用,故而可以对实现对该变量的操作. 与我们通常使用 Java 时的处理方式本质是一样的,只是 Kotlin 在编译层面避免了我们书写更多的模板代码. ObjectRef 是 Kotlin SDK 中的类,源码如下: public static final class ObjectRef implements Serializable { public T element; @Override public String toString() { return String.valueOf(element); } } 总结: 1)Kotlin Lambda 实现本质依旧是在编译阶段将 Lambda 转换了等价的内部类实现; 2)Lambda 未访问作用域外部的变量时,每次执行都会使用同一个内部类;而若 Lambda 访问了作用域外部的变量,每次执行 Lambda 表达式,实质上都需要重新创建一个内部类;在这点上,Kotlin 其实进行了一定的性能优化. 3) 使用 object expression 可以实现与 Java 匿名内部类的同样效果,但是 object expression 每次都会创建新的内部类,因此若在语法层面可以使用 Labmda 代替 object expression,那么则应该尽量使用 Lambda 表达式. 3) Kotlin Lambda 中若访问外部成员,并不要求外部成员“不可变”,并且可以在其内部修改该变量.

todo:

  • 啥是 Lambda
  • Java Lambda 原理
  • Android 编译器 desugar
  • Kotlin Lambda 原理及性能
  • Android Kotlin 编译.
关注
打赏
1657159701
查看更多评论
立即登录/注册

微信扫码登录

0.0414s