阅读 65

Kotlin进阶知识(七)——内联函数:消除lambda带来的运行时开销

引用:使用**inline**修饰符标记一个函数,在函数被使用的时候编译器并不会生成函数调用的代码

一、内联函数如何运行

当一个函数被声明为inline时,它的函数体是内联的——换句话说,函数体会被直接替换到函数被调用的地方,而不是被正常调用。

inline fun <T> synchronized(lock: Lock, action: () -> T): T {
    lock.lock()
    try {
        return action()
    }
    finally {
        lock.unlock()
    }
}

fun foo(l: Lock) {
    println("Before sync")

    synchronized(l) {
        println("Action")
    }

    println("After sync")
}
复制代码

上述代码在转换成Java代码时,inline修饰的方法会自动添加到对应的方法中

   public static final void foo(@NotNull Lock l) {
      String var1 = "Before sync";
      System.out.println(var1);
      l.lock();

      try {
         String var3 = "Action";
         System.out.println(var3);
         Unit var8 = Unit.INSTANCE;
      } finally {
         l.unlock();
      }

      var1 = "After sync";
      System.out.println(var1);
   }
复制代码

注意:lambda表达式和synchronized函数的实现都被内联了。由lambda生成的字节码称为了函数调用者定义的一部分,而不是被包含在一个实现了函数接口的匿名类中。

在调用内联函数的时候也可以传递函数类型的变量作为参数:

class LockOwner(val lock: Lock) {
    fun runUnderLock(body: () -> Unit) {
        // 传递一个函数类型的变量作为参数,而不是一个lambda
        synchronized(lock, body)
    }
}
复制代码

这种情况下,lambda的代码在内联函数被调用点是不可用的,因此并不会被内联。 只有synchronized的函数体别内联了,lambda才会被正常调用。

如果在两个不同的位置使用同一个内联函数,但是用的是不同的lambda,那么内联函数会在每一个被调用的位置被分别内联。

内联函数的代码会被拷贝到使用它的两个不同为孩子,并把不同的lambda替换到其中。

二、内联集合操作

大部分标准库中的集合函数都带有lambda参数。相比于使用标准库函数,直接实现这些操作不是更高效吗?

  • 使用lambda过滤一个集合
data class PersonNew(val name: String = "", val age: Int = 0)

val people = listOf(PersonNew("Alice", 29), PersonNew("Bob", 31))

fun filterSetByLambdaTest() {
    println(people.filter { it.age < 30 })
}

// 输出结果
[Person(name=Alice, age=29)]
复制代码
  • 手动过滤一个集合
fun filterSetByManualTest() {
    val result = mutableListOf<PersonNew>()
    for (person in people) {
        if (person.age < 30) result.add(person)
    }
    println(result)
}
复制代码

在Kotlin中,filter函数被声明为内联函数。这意味着filter函数,以及传递给它的lambda的字节码会被一起关联到filter被调用的地方。最终,第一种实现所产生的字节码和第二种实现所产生的字节码大致一样的。

因而Kotlin对内联函数的支持让你不必担心性能问题

注意: 1、有大量元素需要处理,中间集合的运行开销将成为不可忽视的问题,因而使用asSequence调用,用序列来替代集合。 2、只在处理大量数据的集合时序列有用,小集合可以用普通的集合操作处理即可。

三、决定何时将函数声明成内联

使用inline关键字只能提高带有lambda参数的函数的性能,其他情况需要额外的度量和研究。

对于普通的函数调用JVM已经提高了强大的内联支持。它会分析代码的执行,并在任何通过内联能够带来好处的时候将函数调用内联。

lambda参数的函数内联能带来的好处

  • 通过内联避免的运行时开销更下明显了。不仅节约了函数调用的开销,而且节约了为lambda创建匿名类,以及创建lambda实例对象的开销。
  • JVM目前并没有总是将函数调用内联。
  • 内联可以使用一些不可能被普通lambda使用的特性,比如非局部返回。