前言

最近,無意中看到一篇文章,是聊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,徒增消耗。

尾聲

到此這篇文章就結束了。

但是看了外國小哥這篇文章的時候,的確發現自己有很多內容是有遺漏的。所以接下來如果有機會的話,會繼續寫或者翻譯一些這類“最佳實踐”的文章。

相關文章