Kotlin最佳实践:在高阶函数中使用inline


Kotlin最佳实践:在高阶函数中使用inline

前言

最近,无意中看到一篇文章,是聊inline在高阶函数中的性能提升,说实话之前没有认真关注过这个特性,所以借此机会好好学习了一番。

高阶函数:入参中含有lambda的函数(方法)。

原文是一位外国小哥写的,这里把它翻译了一下重写梳理了一遍发出来。也算是技术无国界吧,哈哈~

官方文档对inline的使用主要提供了俩种方式:内联类、内联函数

正文

操作符是我们日常Kotlin开发的利器,如果我们点进去看看源码,我们会发现这些操作符大多都会使用inline。

<code>inline fun  Iterable.filter(predicate: (T)->Boolean): List{
    val destination = ArrayList()
    for (element in this) 
        if (predicate(element))
            destination.add(element)
    return destination
}
/<code>

既然官方标准库中如果使用,我们则需要验证一下inline是不是真能有更好的性能:

<code>inline fun repeat(times: Int, action: (Int) -> Unit) {
    for (index in 0 until times) {
        action(index)
    }
}

fun noinlineRepeat(times: Int, action: (Int) -> Unit) {
    for (index in 0 until times) {
        action(index)
    }
}/<code>

俩个函数,除了inline没什么其他区别。接下来咱们执行个100000000次,看看方法耗时:

<code>var a = 0
repeat(100_000_000) {
    a += 1
}
var b = 0
noinlineRepeat(100_000_000) {
    b += 1
}/<code>

跑起来我们会发现:inlineRepeat()平均完成了0.335ns,而noinlineRepeat()平均需要153 980484.884ns。是46.6万倍!看起来inline的确很重要,那么这种性能改进是没有成本的吗?我们什么时候应该使用inline?接下来咱们就来聊一聊这个问题,不过咱们先从一个基本的问题开始:inline有什么作用?

inline有什么用?

简单来说被inline修饰过的函数,会在调用的时候把函数体替换过来。说起来可能很从抽象,直接看代码:

<code>public inline fun print(message: Int) {
    System.out.print(message)
}

fun main(args: Array<string>) {
    print(2)
    print(2)
}/<string>/<code>

反编译class之后,我们会看到是这个样子的:

<code>   public static final void main(@NotNull String[] args) {
      int message$iv = 2;
      int $i$f$print = false;
      System.out.print(message$iv);
      message$iv = 2;
      $i$f$print = false;
      System.out.print(message$iv);
   }/<code>

接下来咱们看看高阶函数中的优化repeat(100) { println("A") },反编译之后:

<code>for (index in 0 until 1000) {
    println("A")
}/<code>

看到这我猜大家应该可以理解inline的作用了吧。不过,话又说回来。“仅仅”做了这点改动,会什么会有如此大的性能提升?解答这个问题,不得不聊一聊JVM是如何实现Lambda的。

Lambda的原理

一般来说,会有俩种方案:

  • 匿名类
  • “附加”类

咱们直接通过一个demo来看这俩种实现:

<code>val lambda: ()->Unit = {
    // body
}/<code>

对于匿名类的实现来说,反编译是这样的:

<code>Function0 lambda = new Function0() {
   public Object invoke() {
      // body
   }
};/<code>

对于“附加”类的实现来说,反编译是这样的:

<code>// Additional class in separate file
public class TestInlineKt$lambda implements Function0 {
   public Object invoke() {
      // code
   }
}
// Usage
Function0 lambda = new TestInlineKt$lambda()/<code>

有了上边的代码,咱们也就明白高阶函数的开销为什么这么大:毕竟每一个Lambda都会额外创建一个类。接下来咱们通过一个demo进一步感受这些额外的开销:

<code>fun main(args: Array<string>) {
    var a = 0
    repeat(100_000_000) {

        a += 1
    }
    var b = 0
    noinlineRepeat(100_000_000) {
        b += 1
    }
}/<string>/<code>

反编译之后:

<code>public static final void main(@NotNull String[] args) {
   int a = 0;
   int times$iv = 100000000;
   int var3 = 0;

   for(int var4 = times$iv; var3       ++a;
   }

   final IntRef b = new IntRef();
   b.element = 0;
   noinlineRepeat(100000000, (Function1)(new Function1() {
      public Object invoke(Object var1) {
         ++b.element;
         return Unit.INSTANCE;
      }
   }));
}/<code>

inline的成本

inline并不是没有任何成本的。其实咱们最上边看public inline fun print(message: Int) { System.out.print(message) }的时候,看反编译的内容也能看出它得到的成本。

接下来咱们就基于这个print()函数,来对比一下:

<code>fun main(args: Array<string>) {
    print(2)
    print(2)
    System.out.print(2)
}/<string>/<code>

反编译如下:

<code>public static final void main(@NotNull String[] args) { 

    int message$iv = 2;
    int $i$f$print = false;
    System.out.print(message$iv);
    message$iv = 2;
    $i$f$print = false;
    System.out.print(message$iv);
    System.out.print(2);
}/<code>

可以看出inline额外生成了一些代码,这也就是它额外的开销。因此咱们在使用inline的时候还是需要有一定的规则的,以免适得其反。

最佳实践

当我们没有高阶函数、没有使用reified关键词时不应该随意使用inline,徒增消耗。

尾声

到此这篇文章就结束了。

但是看了外国小哥这篇文章的时候,的确发现自己有很多内容是有遗漏的。所以接下来如果有机会的话,会继续写或者翻译一些这类“最佳实践”的文章。


分享到:


相關文章: