一葛躏、前言
Kotlin標(biāo)準(zhǔn)庫(kù)中所有集合操作的函數(shù)都是內(nèi)聯(lián)的( inline ),例如:
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}
public inline fun <T> Iterable<T>.forEachIndexed(action: (index: Int, T) -> Unit): Unit {
var index = 0
for (item in this) action(checkIndexOverflow(index++), item)
}
這個(gè) inline 修飾符有多重要呢?
假設(shè)我們有 5000 件商品讼积,我們需要對(duì)已購(gòu)買(mǎi)的商品計(jì)算總價(jià),我們可以通過(guò)以下方式完成:
products.filter{ it.bought }.sumByDouble { it.price }
在我的機(jī)器上脚仔,運(yùn)行上述代碼平均需要38毫秒勤众。如果這個(gè)函數(shù)不是內(nèi)聯(lián)的話(huà)會(huì)是多長(zhǎng)時(shí)間呢? 不是內(nèi)聯(lián)在我的機(jī)器上大概平均42毫秒。你們可以自己檢查嘗試下鲤脏,這里是完整源碼. 這似乎看起來(lái)差距不是很大们颜,但每調(diào)用一次這個(gè)函數(shù)對(duì)集合進(jìn)行處理時(shí),你都會(huì)注意到這個(gè)時(shí)間差距大約為10%左右猎醇。
當(dāng)我們修改lambda表達(dá)式中的局部變量時(shí)掌桩,可以發(fā)現(xiàn)差距將會(huì)更大。對(duì)比下面兩個(gè)函數(shù):
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)
}
}
除了函數(shù)名不一樣之外姑食,唯一的區(qū)別就是第一個(gè)函數(shù)使用inline修飾符波岛,而第二個(gè)函數(shù)沒(méi)有。用法也是完全一樣的:
var a = 0
repeat(100_000_000) {
a += 1
}
var b = 0
noinlineRepeat(100_000_000) {
b += 1
}
上述代碼在執(zhí)行時(shí)間上對(duì)比有很大的差異音半。內(nèi)聯(lián)的repeat函數(shù)平均運(yùn)行時(shí)間是0.335ns, 而noinlineRepeat函數(shù)平均運(yùn)行時(shí)間是153980484.884ns则拷。大概是內(nèi)聯(lián)repeat函數(shù)運(yùn)行時(shí)間的466000倍! 你們可以自己檢查嘗試下,這里是完整源碼.
為什么這個(gè)如此重要呢? 這種性能的提升是否有其他的成本呢? 我們應(yīng)該什么時(shí)候使用內(nèi)聯(lián)(inline)修飾符呢?這些都是重點(diǎn)問(wèn)題曹鸠,我們將盡力回答這些問(wèn)題煌茬。然而這一切都需要從最基本的問(wèn)題開(kāi)始: 內(nèi)聯(lián)修飾符到底有什么作用?
二、內(nèi)聯(lián)修飾符有什么作用彻桃?
我們都知道函數(shù)通常是如何被調(diào)用的坛善。先執(zhí)行跳轉(zhuǎn)到函數(shù)體,然后執(zhí)行函數(shù)體內(nèi)所有的語(yǔ)句邻眷,最后跳回到最初調(diào)用函數(shù)的位置眠屎。盡管強(qiáng)行對(duì)函數(shù)使用inline修飾符標(biāo)記,但是編譯器將會(huì)以不同的方式來(lái)對(duì)它進(jìn)行處理肆饶。在代碼編譯期間改衩,它用它的主體替換這樣的函數(shù)調(diào)用。 print函數(shù)是inline函數(shù):
public inline fun print(message: Int) {
System.out.print(message)
}
當(dāng)我們?cè)趍ain函數(shù)中調(diào)用它時(shí):
fun main(args: Array<String>) {
print(2)
print(2)
}
編譯后驯镊,它將變成下面這樣:
public static final void main(@NotNull String[] args) {
System.out.print(2)
System.out.print(2)
}
這里有一點(diǎn)不一樣的是我們不需要跳回到另一個(gè)函數(shù)中葫督。雖然這種影響可以忽略不計(jì)竭鞍。
這就是為什么你定義這樣的內(nèi)聯(lián)函數(shù)時(shí)會(huì)在IDEA IntelliJ中發(fā)出以下警告:
Expected performance impact from inline is insignificant. Inlining works best for functions with parameters of functional types
意思就是:內(nèi)聯(lián)不會(huì)有顯著的影響,它比函數(shù)要好橄镜。
為什么IntelliJ建議我們?cè)诤衛(wèi)ambda表達(dá)式作為形參的函數(shù)中使用內(nèi)聯(lián)呢偎快?因?yàn)楫?dāng)我們內(nèi)聯(lián)函數(shù)體時(shí),我們不需要從參數(shù)中創(chuàng)建lambda表達(dá)式實(shí)例洽胶,而是可以將它們內(nèi)聯(lián)到函數(shù)調(diào)用中來(lái)晒夹。這個(gè)是上述repeat函數(shù)的調(diào)用:
repeat(100) { println("A") }
將會(huì)編譯成這樣:
for (index in 0 until 1000) {
println("A")
}
正如你所看見(jiàn)的那樣,lambda表達(dá)式的主體println("A")替換了內(nèi)聯(lián)函數(shù)repeat中action(index)的調(diào)用妖异。
讓我們看另一外個(gè)例子惋戏。filter函數(shù)的用法:
val products2 = products.filter { it.bought }
替換為:
val destination = ArrayList<T>()
for (element in this)
if (predicate(element))
destination.add(element)
val products2 = destination
這是一項(xiàng)非常重要的改進(jìn)。這是因?yàn)镴VM天然地不支持lambda表達(dá)式他膳。
說(shuō)清楚lambda表達(dá)式是如何被編譯的是件很復(fù)雜的事响逢。但總的來(lái)說(shuō),有兩種結(jié)果:
- 匿名類(lèi)
- 單獨(dú)的類(lèi)
我們來(lái)看個(gè)例子棕孙。我們有以下lambda表達(dá)式:
val lambda: ()->Unit = {
// body
}
它變成了JVM中的匿名類(lèi):
// Java
Function0 lambda = new Function0() {
public Object invoke() {
// code
}
};
或者它變成了單獨(dú)的文件中定義的普通類(lèi):
// Java
// Additional class in separate file
public class TestInlineKt$lambda implements Function0 {
public Object invoke() {
// code
}
}
// Usage
Function0 lambda = new TestInlineKt$lambda()
第二種效率更高舔亭,我們盡可能使用這種。僅僅當(dāng)我們需要使用局部變量時(shí)蟀俊,第一種才是必要的钦铺。
這就是為什么當(dāng)我們修改局部變量時(shí),repeat和noinlineRepeat之間存在如此之大的運(yùn)行速度差異的原因:
非內(nèi)聯(lián)函數(shù)中的Lambda需要編譯為匿名類(lèi)肢预!
這是一個(gè)巨大的性能開(kāi)銷(xiāo)矛洞,從而導(dǎo)致它們的創(chuàng)建和使用都較慢。
當(dāng)我們使用內(nèi)聯(lián)函數(shù)時(shí)烫映,我們根本不需要?jiǎng)?chuàng)建任何其他類(lèi)沼本。
自己檢查一下。編譯這段代碼并把它反編譯為Java代碼:
fun main(args: Array<String>) {
var a = 0
repeat(100_000_000) {
a += 1
}
var b = 0
noinlineRepeat(100_000_000) {
b += 1
}
}
你會(huì)發(fā)現(xiàn)一些相似的東西
// Show Kotlin Bytecode
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
int a = 0;
int times$iv = 100000000;
int $i$f$repeat = false;
int var4 = 0;
for(int var5 = times$iv; var4 < var5; ++var4) {
int var7 = false;
++a;
}
final IntRef b = new IntRef();
b.element = 0;
noinlineRepeat(100000000, (Function1)(new Function1() {
public Object invoke(Object var1) {
this.invoke(((Number)var1).intValue());
return Unit.INSTANCE;
}
public final void invoke(int it) {
++b.element;
}
}));
}
在filter函數(shù)例子中锭沟,使用內(nèi)聯(lián)函數(shù)改進(jìn)效果不是那么明顯抽兆,這是因?yàn)閘ambda表達(dá)式在非內(nèi)聯(lián)函數(shù)中是編譯成普通的類(lèi)而非匿名類(lèi)。所以它的創(chuàng)建和使用效率還算比較高族淮,但仍有性能開(kāi)銷(xiāo)辫红,所以也就證明了最開(kāi)始那個(gè)filter例子為什么只有10%的運(yùn)行速度差異。
三祝辣、集合流處理方式與經(jīng)典處理方式
內(nèi)聯(lián)修飾符是一個(gè)非常關(guān)鍵的元素贴妻,它能使集合流處理的方式與基于循環(huán)的經(jīng)典處理方式一樣高效。它經(jīng)過(guò)一次又一次的測(cè)試较幌,在代碼可讀性和性能方面已經(jīng)優(yōu)化到極點(diǎn)了揍瑟,并且相比之下經(jīng)典處理方式總是有很大的成本。例如乍炉,下面的代碼:
return data.filter { filterLoad(it) }.map { mapLoad(it) }
工作原理與下面代碼相同并具有相同的執(zhí)行時(shí)間:
val list = ArrayList<String>()
for (it in data) {
if (filterLoad(it)) {
val value = mapLoad(it)
list.add(value)
}
}
return list
基準(zhǔn)測(cè)量的具體結(jié)果(源碼在這里):
Benchmark (size) Mode Cnt Score Error Units
filterAndMap 10 avgt 200 561.249 ± 1 ns/op
filterAndMap 1000 avgt 200 29803.183 ± 127 ns/op
filterAndMap 100000 avgt 200 3859008.234 ± 50022 ns/op
filterAndMapManual 10 avgt 200 526.825 ± 1 ns/op
filterAndMapManual 1000 avgt 200 28420.161 ± 94 ns/op
filterAndMapManual 100000 avgt 200 3831213.798 ± 34858 ns/op
四绢片、內(nèi)聯(lián)修飾符的成本
通過(guò)上面已經(jīng)得出,內(nèi)聯(lián)函數(shù)岛琼,實(shí)際就是替換掉你原來(lái)的代碼底循,改為內(nèi)聯(lián)函數(shù)中的代碼。
因此槐瑞,內(nèi)聯(lián)的優(yōu)點(diǎn)是代碼簡(jiǎn)潔熙涤,可讀性強(qiáng);缺點(diǎn)是編譯后的代碼體積會(huì)變大(變大多少取決于用了多少 inline )困檩。
五祠挫、內(nèi)聯(lián)修飾符在不同方面的用法
內(nèi)聯(lián)修飾符因?yàn)樗厥獾恼Z(yǔ)法特性而發(fā)生的變化遠(yuǎn)遠(yuǎn)超過(guò)我們?cè)诒酒恼轮锌吹降膬?nèi)容。
它可以實(shí)化泛型類(lèi)型悼沿。但是它也有一些局限性等舔。
我們使用內(nèi)聯(lián)修飾符時(shí)最常見(jiàn)的場(chǎng)景就是:
- 把函數(shù)作為另一個(gè)函數(shù)的參數(shù)時(shí)(高階函數(shù))糟趾;
- 集合或字符串處理(如filter,map或者joinToString)慌植;
- 一些獨(dú)立的函數(shù)(如repeat)
這就是為什么inline修飾符經(jīng)常被庫(kù)開(kāi)發(fā)人員用來(lái)做一些重要優(yōu)化的原因了。他們應(yīng)該知道它是如何工作的义郑,哪里還需要被改進(jìn)以及使用成本是什么蝶柿。當(dāng)我們使用函數(shù)類(lèi)型作為參數(shù)來(lái)定義自己的工具類(lèi)函數(shù)時(shí),我們也需要在項(xiàng)目中使用inline修飾符非驮。當(dāng)我們沒(méi)有函數(shù)類(lèi)型作為參數(shù)交汤,沒(méi)有reified實(shí)化類(lèi)型參數(shù)并且也不需要非本地返回時(shí),那么我們很可能不應(yīng)該使用inline修飾符了劫笙。這就是為什么我們?cè)诜巧鲜銮闆r下使用inline修飾符會(huì)在Android Studio或IDEA IntelliJ得到一個(gè)警告原因芙扎。