本文永久更新地址: https://xiaozhuanlan.com/topic/3458207169

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

object,史上最 “快” 单例 ?

typealias ,穿了马甲,我就不知道你了?

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

内联函数的语义很简单: 把函数体复制粘贴到函数调用处。运用起来也毫无困难,用 inli? z u 6ne要害字修饰函数即可。

可是问题的要害并不是怎样运用 inline,而是什么时分运用 inline? 已然 Kotlin 供给了内联,它必定是为了功能优化而存在的,那么,它又真的是包治百病的功能良药吗?

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

目录

  1. inline 的实质
  2. 主张我不要用 inline ?
  3. Java 支撑内联吗?
  4. 解救 Lambda
  5. Java 是怎样优化Y V C W 9 7 f Lambda 的?h D 6
  6. 不想内联怎样办?
  7. 怎样从 Lambda 回来?
  8. 最终

inline 的实质

^ & b o M面现已说过 inline 便是 把函数体复制粘贴到函数调用处,完全是编译器的小把戏。本着严谨科学的情绪,咱们仍是来反编译验证一下。

inline fun test() {
println("I'mO r 7 a inline function")= I 8 , a W
}

f# = C } ? s L @ ^un run() { test(3 C w I Y I s ! q) }

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

public static final void test() {
    String var1 = "I'm a inline function";
    Sp v l D K / _ pystem.out.println(var1);
}

public static final void run() {
    String var1 = "I'm a inline function";
    System.o% D } , 4 0 lut.println(var1);
}

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

那么,问题就来了。这样就能够提高运转功率吗?假如W z & ) 能够,为什么?

咱们先从 JVM 的办法履行机制说起。

JVM 进行办法调用和办法履@ I 3 * w G h J行依赖 栈帧,每一个办法从调用开端至履行完| _ . C结的进程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的进程。M U f . 6 s 1 ~ P

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

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

未内联的状况下,整个履行进程中会发生两个办法栈帧,每一个办法栈帧都包~ T M { 2含了 部分f R s & ] W变量表、操作数栈、动态连接、办法回来地址和一些额定的附加信息

运用内联的状况下,只需求一个办法栈帧,降低了办法调用的本钱。

乍一看,的确的提高了运转功率,究竟少用一个栈帧嘛。

可是?

主张不要运用 inline ?

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

Expected performance impact from inlining is insignificant. Inlining! l O y ^ works bG h 8 : C : uest for functions with parameters of functional types

大致意思是在这儿运用内联对功能的影响微乎其微,或者说没有什么含义。Kotlin 的内* 0 9 u |联最M l 8 y好用在函数参数类型中。

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

Java 支撑内联吗?

你能够说不支撑,由于 Java 并没有供给相似 inline的显示声明内联函数的办法。

可是 JVM 是支撑的。Java 把内联优化交给虚拟q M Y [ k U机来进行,然后防止开发者的滥用。

典型的一种滥用,% o m v ^ z k _ J 内联超长办法,极大的增大字节码长度,反而得不偿失。你能够注意7 c / ) m y Kotlin 规范库中的内联函数,基本都是简略的函数。

关于一般的函数调用,JVM 现已供给了满足的内联支撑。因而,在 Kotlin 中,没有必要为一般函数运用内联,交给 JVM 就行了。

另外,Java 代码是由 javac 编译的U a ~ %,KoD 4 x 5 ! W # s Ktlin 代码是由 kotlinc 编译的,而 JVM 能够对字节码做统一的内联优化。所v H V g 5 .以,能够推断出,不管是 jav) h m 4ac ,仍是 kotlinc,在编译期是没有内联L u ! = ~ : | 优化的。

至于 JVM 具体的内联优化机制,我了解的并不多,这儿就不做过多介绍了。后续P V f | r假如我看到相t O 3 R { w关资料,会到这儿进行补充。7 E ? 6 ] 5 e W

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

JVM 现已供给了内联支撑,所以没有必要在 Kotlin 中内联一般函数。

那么问题又来了。@ + t 已然 JVM 现已支撑内联优化,Kotlin 的内联存在的含义是什么 ?答案便是 Lambda

解救 Lambda

为什么要解救 Lambda,咱们首先得知道Kotlin 的 Lambda 关于 JVM 而言究竟是什么。

Kotlin 规范库中有一个叫 runCatching 的函数,我在这儿完结一个简化版t X 0 F J % J ) hrunCatch,参数是一个函e B b数类型。

fun runCatch(block: ()P v 5 4 m s  F . -> Unit){
    try {
        block()
    }catch (e:Exception u 0){
        e.printStackTrace()
    }
}

fun run(){
    runCQ C P . U 3 B y Uatch { println("xxx") }
}

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

public final class InlineKt {
    public static final voidJ 2 v s v runCatch(@NotNull Fun1 E i x _ f i Tction0<Unit> block) {
        Intrinsics.checkw = r C & VParameterIsNotNull(block, (String)"block");
        try {
            block.invoke();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static final void run() {
        InlineKt.runCatch((Function0<Unit&u o Y v K | h ; jgt;)((Function0)run.1.INSTANCE));
    }
}

static final class InlineK N q 0 H J s y ~t.run.19 ~ ; & ; D g + extends Lambda implements Function0<Unit>D A 3 % M 9 : {
    public static final InlineKt.run.1 INSTANCE = new /* invalid duplicate definition of idec t 2 *ntical inner class */;

    pubb N Dlic final void invoke() {
        String string = "xxx";
        bo7 h .olean bl = false;
        SysteP e Y i 6 Am.out.println((Object)string);
    }

    InlineKt.run.1() {
    }
}

Kotlin 自诞生之初,就以 兼容 Java为首要方针。因而,Kotlin 关于 Lambda 表H @ ) & $ f f u ,达式的处理是编译生成匿名类。

经过编译器编译之后, runCatch()办法中的 Lambda 参数被替换为 Function0<Unit>类型,在 run()办法中实践调用 runCatch() 时传入的参数是完结了 Function0<Unit> 接口H D L U p BInlineKt.run.1 ,并重写 了 invoke()办法。

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

fun runCatch(block: () -> Unit){
    try {
        block()
    }catch (ek h V S | = ? t t:Exception){
        e.prin7 j  l i ( v  $tStackTrace()
    }
}

fun run(){
    var messaO f  V f &ge = "xxx"
    runCatch { println(mes@ ; 5 4  y + |sage) }
}

在 Lambda 内部捕捉了外部变量 messx e a j m a $ ^ }age ,其对应的 javaG # ) 代码如下所示:

public^ i V # z M f, M ` I Vinal class InlineKt {
    public static final void runCatch(@NotNull Function0<Unit> block) {
        Intrinsics.checkParameterIsNotNull(block, (String)"block");
        try {
            block.invoke();
        }
        catch (Exceptionw + S e) {
            e.printStackTrace()V z { F;
        }
    }

    public static final void run() {
        void message;
        Ref.ObjectRef objectRef = new Ref.ObjectRef();
        objectRef.elementJ t y 8 n _ 3 = "xxx";
        // 这儿每次运转都会 new 一g #  ~个对象
        InlineKt.runCatch((Function0<Unit>)((Function0) m . $new Function0<Unit>((Ref.ObjectRef)message){
            final /* synthetic */ Ref.ObjectRef $message;

            public finaB r J g F ] A h Bl void invoke() {
                String string = (String)this.$message.element;
                boop E z 6 w Dlean bl = fa- B klse;
                System.out.println((Objey N M e 4 l q act)string);
            }
            {
                this.$message = o_ Y : z j RbjectRef;
                super(0);
            }
        }));
    }
}

假如 Lambda 捕捉了外部变量,那么每次运转都会 new 一个持有外部变量值的 Function0<Unit> 对象。这比未发生捕捉变量^ 6 x F W的状况更加糟糕。

总而言之,Kotlin 的 Ll 9 F FamW h 7 _ 6bda 为了完全兼容到 Java6,不只增大了编: W q译代码的体积,也带来了额定的运转时开支。为了处理这个问题,Kotlin 供给了 inline 要害字。

KotliJ h n E | o Zn 内联函数的作用是消除 lambda 带来的额M | p x ] = N L定开支r , | 6 s O

runCatch() 加持 inC O + o d `line :

inline fun runCatch(block: () -> Unit){
    try {
        block()
    }catch (e:Exception){
        e.6 - tprintStackTrace()
    }
}

fun run(){
    var message = "xxx"
    runCatch { println(message) }
}

反编译检查 java 代码:

public sto T ,atic final void run() {
      Object message =Y y o 9 % B "xxx";
      boolean var1 =R a `  .  # J v fq f 3 l ] J + c alse;
      try {
         int var2 = false;
         SysteK q H 5m.out.println(me+ g n iss] W  x L 0 Z } Bage);
      } catch (Exception var5) {
         var5.printStackTrace();
      }
}

