Kotlin内联函数:消除lambda带来的运行时开销
1.内联函数如何运作
当一个函数被声明为inline时,它的函数体是内联的——换句话说,函数体会被直接替换到函数调用的地方,而不是被正常调用。来看一个例子以便理解生成的最终代码。
定义一个内联函数
inline fun <T> synchronized(lock:Lock,action:()->T):T{
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}
val l=Lock()
synchronized(l){
...
}
调用这个函数的语法跟Java中使用synchronized语句完全一致。区别在于Java的synchronized语句可以用于任何对象,而这个函数则要求传入一个Lock实例。这里展示的定义只是一个实例,kotlin标准库种定义了一个可以接收任何对象作为参数的synchronized函数版本。
但在同步操作时使用显示的对象锁能提高代码的可读性和可维护性。
因为已经将synchronized函数声明inline,所以每次调用它所生成的代码跟Java的synchronized语句是一样的。
原函数
fun foo(l:Lock){
LogS("before sync")
synchronized(l){
LogS("action")
}
LogS("after sync")
}
引用内联函数
fun foo(l:Lock){
LogS("before sync")
l.lock()
try {
LogS("action")
}
finally {
l.unlock()
}
LogS("after sync")
}
注意lambda表达式和synchronized函数的实现都被内联了。由lambda生成字节码成为了函数调用者定义的一部分,而不是被包含在一个实现了函数接口的内部类中。
注意:在调用内联函数的时候也可以传递函数类型的变量作为参数:
class LockOwner(val lock: Lock){
fun runUnderLock(body:()->Unit){
synchronized(lock,body)
}
}
在这种情况下,lambda的代码在内联函数被调用点是不可用的,因此并不会被内联。只有synchronized的函数体被内联了,lambda才会被正常调用。runUnderLock函数被编译成类似于以下函数的字节码:
class LockOwner(val lock:Lock){
fun _runUnderLock_(body:()->Unit){
lock.lock()
try {
body
}
finally {
lock.unlock()
}
}
}
如果在两个不同位置使用同一个内联函数,但是用的是不同的lambda,那么内联函数会在每一个被调用的位置分别内联。内联函数代码会被拷贝到使用它的两个不同位置,并把不同的lambda替换到其中。
2.内联函数的限制
鉴于内联函数的运作方式,不是所有使用;ambda的函数都可以被内联。当函数被内联的时候,作为参数的lambda表达式的函数体会被直接替换到最终生成的代码中。这将限制函数体中对应的参数的使用。如果,lambda参数被调用,这样的代码被容易的内联。但如果参数在某个地方被保存起来,以便后面可以继续使用,lambda表达式的代码将不能被内联,因为必须要有一个包含这些代码的对象存在。
一般来说,参数如果被直接调用或者作为参数传递给另一个inline函数,它是可以被内联的。否则,编译器将会禁止参数被内联并给出错误信息“Illegal usage of inline-parameter”(非法参数内联函数)
例如,许多作用于序列的函数会返回一些类的实例,这些类代表对应的操作并接收lambda作为构造方法的参数。以下是Sequence,map函数的定义:
fun <T,R>Sequence<T>.map(transform:(T)->R):Sequence<R>{
return TransformingSequence(this,transform)
}
map函数没有直接调用作为transform参数传递进来的函数。而是将这个函数传递给一个类的构造方法,构造方法将它保存在一个属性中。为了支持这一点,作为transform参数传递的lambda需要被编译成标准的非内联表示方法,即一个实现了函数接口的匿名类。
如果一个函数期望两个或更多的lambda参数,可以选择内联其中一些参数。这是有道理的,因为一个lambda可能会包含很多代码或者不允许内联的方式使用。接收这样的非内联lambda的参数,可以使用inline修饰符来标记它:
inline fun foo(inline:()->Unit,noinline notInlined:()->Unit){}
注意,编译器完全支持内联模块的函数或者第三方库定义的函数。也可以在Java中调用绝大部分内联函数,但这些调用并不会被内联,而是被编译成普通的函数调用。
3.内联操作集合
我们来仔细看一看Kotlin标准库种操作集合的函数性能。大部分标准库种的集合函数都带有lambda参数。
使用lambda过滤一个集合
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
LogS(people.filter { it.age < 30 })//[Person(name=Alice, age=29)]
手动过滤一个集合
val result = mutableListOf<Person>()
for (person in people) {
if (person.age < 30) result.add(person)
}
LogS(result)
在kotlin中,filter函数被胜民为内联函数。这意味着filter函数,以及传递给它的lambda的字节码会被一起内联到filter被调用的地方。最终,第一种实现产生的字节码和第二种实现所产生的字节码大致是一样的。你可以很安全地使用符合语言习惯的集合操作,kotlin对内联函数的支持让你不必担心性能的问题。