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
也对内联优化做了支持。