Inline Basics
Inline or Inlining,我們更經(jīng)常聽到的詞是方法內(nèi)聯(lián)或者內(nèi)聯(lián)函數(shù)。在大多數(shù)情況下,他們指的都是同一個(gè)意思。即,在編譯期間對(duì)函數(shù)進(jìn)行優(yōu)化,以便讓代碼在機(jī)器執(zhí)行時(shí)獲得更高的效率。
方法內(nèi)聯(lián)一般可能出現(xiàn)在兩個(gè)階段:
- 編譯器:編譯輸出.class文件時(shí)
- JVM:編譯輸入機(jī)器執(zhí)行碼時(shí)
在Java領(lǐng)域,方法內(nèi)聯(lián)是商用JVM登峰造極的虛擬機(jī)優(yōu)化中重要的一環(huán),下面節(jié)選自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的方法內(nèi)聯(lián)帶來兩個(gè)優(yōu)點(diǎn):
- 可以動(dòng)態(tài)的減少方法調(diào)用來提高執(zhí)行效率
- 可以極大地提高其他優(yōu)化手段的優(yōu)化效果
但其實(shí)文檔應(yīng)該還想表達(dá)另外一件事情:方法內(nèi)聯(lián)的優(yōu)化效果很難在JVM外部,更不用說單獨(dú)觀測(cè)到。
HotSpot的方法內(nèi)聯(lián)是(晚期)JVM運(yùn)行期進(jìn)行方法內(nèi)聯(lián)的代表作之一,而Kotlin Inline Functions則是典型的在(早期)編譯期進(jìn)行方法內(nèi)聯(lián)。
Inline Function
首先,Kotlin的內(nèi)聯(lián)方法優(yōu)化針對(duì)的是lambda表達(dá)式(如果不是針對(duì)lambda表達(dá)式使用IDE亦提示)。
我們知道,在Kotlin中,F(xiàn)unction是"一等公民",每一個(gè)函數(shù)都是一個(gè)對(duì)象,并且擁有其對(duì)應(yīng)的閉包。也即是說:
1. 對(duì)于每一個(gè)lambda函數(shù)都需要分配一定的內(nèi)存來創(chuàng)建和維護(hù)Function對(duì)象。
2. 在調(diào)用lambda函數(shù)的時(shí)候,需要先訪問到閉包對(duì)象,再訪問到真正的函數(shù)執(zhí)行塊。
在多數(shù)情況下這些東西對(duì)性能的影響可以忽略不計(jì),但是在對(duì)性能有要求時(shí),這便是我們可以優(yōu)化提升的地方。而Kotlin Inline Function 可以輕松地消除這些東西對(duì)于性能的損耗,而實(shí)現(xiàn)的方式類似于JVM運(yùn)行期的Method Inlining(方法內(nèi)聯(lián)),這也是為什么Kotlin將這一手段稱之為"Inline Function"
通過查看編譯生成的.class文件字節(jié)碼,可以清楚地看到Inline Function做的事情:
考慮這樣一個(gè)例子,我們需要確保在 c() 中先執(zhí)行了D類的實(shí)例方法 open() ,如果執(zhí)行成功了再執(zhí)行一塊函數(shù)塊(lambda),里面執(zhí)行 foo1() foo2()。對(duì)于這個(gè)lambda函數(shù)塊的執(zhí)行使用 inline 來優(yōu)化。
寫成.kt (Kotlin Source File)是這個(gè)樣子:
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() 的字節(jié)碼是這樣:
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
字節(jié)碼非常清楚地展示了 inline 所作的優(yōu)化效果:
1. Inline Function 中的內(nèi)容都完全被嵌入到了 c() 當(dāng)中,這意味著減少了從 c() 到真正執(zhí)行方法 foo1() foo2() 之間的的函數(shù)調(diào)用
2. lambda表達(dá)式對(duì)應(yīng)的函數(shù)對(duì)象也完全消失,消除了因?yàn)楸4鎙ambda表達(dá)式函數(shù)對(duì)象而造成的內(nèi)存損耗。
如果我們對(duì)比以下直接使用lambda表達(dá)式而不進(jìn)行inline優(yōu)化 會(huì)是什么樣一種情況(僅將inline關(guān)鍵字拿掉其他不變):
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
在我們關(guān)注的代碼執(zhí)行時(shí),先獲取了C類中一個(gè)對(duì)應(yīng)的函數(shù)對(duì)象然后將其強(qiáng)制轉(zhuǎn)型為Funtion0 (性能損耗1: 保存lambda函數(shù)對(duì)象),然后進(jìn)入在C中定義的Top-Level function open()中繼續(xù)執(zhí)行 (性能損耗2:方法調(diào)用),在 open() 中:
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
我們發(fā)現(xiàn),
- 由于寫的是 NotNull 的參數(shù),首先會(huì)對(duì)入?yún)⑦M(jìn)行非空的檢查(性能損耗3:進(jìn)行不必要的非空檢查),在d.open 確認(rèn)之后開始執(zhí)行l(wèi)amdba函數(shù)塊中的方法,在這里也就是執(zhí)行函數(shù)對(duì)象對(duì)應(yīng)的 invoke() (性能損耗4: 再次方法調(diào)用)。在 invoke() 方法中才是真正的對(duì) foo1() foo2() 方法進(jìn)行調(diào)用:
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
哇!你猜Inline Function為我們做的性能優(yōu)化 是不是不是很少 呢?
Non-local returns
由于我們知道在使用 inline 時(shí),Kotlin編譯器會(huì)自動(dòng)幫我們消除lambda函數(shù)對(duì)應(yīng)的enclosing對(duì)象。因此,想要從lambda函數(shù)中使用 return 關(guān)鍵字來退出調(diào)用inline函數(shù)的enclosing函數(shù) (上面第一個(gè)例子中inline函數(shù)是Ckt.class中的open(), 調(diào)用open()函數(shù)的enclosing函數(shù)是C.class中的c() )是可以的.
官方把這叫作 non-local returns.
根本原理:每一個(gè)lambda函數(shù)都對(duì)應(yīng)一個(gè)enclosing函數(shù),不帶label 的return只能退出一個(gè)函數(shù)。(在Kotlin中普通的return同在Java中一樣,相對(duì)應(yīng)的都是方法返回字節(jié)碼,而方法返回字節(jié)碼的字面意就是退出處于當(dāng)前棧頂?shù)膱?zhí)行方法。)Inline Function的lambda函數(shù)執(zhí)行其實(shí)都在enclosing函數(shù)的閉包中,return退出lambda其實(shí)也就是退出enclosing函數(shù)。
【!!!】從上面的例子也可以很明顯的看到,lambda不能直接退出沒被 inline 修飾的函數(shù)是因?yàn)閘ambda函數(shù)的執(zhí)行都是在其 Function對(duì)象的invoke() 中,因此lambda中的return也僅僅只是退出 invoke() 罷了。