前言
最近,無意中看到一篇文章,是聊inline在高階函數中的性能提升,說實話之前沒有認真關注過這個特性,所以藉此機會好好學習了一番。
高階函數:入參中含有lambda的函數(方法)。
原文是一位外國小哥寫的,這裡把它翻譯了一下重寫梳理了一遍發出來。也算是技術無國界吧,哈哈~
官方文檔對inline的使用主要提供了倆種方式:內聯類、內聯函數
正文
操作符是我們日常Kotlin開發的利器,如果我們點進去看看源碼,我們會發現這些操作符大多都會使用inline。
<code>inline funIterable /<code>.filter(predicate: (T)->Boolean): List {
val destination = ArrayList()
for (element in this)
if (predicate(element))
destination.add(element)
return destination
}
既然官方標準庫中如果使用,我們則需要驗證一下inline是不是真能有更好的性能:
<code>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)
}
}/<code>
倆個函數,除了inline沒什麼其他區別。接下來咱們執行個100000000次,看看方法耗時:
<code>var a = 0
repeat(100_000_000) {
a += 1
}
var b = 0
noinlineRepeat(100_000_000) {
b += 1
}/<code>
跑起來我們會發現:inlineRepeat()平均完成了0.335ns,而noinlineRepeat()平均需要153 980484.884ns。是46.6萬倍!看起來inline的確很重要,那麼這種性能改進是沒有成本的嗎?我們什麼時候應該使用inline?接下來咱們就來聊一聊這個問題,不過咱們先從一個基本的問題開始:inline有什麼作用?
inline有什麼用?
簡單來說被inline修飾過的函數,會在調用的時候把函數體替換過來。說起來可能很從抽象,直接看代碼:
<code>public inline fun print(message: Int) {
System.out.print(message)
}
fun main(args: Array<string>) {
print(2)
print(2)
}/<string>/<code>
反編譯class之後,我們會看到是這個樣子的:
<code> 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);
}/<code>
接下來咱們看看高階函數中的優化repeat(100) { println("A") },反編譯之後:
<code>for (index in 0 until 1000) {
println("A")
}/<code>
看到這我猜大家應該可以理解inline的作用了吧。不過,話又說回來。“僅僅”做了這點改動,會什麼會有如此大的性能提升?解答這個問題,不得不聊一聊JVM是如何實現Lambda的。
Lambda的原理
一般來說,會有倆種方案:
- 匿名類
- “附加”類
咱們直接通過一個demo來看這倆種實現:
<code>val lambda: ()->Unit = {
// body
}/<code>
對於匿名類的實現來說,反編譯是這樣的:
<code>Function0 lambda = new Function0() {
public Object invoke() {
// body
}
};/<code>
對於“附加”類的實現來說,反編譯是這樣的:
<code>// Additional class in separate file
public class TestInlineKt$lambda implements Function0 {
public Object invoke() {
// code
}
}
// Usage
Function0 lambda = new TestInlineKt$lambda()/<code>
有了上邊的代碼,咱們也就明白高階函數的開銷為什麼這麼大:畢竟每一個Lambda都會額外創建一個類。接下來咱們通過一個demo進一步感受這些額外的開銷:
<code>fun main(args: Array<string>) {
var a = 0
repeat(100_000_000) {
a += 1
}
var b = 0
noinlineRepeat(100_000_000) {
b += 1
}
}/<string>/<code>
反編譯之後:
<code>public static final void main(@NotNull String[] args) {
int a = 0;
int times$iv = 100000000;
int var3 = 0;
for(int var4 = times$iv; 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;
}
}));
}/<code>
inline的成本
inline並不是沒有任何成本的。其實咱們最上邊看public inline fun print(message: Int) { System.out.print(message) }的時候,看反編譯的內容也能看出它得到的成本。
接下來咱們就基於這個print()函數,來對比一下:
<code>fun main(args: Array<string>) {
print(2)
print(2)
System.out.print(2)
}/<string>/<code>
反編譯如下:
<code>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);
}/<code>
可以看出inline額外生成了一些代碼,這也就是它額外的開銷。因此咱們在使用inline的時候還是需要有一定的規則的,以免適得其反。
最佳實踐
當我們沒有高階函數、沒有使用reified關鍵詞時不應該隨意使用inline,徒增消耗。
尾聲
到此這篇文章就結束了。
但是看了外國小哥這篇文章的時候,的確發現自己有很多內容是有遺漏的。所以接下來如果有機會的話,會繼續寫或者翻譯一些這類“最佳實踐”的文章。
閱讀更多 碼農登陸 的文章