Kotlin —— 內(nèi)聯(lián)函數(shù)

一葛躏、前言

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)型悼沿。但是它也有一些局限性等舔。

\color{red}{一般來(lái)說(shuō),我們應(yīng)該什么時(shí)候使用內(nèi)聯(lián)修飾符呢?}

我們使用內(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è)警告原因芙扎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市邀摆,隨后出現(xiàn)的幾起案子纵顾,更是在濱河造成了極大的恐慌,老刑警劉巖栋盹,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件施逾,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡例获,警方通過(guò)查閱死者的電腦和手機(jī)汉额,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)榨汤,“玉大人蠕搜,你說(shuō)我怎么就攤上這事∈蘸荆” “怎么了妓灌?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵轨蛤,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我虫埂,道長(zhǎng)祥山,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任掉伏,我火速辦了婚禮缝呕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘斧散。我一直安慰自己供常,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布鸡捐。 她就那樣靜靜地躺著栈暇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪闯参。 梳的紋絲不亂的頭發(fā)上瞻鹏,一...
    開(kāi)封第一講書(shū)人閱讀 51,737評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音鹿寨,去河邊找鬼新博。 笑死,一個(gè)胖子當(dāng)著我的面吹牛脚草,可吹牛的內(nèi)容都是我干的赫悄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼馏慨,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼埂淮!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起写隶,我...
    開(kāi)封第一講書(shū)人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤倔撞,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后慕趴,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體痪蝇,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年冕房,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了躏啰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡耙册,死狀恐怖给僵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情详拙,我是刑警寧澤帝际,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布蔓同,位于F島的核電站,受9級(jí)特大地震影響胡本,放射性物質(zhì)發(fā)生泄漏牌柄。R本人自食惡果不足惜畸悬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一侧甫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蹋宦,春花似錦披粟、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蒿辙,卻和暖如春拇泛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背思灌。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工俺叭, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人泰偿。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓熄守,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親耗跛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子裕照,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 在Kotlin中,lambda表達(dá)式會(huì)被正常的編譯成匿名類(lèi)调塌。這表示每調(diào)用一次lambda表達(dá)式晋南,一個(gè)額外的類(lèi)就會(huì)被...
    leilifengxingmw閱讀 973評(píng)論 0 3
  • 一、內(nèi)聯(lián)函數(shù)原理 使用高階函數(shù)為開(kāi)發(fā)帶來(lái)了便利羔砾,但同時(shí)也產(chǎn)生了一些性能上的損失负间,官方是這樣描述這個(gè)問(wèn)題: 使用高階...
    SheHuan閱讀 5,107評(píng)論 1 21
  • Kotlin 中新增了「內(nèi)聯(lián)函數(shù)」,內(nèi)聯(lián)函數(shù)起初是在 C++ 里面的蜒茄。 那在 Kotlin 中加入內(nèi)聯(lián)函數(shù)唉擂,是有什...
    chendroid閱讀 23,334評(píng)論 12 61
  • kotlin的內(nèi)聯(lián)函數(shù)屬于kotlin的高級(jí)特性了,也是不同于java的區(qū)別之一;至于為什么kotlin要使用內(nèi)聯(lián)...
    Villa__Mou閱讀 9,209評(píng)論 0 25
  • Kotlin里使用關(guān)鍵 inline 來(lái)表示內(nèi)聯(lián)函數(shù),那么到底什么是內(nèi)聯(lián)函數(shù)呢檀葛,內(nèi)聯(lián)函數(shù)有什么好處呢玩祟? 1. 什么...
    云飛揚(yáng)1閱讀 2,870評(píng)論 0 58