玖叶教程网

前端编程开发入门

如何在Kotlin中高效的使用inline(内联)


前言

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

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

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

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

正文

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

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

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

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)
    }
}

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

var a = 0
repeat(100_000_000) {
    a += 1
}
var b = 0
noinlineRepeat(100_000_000) {
    b += 1
}

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

inline有什么用?

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

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

fun main(args: Array<String>) {
    print(2)
    print(2)
}

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

   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);
   }

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

for (index in 0 until 1000) {
    println("A")
}

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

Lambda的原理

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

  • 匿名类
  • “附加”类

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

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

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

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

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

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

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

fun main(args: Array<String>) {
    var a = 0
    repeat(100_000_000) {
        a += 1
    }
    var b = 0
    noinlineRepeat(100_000_000) {
        b += 1
    }
}

反编译之后:

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

   for(int var4 = times$iv; var3 < var4; ++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;
      }
   }));
}

inline的成本

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

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

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

反编译如下:

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);
}

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

最佳实践

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

尾声

到此这篇文章就结束了。

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

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言