runCatch()的代码被直接内联到 run(J { 8 J C G q T r)办法中,没有额定生成其他类,消除了 Lambda 带来的额H & u m R w 9 6 –定开支。

Java 是怎样优化 Lambda 的?

已然 Kotlin 的 La7 ( j l & ) Z zmbda 存在功能问题,那周围的 Java 大兄弟必定也逃脱不了。

从 Java8 开端,Java 凭借 invokedH F G h s O y ! uynamic来完结的 Lambda 的优化。

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

关于 invokedynamic的具体介绍,能够阅览极客时间专栏 《深入拆解t @ = 2 tJava虚拟机》的第 8,9 两讲。

JVM 是怎样a f ^ ,完结& u l invokedynamic 的?(上)

JVM 是怎样完结 invokedynamic 的?(下)

不想内联怎样办?

一个高阶函数一旦被符号为内联,它的办法体和一切 Lambda 参数都会被内联。

inline f~ U : nun test(block1i E e 4 & r: () -> Unit, block2: () -> Unit) {
    blockN m [ 7 /1()
    println("xxxp s Z o R m F s $")
    block2()
}

test()函数被符号为了 inline ,所以它的函数体以及两个 Lambda 参数都会被内联。可是m # I G由于我要传入的 block1 代码块巨长(或者q g o其他原因),我并不想将其内联,这时分就要运用 noinline

inline fun test(noinline block1: () -> Unit, block2: () -l j {> Unit) {
    bl0 } 3 + 9 d p D _ock1()
    println("xxx")
    block2()
}

这样,9 k r c O N g e block1就不z H {会被内联了。篇幅原因,这儿就不展现 Java 代码了,相信9 o { 你也能很简单了解 noinline

怎样从 Lambda 回来?

首先,一般的 lambda 是不答应直接运用 return的 。

fun runCatch(block: () -> Unit) {
    try {
        print("bex - H [ Y  0 6fore lambda! : K P 1")
        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.pr k kri[ A x H  m R NntStackTrace()
    }
}

fun run() {
    runCatch { return }
}

N g %面的代码是能够正常编译运转的。和之前的例子仅有的差异便是多了 { D ] Q e _ inline

已然答应 return ,那么这儿究竟是从 Lambda 中回来,持续运转后边的代码?仍是直接结束外层函数的运转呢?看一下 run() 办法的履行成果。

before lambda

从运转成果来看,是直接结束外层函数的运转。其实不难了解,这个 return 是直接内联到 run()办法内部的,相当于在 run() 办法中直接调用 retud I | mrn。从反编译的 java 代码看,一望而知。

   public static final void run() {
      boolean var0 = false;

      try {
         String var1 = "before lambda";
         System.out.print(var1);
         int var2 = false;
      } catc` x W i . .h (Exception var3) {
         varp l G . { 9 {3.printStackTrace();
      }
   }

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

可是有些时分我并不想直接退出外层函数,而是只是退出 Lambda 的运转,就能够这样写。

inline fun runCatch(block: () -J 6 / v F W ? o Q> Unit) {
    try {
        print("bW h `efore lambda")
        block()
        print("after lambda")
    } ca_ G J k 8tch (c 7 g = = L 4 w ie: Exception) {
        e.printStan r c # x C F eckTrace()
    }
}

fun run() {
    // 从 lambda 中回来
    runCatch { return@runCatch }
}

return@label,这样就会持续履行 Lambda 之后的代码了。这样的场景叫做 部分回来

还有一种场景,我是 API 的设计者,我不想 API 运用者进行非部分回来 ,改动我的代码流程。同时我又想运用 inline ,这样其实b C 6 J . Q l是抵触的。前面介绍过,内联z 2 v (会让 Lambda 答应非部分回来。

crossinliO j ` O j ` m #ne 便是为了处理这一抵触而生。a M *它能够在保e o Z d 5 } R ,持内联的状况下,制止 lambda 从外层函数直接回来d + d U

inline fun runCatch(crossinline block: () -> Unitx { ^) {
    try {
        print("before lambda")
        block()
        print("after] r y ; i Y * [ P lambda")
    } catB k [ ? s p 8 Hch (e: Exception)a ^ # ( {
        e.printSt/ Y 5 m G $ $ =ackTrace()
    }
}

fun run() {
    runCatch { return }
}

增加 crossX # E 1 M vinline之后,上面的代码将无法编译。但下面的代码仍然是能够编译运转的。

inline fun runCatch(crossinline block: () -> Unit) {
    try {
        print("before le g z I v ~ G + 9ambda")
        block()
        print("after lambda")
    } catch (e: Excepti0 , f } , Won) {
        e.printSp A E 2 a D HtackTrace()
    }
}

fun run() {
    runCatch { return@runCatch@ s 4 }
}

crossinline能够阻挠非部分回来,但并不能阻挠部分回来,其实也没有必要。

最终

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

在 Kotlin 中,内联函数是用来补偿高阶函数中 Lambda 带来的额f S b p定运转开支的。关于一般函数,没有必要运用内联,由于 JVM 现已供给了一定的内联支撑。

对指定的 Lambda 参数运用 noinline ,能够防止该 Lambda 被内联。

一般的 Lambda 不支撑非部分回来,内联之后答应非部分回来。既要内联,又要制止非部分回来,请运用 cro9 v @ssij L _ B xnline

除了内联函数之外,Kotlin 1.3 开端支撑 inline class ,但这是一个实验性 API,需求手动敞开编译器支撑。不知道大家对内联类有什么共同的看法,欢迎在谈论区– $ L W Q +交流。

本文运用 mdnice 排版

这儿是秉心说,关注我,不迷路!

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