前言

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

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

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

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

正文

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

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

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

inlinefunrepeat(times: Int, action: (Int) -> Unit) {for (index in0 until times) {action(index) }}funnoinlineRepeat(times: Int, action: (Int) -> Unit) {for (index in0 until times) { action(index) }}

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

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

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

inline有什么用?

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

publicinlinefunprint(message: Int) {

System.out.print(message)}funmain(args: Array<String>) { print(2) print(2)}

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

publicstaticfinalvoidmain(@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 in0 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 filepublicclassTestInlineKt$lambdaimplementsFunction0{public Object invoke(){// code}}// UsageFunction0 lambda = new TestInlineKt$lambda()

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

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

反编译之后:

publicstaticfinalvoidmain(@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)}

反编译如下:

publicstaticfinalvoidmain(@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,徒增消耗。

尾声

到此这篇文章就结束了。

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

相关文章