Kotlin中的inline
函数的频繁调用开销
在编程中我们一般习惯将特定的功能封装成一个函数,使我们的代码结构清晰,实现模块化。同时使用函数能够避免将相同代码重写多次的麻烦,从而减少可代码的体积。但是函数的每一次调用都会带来性能开销(创建栈帧 压栈、出栈,创建局部变量表,操作数栈,保存函数出口信息),函数的频繁调用势必会带来性能的下降
这里我们假设实现一个简单的图片滤镜功能,滤镜filter001要实现的效果就是首先提高亮度,然后把图片变为黑白
1 | // Kotlin Code |
通常我们我会提亮操作和黑白处理分别封装成单独的方法,使我们的代码变得清晰可读,上面的代码很好理解,就是循环取出图像上的每一个像素做处理,如果处理的是一张4000万像素的图片,我们则需要对4000万个像素做亮度和黑白处理,伴随而来就是8000万次的函数调用开销。
这就是我们对特定功能封装之后所付出的代价,如果不对提亮操作和黑白处理做单独的封装,我们代码可读性/可维护性就会下降。我们的代码除了给编译器看,同时也要给人看。因此保持代码的清晰结构和可读性尤为重要。一边是性能,一边是代码的清晰度,如何选择 ?
inline 函数
在Kotlin中为我们提供inline关键字,inline的意思是告诉编译器在编译时展开。inline关键字可用于修饰一个函数,称之为内联函数,告诉编译器在编译时对该函数的调用处对函数展开,以消除函数调用带来的开销。
如:
1 | //Kotlin Code |
上面的代码我为brighten和gray函数加上inline修饰,编译器在编译时会在调用处对函数展开,类似下面的效果:
1 | // Kotlin Code |
对于inline函数可以简单的理解为,编译时把函数体复制到了函数调用处,函数的调用不存在了。这样以来我们就通过inline即保证了代码的清晰结构,同时也消除的函数频繁调用带来的性能开销。另外内联之后,编译器可以获得更多的上下文信息,进一步加大了编译器对代码的优化空间。借助于inline函数的编译时展开,Kotlin还为我们提供reified关键字来实现具体化的类型参数,前段时间我写过一篇关于泛型擦除文章有提到,具体可以参看《浅谈JVM泛型》。
inline 与 lambda
上面我们讲到了通过内联函数消除函数频繁调用的开销,然而在Kotlin中最能体现inline价值的则是高阶函数中的 lambda。
1 | //Kotlin Code |
高阶函数higherOrder接收两个Int类型的值和一个lambda,Kotlin目前编译的字节码最低要支持jvm6,但是jvm从7.0开始才引入invokedynamic指令,所以lambda的调用不能通过invokedynamic指令完成。
lambda 脱糖(desugar)
由于Kotlin要兼容jvm6,在Kotlin中目前lambda就是一个语法糖,对lambda的支持依赖Kotlin 编译器的对lambda的语法脱糖,上面的代码语法脱糖后:
1 | //Kotlin Code |
在Android开发中也有不少支持Java8的lambda的解决方案(RetroLambda,Jack,D8),与Kotlin的方案类似,都是语法脱糖
λ: (Int, Int) -> Int是两个输入一个输出,所以脱糖后由Function2实现。label 1 和label 2处共享一个实例对象,label 3处的lambda实现要依赖外部的m变量,所以每一次循环都要创建一个匿名内部类的实例。这里大家已经可以看出我们所编写简单的高价函数的调用的开销,要不停的创建Function2的匿名内部类实例,并且之后还有invoke方法的调用开销。要解决这个问题,当然还是inline关键字
1 | //Kotlin Code |
被
inline关键字修饰的高阶函数,会传递给它的lambda表达式参数。
为igherOrder函数加上inline之后,编译内联之后的代码类似:
1 | //Kotlin Code |
没错higherOrder被inline修饰后,不但自身函数体在编译时展开了,它的参数λ: (Int, Int) -> Int也展开了。所以我们可以用inline关键字来解决lambda的性能问题。
noinline
上面我们提到被inline修饰的高阶函数不但自身的函数体会展开,他的lambda参数也会展开,由于lambda被展开了,所以它将不能被存储和传递。如果不想lambda参数被展开可以通过noinline对lambda修饰。
1 | //Kotlin Code |
1 | //Kotlin Code |
内联函数中的lambda参数被noline修饰后不会伴随内联函数一块展开。上面的代码,λ1会展开,λ2不会展开。
crossInline
non-local-return
在Kotlin中裸return只允许退出函数,不允许退出lambda,lambda的退出要通过return@label的方式,普通的lambda(非inline)中也禁止使用裸return。
1 | //Kotlin Code |
在lambda被内联之后,是允许在lambda中使用裸return的,即非局部返回(non-local-return),因为lambda被展开了,return也伴随着lambda一块被展开了。也正式因为inline函数和lambda都被展开了,所以被内联lambda中的rerurn返回的目标是调用该内联函数的函数,例:
1 | //Kotlin Code |
上面的代码 highOrder中的return目标是main方法。所以下面的println("3")不会被执行,输出的结果是:1
crossInline
1 | // Kotlin Code |
上面我们讲到在伴随内联函数一块展开的lambda中允许使用裸return。按照语意上面代码中的return代表的是退出main方法,但是内联函数highOrder参数的λ并非在其函数体中直接执行,而是在另一个上下文中即Runnable的run方法,run方法并不会被内联,所以例中的return无法从穿过run方法直接把main方法给返回了(二者执行不在同一个栈桢中),所以上面的代码是编译失败的。
为此,kotlin 为我们提供了另一个关键字crossinline用于修饰内联函数参数中lambda,表示该lambda可以在其他上下文中执行。被crossinline修饰的lambda中禁止使用裸return。 例:
1 | // Kotlin Code |
inline properties(内联属性)
inline 关键字也可修饰属性,内联其get和set方法,这里不做过多介绍。
inline class
Kotlin从 1.3 开始引入了inline class,内联类必须有且有一个属性(包装一个类型)。
1 | // Kotlin code |
上面的编译后类似于:
1 | // Java Code |
编译后Name类的实例方法变成静态方法。调用处并没有创建Name类的实例,而是通过Name类的静态方法完成了功能实现。通过inline class我们可以避免对象的创建开销,例中对Name类的操作将在在栈上完成,不会为Name类在堆上创建实例,减轻 GC 压力,从而提高性能。
* 由于内联类使用的并不会创建具体实例,所以内联类中禁止使用init 代码块, inline class 目前还是一个实验性功能
Java 中的内联
在Java中并没有为我们提供inline关键字,Java的静态编译器javac也不会对方法做内联优化。但是JIT(Jvm 运行时的即时编译器)会对热点代码做内联优化。如我们文章开始提到的图片滤镜的例子,brighten和gray方法在循环体中被频繁调用。即使我们不为这两个方法加inline修饰,在运行时JIT也会对其做内联优化。类似于kotlin中inline class JIT编译器运行时,也会对对象做逃逸分析判断,如果对象未发生逃逸,则可以做栈上分配、标量替换。
一些字节码优化工具如ProGuard在优化代码时候会按照规则对一些方法调用做内联优化。Android编译工具里面的R8也对内联优化做了支持。