重学 Kotlin —— inline,包治百病的性能良药?

简介: 重学 Kotlin —— inline,包治百病的性能良药?

重学 Kotlin 已经来到了第三期,前面已经介绍了:

object,史上最 “快” 单例 ?

typealias ,穿了马甲,我就不认识你了?

今天的主角是 inline ,这不是一个 Kotlin 特有的概念,大多数编程语言都支持内联。

内联函数 的语义很简单: 把函数体复制粘贴到函数调用处 。使用起来也毫无困难,用 inline关键字修饰函数即可。


然而问题的关键并不是如何使用 inline ,而是什么时候使用  inline ?  既然 Kotlin 提供了内联,它肯定是为了性能优化而存在的,那么,它又真的是包治百病的性能良药吗?


今天,我们就一起来刨根挖底,寻找一下答案。


目录


  1. inline 的本质
  2. 建议我不要用 inline ?
  3. Java 支持内联吗?
  4. 拯救 Lambda
  5. Java 是如何优化 Lambda 的?
  6. 不想内联怎么办?
  7. 如何从 Lambda 返回?
  8. 最后


inline 的本质


前面已经说过  inline 就是 把函数体复制粘贴到函数调用处 ,完全是编译器的小把戏。本着严谨科学的态度,我们还是来反编译验证一下。

inline fun test() {
    println("I'm a inline function")
}
复制代码fun run() { test() }


run() 函数中调用了内联函数 test()。反编译查看对应的 java 代码:

public static final void test() {
    String var1 = "I'm a inline function";
    System.out.println(var1);
}
public static final void run() {
    String var1 = "I'm a inline function";
    System.out.println(var1);
}
复制代码


可以看到 run() 函数中并没有直接调用 test() 函数,而是把 test() 函数的代码直接放入自己的函数体中。这就是 inline 的功效。


那么,问题就来了。这样就可以提高运行效率吗?如果可以,为什么?

我们先从 JVM 的方法执行机制说起。


JVM 进行方法调用和方法执行依赖 栈帧,每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。


线程的栈帧是存储在虚拟机栈中,以上面示例代码的 未内联 版本为例,对应的方法执行过程和对应的栈帧结构如下所示:

image.png

未内联的情况下,整个执行过程中会产生两个方法栈帧,每一个方法栈帧都包括了 局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息

使用内联的情况下,只需要一个方法栈帧,降低了方法调用的成本。

乍一看,的确的提高了运行效率,毕竟少用一个栈帧嘛。

然而?


建议不要使用 inline ?



一切看起来都很美好,除了 IDE 给我的刺眼提示。

Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types

大致意思是在这里使用内联对性能的影响微乎其微,或者说没有什么意义。Kotlin 的内联最好用在函数参数类型中。

不急着解释,首先来一发灵魂拷问。


Java 支持内联吗?


你可以说不支持,因为 Java 并没有提供类似 inline 的显示声明内联函数的方法。

但是 JVM 是支持的。Java 把内联优化交给虚拟机来进行,从而避免开发者的滥用。

典型的一种滥用, 内联超长方法 ,极大的增大字节码长度,反而得不偿失。你可以注意 Kotlin 标准库中的内联函数,基本都是简短的函数。


对于普通的函数调用,JVM 已经提供了足够的内联支持。因此,在 Kotlin 中,没有必要为普通函数使用内联,交给 JVM 就行了。


另外,Java 代码是由 javac 编译的,Kotlin 代码是由 kotlinc 编译的,而 JVM 可以对字节码做统一的内联优化。所以,可以推断出,不管是 javac ,还是 kotlinc,在编译期是没有内联优化的。


至于 JVM 具体的内联优化机制,我了解的并不多,这里就不做过多介绍了。后续如果我看到相关资料,会到这里进行补充。

所以,上一节中 IDE 给开发者的提示就很明了了。

JVM 已经提供了内联支持,所以没有必要在 Kotlin 中内联普通函数。

那么问题又来了。 既然 JVM 已经支持内联优化,Kotlin 的内联存在的意义是什么 ? 答案就是 Lambda  。


拯救 Lambda



为什么要拯救 Lambda,我们首先得知道Kotlin 的 Lambda 对于 JVM 而言究竟是什么。

Kotlin 标准库中有一个叫 runCatching  的函数,我在这里实现一个简化版本 runCatch ,参数是一个函数类型。


fun runCatch(block: () -> Unit){
    try {
        block()
    }catch (e:Exception){
        e.printStackTrace()
    }
}
fun run(){
    runCatch { println("xxx") }
}
复制代码


反编译生成的 Java 代码如下所示:

public final class InlineKt {
    public static final void runCatch(@NotNull Function0<Unit> block) {
        Intrinsics.checkParameterIsNotNull(block, (String)"block");
        try {
            block.invoke();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static final void run() {
        InlineKt.runCatch((Function0<Unit>)((Function0)run.1.INSTANCE));
    }
}
static final class InlineKt.run.1 extends Lambda implements Function0<Unit> {
    public static final InlineKt.run.1 INSTANCE = new /* invalid duplicate definition of identical inner class */;
    public final void invoke() {
        String string = "xxx";
        boolean bl = false;
        System.out.println((Object)string);
    }
    InlineKt.run.1() {
    }
}
复制代码


Kotlin 自诞生之初,就以 兼容 Java 为首要目标。因此,Kotlin 对于 Lambda 表达式的处理是编译生成匿名类。


经过编译器编译之后, runCatch() 方法中的 Lambda 参数被替换为 Function0<Unit> 类型,在 run() 方法中实际调用 runCatch()  时传入的参数是实现了 Function0<Unit>  接口的 InlineKt.run.1  ,并重写 了 invoke() 方法。


所以,调用 runCatch() 的时候,会创建一个额外的类 InlineKt.run.1。这是 Lambda 没有捕捉变量的场景。如果捕捉了变量,表现会怎么样?

fun runCatch(block: () -> Unit){
    try {
        block()
    }catch (e:Exception){
        e.printStackTrace()
    }
}
fun run(){
    var message = "xxx"
    runCatch { println(message) }
}
复制代码


在 Lambda 内部捕捉了外部变量 message  ,其对应的 java 代码如下所示:

public final class InlineKt {
    public static final void runCatch(@NotNull Function0<Unit> block) {
        Intrinsics.checkParameterIsNotNull(block, (String)"block");
        try {
            block.invoke();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static final void run() {
        void message;
        Ref.ObjectRef objectRef = new Ref.ObjectRef();
        objectRef.element = "xxx";
        // 这里每次运行都会 new 一个对象
        InlineKt.runCatch((Function0<Unit>)((Function0)new Function0<Unit>((Ref.ObjectRef)message){
            final /* synthetic */ Ref.ObjectRef $message;
            public final void invoke() {
                String string = (String)this.$message.element;
                boolean bl = false;
                System.out.println((Object)string);
            }
            {
                this.$message = objectRef;
                super(0);
            }
        }));
    }
}
复制代码


如果 Lambda 捕捉了外部变量,那么每次运行都会 new 一个持有外部变量值的 Function0<Unit>  对象。这比未发生捕捉变量的情况更加糟糕。


总而言之,Kotlin 的 Lambda 为了完全兼容到 Java6,不仅增大了编译代码的体积,也带来了额外的运行时开销。为了解决这个问题,Kotlin 提供了 inline 关键字。


Kotlin 内联函数的作用是消除 lambda 带来的额外开销

runCatch()  加持 inline :

inline fun runCatch(block: () -> Unit){
    try {
        block()
    }catch (e:Exception){
        e.printStackTrace()
    }
}
fun run(){
    var message = "xxx"
    runCatch { println(message) }
}
复制代码


反编译查看 java 代码:

public static final void run() {
      Object message = "xxx";
      boolean var1 = false;
      try {
         int var2 = false;
         System.out.println(message);
      } catch (Exception var5) {
         var5.printStackTrace();
      }
}
复制代码


runCatch() 的代码被直接内联到 run() 方法中,没有额外生成其他类,消除了 Lambda 带来的额外开销。


Java 是如何优化 Lambda 的?


既然 Kotlin 的 Lambda 存在性能问题,那旁边的 Java 大兄弟肯定也逃脱不了。

从 Java8 开始,Java 借助 invokedynamic 来完成的 Lambda 的优化。


invokedynamic  用于支持动态语言调用。在首次调用时,它会生成一个调用点,并绑定该调用点对应的方法句柄。后续调用时,直接运行该调用点对应的方法句柄即可。说直白一点,第一次调用 invokeddynamic 时,会找到此处应该运行的方法并绑定, 后续运行时就直接告诉你这里应该执行哪个方法。


关于 invokedynamic 的详细介绍,可以阅读极客时间专栏 《深入拆解Java虚拟机》的第 8,9 两讲。

JVM 是怎么实现 invokedynamic 的?(上)

JVM 是怎么实现 invokedynamic 的?(下)


不想内联怎么办?



一个高阶函数一旦被标记为内联,它的方法体和所有 Lambda 参数都会被内联。

inline fun test(block1: () -> Unit, block2: () -> Unit) {
    block1()
    println("xxx")
    block2()
}
复制代码


test() 函数被标记为了 inline  ,所以它的函数体以及两个 Lambda 参数都会被内联。但是由于我要传入的 block1  代码块巨长(或者其他原因),我并不想将其内联,这时候就要使用 noinline  。


inline fun test(noinline block1: () -> Unit, block2: () -> Unit) {
    block1()
    println("xxx")
    block2()
}
复制代码


这样, block1 就不会被内联了。篇幅原因,这里就不展示 Java 代码了,相信你也能很容易理解 noinline


如何从 Lambda 返回?



首先,普通的 lambda 是不允许直接使用 return 的 。

fun runCatch(block: () -> Unit) {
    try {
        print("before lambda")
        block()
        print("after lambda")
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
fun run() {
    // 普通 lambda 不允许 return
    runCatch { return }
}
复制代码


上面的代码没有办法通过编译,IDE 会提示你 return is not allowed here 。 而  inline  可以让我们突破这个限制。

// 标记为 inline
inline fun runCatch(block: () -> Unit) {
    try {
        print("before lambda")
        block()
        print("after lambda")
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
fun run() {
    runCatch { return }
}
复制代码


上面的代码是可以正常编译运行的。和之前的例子唯一的区别就是多了 inline

既然允许 return ,那么这里究竟是从 Lambda 中返回,继续运行后面的代码?还是直接结束外层函数的运行呢?看一下 run() 方法的执行结果。

before lambda
复制代码


从运行结果来看,是直接结束外层函数的运行。其实不难理解,这个 return 是直接内联到 run() 方法内部的,相当于在 run() 方法中直接调用 return。从反编译的 java 代码看,一目了然。

public static final void run() {
      boolean var0 = false;
      try {
         String var1 = "before lambda";
         System.out.print(var1);
         int var2 = false;
      } catch (Exception var3) {
         var3.printStackTrace();
      }
   }
复制代码


编译器直接把 return 之后的代码优化掉了。这样的场景叫做 non-local return (非局部返回)


但是有些时候我并不想直接退出外层函数,而是仅仅退出 Lambda 的运行,就可以这样写。

inline fun runCatch(block: () -> Unit) {
    try {
        print("before lambda")
        block()
        print("after lambda")
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
fun run() {
    // 从 lambda 中返回
    runCatch { return@runCatch }
}
复制代码


return@label,这样就会继续执行 Lambda 之后的代码了。这样的场景叫做 局部返回  。


还有一种场景,我是 API 的设计者,我不想 API 使用者进行非局部返回 ,改变我的代码流程。同时我又想使用 inline ,这样其实是冲突的。前面介绍过,内联会让 Lambda 允许非局部返回。


crossinline  就是为了解决这一冲突而生。它可以在保持内联的情况下,禁止 lambda 从外层函数直接返回。

inline fun runCatch(crossinline block: () -> Unit) {
    try {
        print("before lambda")
        block()
        print("after lambda")
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
fun run() {
    runCatch { return }
}
复制代码


添加  crossinline 之后,上面的代码将无法编译。但下面的代码仍然是可以编译运行的。

inline fun runCatch(crossinline block: () -> Unit) {
    try {
        print("before lambda")
        block()
        print("after lambda")
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
fun run() {
    runCatch { return@runCatch }
}
复制代码

crossinline 可以阻止非局部返回,但并不能阻止局部返回,其实也没有必要。


最后


关于内联函数,一口气说了这么多,总结一下。

在 Kotlin 中,内联函数是用来弥补高阶函数中 Lambda 带来的额外运行开销的。对于普通函数,没有必要使用内联,因为 JVM 已经提供了一定的内联支持。


对指定的 Lambda 参数使用 noinline ,可以避免该 Lambda 被内联。

普通的 Lambda 不支持非局部返回,内联之后允许非局部返回。既要内联,又要禁止非局部返回,请使用 crossinline


除了内联函数之外,Kotlin 1.3 开始支持 inline class ,但这是一个实验性 API,需要手动开启编译器支持。不知道大家对内联类有什么独特的看法,欢迎在评论区交流。

本文使用 mdnice 排版



相关文章
|
6月前
|
移动开发 Java Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
【4月更文挑战第3天】在移动开发领域,性能优化一直是开发者关注的焦点。随着Kotlin的兴起,其在Android开发中的地位逐渐上升,但关于其与Java在性能方面的对比,尚无明确共识。本文通过深入分析并结合实际测试数据,探讨了Kotlin与Java在Android平台上的性能表现,揭示了在不同场景下两者的差异及其对应用性能的潜在影响,为开发者在选择编程语言时提供参考依据。
|
6月前
|
Java 编译器 Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
【2月更文挑战第30天】 随着Kotlin成为开发Android应用的首选语言,开发者社区对于其性能表现持续关注。本文通过深入分析与基准测试,探讨Kotlin与Java在Android平台上的性能差异,揭示两种语言在编译效率、运行时性能和内存消耗方面的具体表现,并提供优化建议。我们的目标是为Android开发者提供科学依据,帮助他们在项目实践中做出明智的编程语言选择。
|
6月前
|
安全 Java Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
【2月更文挑战第24天】在移动开发领域,性能优化一直是开发者关注的焦点。随着Kotlin在Android开发中的普及,了解其与Java在性能方面的差异变得尤为重要。本文通过深入分析和对比两种语言的运行效率、启动时间、内存消耗等关键指标,揭示了Kotlin在实际项目中可能带来的性能影响,并提供了针对性的优化建议。
100 0
|
6月前
|
安全 Java Android开发
构建高效安卓应用:探究Kotlin与Java的性能对比
【2月更文挑战第22天】 在移动开发的世界中,性能优化一直是开发者们追求的关键目标。随着Kotlin在安卓开发中的普及,许多团队面临是否采用Kotlin替代Java的决策。本文将深入探讨Kotlin和Java在安卓平台上的性能差异,通过实证分析和基准测试,揭示两种语言在编译效率、运行时性能以及内存占用方面的表现。我们还将讨论Kotlin的一些高级特性如何为性能优化提供新的可能性。
425 0
|
6月前
|
Java 编译器 Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
在开发高性能的Android应用时,选择合适的编程语言至关重要。近年来,Kotlin因其简洁性和功能性受到开发者的青睐,但其性能是否与传统的Java相比有所不足?本文通过对比分析Kotlin与Java在Android平台上的运行效率,揭示二者在编译速度、运行时性能及资源消耗方面的具体差异,并探讨在实际项目中如何做出最佳选择。
52 4
|
6月前
|
Java 编译器 Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
【2月更文挑战第24天】 在移动开发领域,性能优化一直是开发者关注的重点。随着Kotlin的兴起,许多Android开发者开始从传统的Java转向Kotlin进行应用开发。本文将深入探讨Kotlin与Java在Android平台上的性能表现,通过对比分析两者在编译效率、运行时性能和内存消耗等方面的差异。我们将基于实际案例研究,为开发者提供选择合适开发语言的数据支持,并分享一些提升应用性能的最佳实践。
|
6月前
|
Java 编译器 Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
【2月更文挑战第22天】随着Kotlin在Android开发中的普及,开发者们对其性能表现持续关注。本文通过深入分析Kotlin与Java在Android平台上的执行效率,揭示了二者在编译优化、运行时性能以及内存占用方面的差异。通过实际案例测试,为开发者提供选择合适编程语言的参考依据。
|
5月前
|
存储 Kotlin
Kotlin中的内联函数:提升性能与解决Lambda表达式参数问题
Kotlin中的内联函数:提升性能与解决Lambda表达式参数问题
|
6月前
|
移动开发 Java Android开发
构建高效Android应用:探究Kotlin与Java的性能对比
【5月更文挑战第4天】在移动开发的世界中,性能一直是衡量应用质量的重要指标。随着Kotlin的兴起,许多Android开发者开始考虑是否应该从传统的Java迁移到Kotlin。本文通过深入分析两者在Android平台上的性能差异,帮助开发者理解Kotlin在实际项目中的表现,并提供选择编程语言时的参考依据。
97 5
|
6月前
|
Java 编译器 Android开发
构建高效Android应用:探究Kotlin与Java的性能差异
【5月更文挑战第1天】 在移动开发的世界中,性能优化始终是开发者关注的焦点。随着Kotlin的兴起,许多团队和开发者面临着一个选择:是坚持传统的Java语言,还是转向现代化、更加简洁的Kotlin?本文通过深入分析和对比Kotlin与Java在Android应用开发中的性能表现,揭示两者在编译效率、运行速度和内存消耗等方面的差异。我们将探讨如何根据项目需求和团队熟悉度,选择最适合的语言,以确保应用的高性能和流畅体验。