前言
最近,无意中看到一篇文章,是聊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,徒增消耗。
尾声
到此这篇文章就结束了。
但是看了外国小哥这篇文章的时候,的确发现自己有很多内容是有遗漏的。所以接下来如果有机会的话,会继续写或者翻译一些这类“最佳实践”的文章。