Kotlin Inline Function 1

Kotlin - Inline Functions 1

Inline Basics

Inline or Inlining,我们更经常听到的词是方法内联或者内联函数。在大多数情况下,他们指的都是同一个意思。即,在编译期间对函数进行优化,以便让代码在机器执行时获得更高的效率。
方法内联一般可能出现在两个阶段:l

  • 编译器:编译输出.class文件时
  • JVM:编译输入机器执行码时

在Java领域,方法内联是商用JVM登峰造极的虚拟机优化中重要的一环,下面节选自HotSpot文档中Method Inlining的一部分:

Inlining has important benefits. It dramatically reduces the dynamic frequency of method invocations, which saves the time needed to perform those method invocations. But even more importantly, inlining produces much larger blocks of code for the optimizer to work on. This creates a situation that significantly increases the effectiveness of traditional compiler optimizations, overcoming a major obstacle to increased Java programming language performance.

Inlining is synergistic with other code optimizations, because it makes them more effective. As the Java HotSpot compiler matures, the ability to operate on large, inlined blocks of code will open the door to a host of even more advanced optimizations in the future.

这里说JVM的方法内联带来两个优点:

  • 可以动态的减少方法调用来提高执行效率
  • 可以极大地提高其他优化手段的优化效果

但其实文档应该还想表达另外一件事情:方法内联的优化效果很难在JVM外部,更不用说单独观测到
HotSpot的方法内联是(晚期)JVM运行期进行方法内联的代表作之一,而Kotlin Inline Functions则是典型的在(早期)编译期进行方法内联。

Inline Function

首先,Kotlin的内联方法优化针对的是lambda表达式(如果不是针对lambda表达式使用IDE亦提示)。
我们知道,在Kotlin中,Function是”一等公民”,每一个函数都是一个对象,并且拥有其对应的闭包。也即是说:

1. 对于每一个lambda函数都需要分配一定的内存来创建和维护Function对象。
2. 在调用lambda函数的时候,需要先访问到闭包对象,再访问到真正的函数执行块。

在多数情况下这些东西对性能的影响可以忽略不计,但是在对性能有要求时,这便是我们可以优化提升的地方。而Kotlin Inline Function 可以轻松地消除这些东西对于性能的损耗,而实现的方式类似于JVM运行期的Method Inlining(方法内联),这也是为什么Kotlin将这一手段称之为”Inline Function

通过查看编译生成的.class文件字节码,可以清楚地看到Inline Function做的事情:

考虑这样一个例子,我们需要确保在 c() 中先执行了D类的实例方法 open() ,如果执行成功了再执行一块函数块(lambda),里面执行 foo1() foo2()。对于这个lambda函数块的执行使用 inline 来优化。
写成.kt (Kotlin Source File)是这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class C {
fun c() {
open(D1()) {
foo1()
foo2()
}
}
}

fun foo1(): Int {
return 1
}

fun foo2(): Int {
return 2
}

inline fun <T> open(d: D, body: () -> T) {
if (d.open()) {
body()
}
}

而反编译出来 c() 的字节码是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final void c();
Code:
0: new #8 // class com/maxtropy/viewtest/D1
3: dup
4: invokespecial #11 // Method com/maxtropy/viewtest/D1."<init>":()V
7: checkcast #13 // class com/maxtropy/viewtest/D
10: astore_1
11: aload_1
12: invokevirtual #17 // Method com/maxtropy/viewtest/D.open:()Z
15: ifeq 27
18: nop
19: invokestatic #23 // Method com/maxtropy/viewtest/CKt.foo1:()I
22: pop
23: invokestatic #26 // Method com/maxtropy/viewtest/CKt.foo2:()I
26: pop
27: nop
28: return

字节码非常清楚地展示了 inline 所作的优化效果

1. Inline Function 中的内容都完全被嵌入到了 c() 当中,这意味着减少了从 c() 到真正执行方法 foo1() foo2() 之间的的函数调用
2. lambda表达式对应的函数对象也完全消失,消除了因为保存lambda表达式函数对象而造成的内存损耗。

如果我们对比以下直接使用lambda表达式而不进行inline优化 会是什么样一种情况(仅将inline关键字拿掉其他不变):

1
2
3
4
5
6
7
8
9
10
public final void c();
Code:
0: new #8 // class com/maxtropy/viewtest/D1
3: dup
4: invokespecial #11 // Method com/maxtropy/viewtest/D1."<init>":()V
7: checkcast #13 // class com/maxtropy/viewtest/D
10: getstatic #19 // Field com/maxtropy/viewtest/C$c$1.INSTANCE:Lcom/maxtropy/viewtest/C$c$1;
13: checkcast #21 // class kotlin/jvm/functions/Function0
16: invokestatic #27 // Method com/maxtropy/viewtest/CKt.open:(Lcom/maxtropy/viewtest/D;Lkotlin/jvm/functions/Function0;)V
19: return

在我们关注的代码执行时,先获取了C类中一个对应的函数对象然后将其强制转型为Funtion0 (性能损耗1: 保存lambda函数对象),然后进入在C中定义的Top-Level function open()中继续执行 (性能损耗2:方法调用),在 open() 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static final <T> void open(com.maxtropy.viewtest.D, kotlin.jvm.functions.Function0<? extends T>);
Code:
0: aload_0
1: ldc #12 // String d
3: invokestatic #18 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: aload_1
7: ldc #20 // String body
9: invokestatic #18 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
12: aload_0
13: invokevirtual #25 // Method com/maxtropy/viewtest/D.open:()Z
16: ifeq 26
19: aload_1
20: invokeinterface #31, 1 // InterfaceMethod kotlin/jvm/functions/Function0.invoke:()Ljava/lang/Object;
25: pop
26: return

我们发现,

  • 由于写的是 NotNull 的参数,首先会对入参进行非空的检查(性能损耗3:进行不必要的非空检查),在d.open 确认之后开始执行lamdba函数块中的方法,在这里也就是执行函数对象对应的 invoke()性能损耗4: 再次方法调用)。在 invoke() 方法中才是真正的对 foo1() foo2() 方法进行调用:
1
2
3
4
5
6
public final int invoke();
Code:
0: invokestatic #23 // Method com/maxtropy/viewtest/CKt.foo1:()I
3: pop
4: invokestatic #26 // Method com/maxtropy/viewtest/CKt.foo2:()I
7: ireturn

Non-local returns

由于我们知道在使用 inline 时,Kotlin编译器会自动帮我们消除lambda函数对应的enclosing对象。因此,想要从lambda函数中使用 return 关键字来退出调用inline函数的enclosing函数 (上面第一个例子中inline函数是Ckt.class中的open(), 调用open()函数的enclosing函数是C.class中的c() )是可以的.
官方把这叫作 non-local returns.
根本原理:每一个lambda函数都对应一个enclosing函数,不带label 的return只能退出一个函数。(在Kotlin中普通的return同在Java中一样,相对应的都是方法返回字节码,而方法返回字节码的字面意就是退出处于当前栈顶的执行方法。)Inline Function的lambda函数执行其实都在enclosing函数的闭包中,return退出lambda其实也就是退出enclosing函数。
!!!】从上面的例子也可以很明显的看到,lambda不能直接退出没被 inline 修饰的函数是因为lambda函数的执行都是在其 Function对象的invoke() 中,因此lambda中的return也仅仅只是退出 invoke() 罢了。