[译]Effective Kotlin系列之探索高阶函数中inline修饰符(三)
簡述: 不知道是否有小伙伴還記得我們之前的Effective Kotlin翻譯系列,之前一直忙于趕時(shí)髦研究Kotlin 1.3中的新特性。把此系列耽擱了,趕完時(shí)髦了還是得踏實(shí)探究本質(zhì)和基礎(chǔ),從今天開始我們將繼續(xù)探索Effective Kotlin系列,今天是Effective Kotlin第三講。
翻譯說明:
原標(biāo)題: Effective Kotlin: Consider inline modifier for higher-order functions
原文地址: blog.kotlin-academy.com/effective-k…
原文作者: Marcin Moskala
你或許已經(jīng)注意到了所有集合操作的函數(shù)都是內(nèi)聯(lián)的(inline)。你是否問過自己它們?yōu)槭裁匆@么定義呢? 例如,這是Kotlin標(biāo)準(zhǔn)庫中的filter函數(shù)的簡化版本的源碼:
inline fun <T> Iterable<T>.filter(predicate: (T)->Boolean): List<T>{val destination = ArrayList<T>()for (element in this) if (predicate(element))destination.add(element)return destination } 復(fù)制代碼這個(gè)inline修飾符到底有多重要呢? 假設(shè)我們有5000件商品,我們需要對已經(jīng)購買的商品累計(jì)算出總價(jià)。我們可以通過以下方式完成:
products.filter{ it.bought }.sumByDouble { it.price } 復(fù)制代碼在我的機(jī)器上,運(yùn)行上述代碼平均需要38毫秒。如果這個(gè)函數(shù)不是內(nèi)聯(lián)的話會(huì)是多長時(shí)間呢? 不是內(nèi)聯(lián)在我的機(jī)器上大概平均42毫秒。你們可以自己檢查嘗試下,這里是完整源碼. 這似乎看起來差距不是很大,但每調(diào)用一次這個(gè)函數(shù)對集合進(jìn)行處理時(shí),你都會(huì)注意到這個(gè)時(shí)間差距大約為10%左右。
當(dāng)我們修改lambda表達(dá)式中的局部變量時(shí),可以發(fā)現(xiàn)差距將會(huì)更大。對比下面兩個(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)} } 復(fù)制代碼你可能已經(jīng)注意到除了函數(shù)名不一樣之外,唯一的區(qū)別就是第一個(gè)函數(shù)使用inline修飾符,而第二個(gè)函數(shù)沒有。用法也是完全一樣的:
var a = 0 repeat(100_000_000) {a += 1 } var b = 0 noinlineRepeat(100_000_000) {b += 1 } 復(fù)制代碼上述代碼在執(zhí)行時(shí)間上對比有很大的差異。內(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)問題,我們將盡力回答這些問題。然而這一切都需要從最基本的問題開始: 內(nèi)聯(lián)修飾符到底有什么作用?
內(nèi)聯(lián)修飾符有什么作用?
我們都知道函數(shù)通常是如何被調(diào)用的。先執(zhí)行跳轉(zhuǎn)到函數(shù)體,然后執(zhí)行函數(shù)體內(nèi)所有的語句,最后跳回到最初調(diào)用函數(shù)的位置。
盡管強(qiáng)行對函數(shù)使用inline修飾符標(biāo)記,但是編譯器將會(huì)以不同的方式來對它進(jìn)行處理。在代碼編譯期間,它用它的主體替換這樣的函數(shù)調(diào)用。 print函數(shù)是inline函數(shù):
public inline fun print(message: Int) {System.out.print(message) } 復(fù)制代碼當(dāng)我們在main函數(shù)中調(diào)用它時(shí):
fun main(args: Array<String>) {print(2)print(2) } 復(fù)制代碼編譯后,它將變成下面這樣:
public static final void main( String[] args) {System.out.print(2)System.out.print(2) } 復(fù)制代碼這里有一點(diǎn)不一樣的是我們不需要跳回到另一個(gè)函數(shù)中。雖然這種影響可以忽略不計(jì)。這就是為什么你定義這樣的內(nèi)聯(lián)函數(shù)時(shí)會(huì)在IDEA IntelliJ中發(fā)出以下警告:
為什么IntelliJ建議我們在含有l(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)用中來。這個(gè)是上述repeat函數(shù)的調(diào)用:
repeat(100) { println("A") } 復(fù)制代碼將會(huì)編譯成這樣:
for (index in 0 until 1000) {println("A") } 復(fù)制代碼正如你所看見的那樣,lambda表達(dá)式的主體println("A")替換了內(nèi)聯(lián)函數(shù)repeat中action(index)的調(diào)用。讓我們看另一外個(gè)例子。filter函數(shù)的用法:
val products2 = products.filter { it.bought } 復(fù)制代碼將被替換為:
val destination = ArrayList<T>() for (element in this) if (predicate(element))destination.add(element) val products2 = destination 復(fù)制代碼這是一項(xiàng)非常重要的改進(jìn)。這是因?yàn)镴VM天然地不支持lambda表達(dá)式。說清楚lambda表達(dá)式是如何被編譯的是件很復(fù)雜的事。但總的來說,有兩種結(jié)果:
- 匿名類
- 單獨(dú)的類
我們來看個(gè)例子。我們有以下lambda表達(dá)式:
val lambda: ()->Unit = {// body } 復(fù)制代碼它變成了JVM中的匿名類:
// Java Function0 lambda = new Function0() {public Object invoke() {// code} }; 復(fù)制代碼或者它變成了單獨(dú)的文件中定義的普通類:
// Java // Additional class in separate file public class TestInlineKt$lambda implements Function0 {public Object invoke() {// code} } // Usage Function0 lambda = new TestInlineKt$lambda() 復(fù)制代碼第二種效率更高,我們盡可能使用這種。僅僅當(dāng)我們需要使用局部變量時(shí),第一種才是必要的。
這就是為什么當(dāng)我們修改局部變量時(shí),repeat和noinlineRepeat之間存在如此之大的運(yùn)行速度差異的原因。非內(nèi)聯(lián)函數(shù)中的Lambda需要編譯為匿名類。這是一個(gè)巨大的性能開銷,從而導(dǎo)致它們的創(chuàng)建和使用都較慢。當(dāng)我們使用內(nèi)聯(lián)函數(shù)時(shí),我們根本不需要?jiǎng)?chuàng)建任何其他類。自己檢查一下。編譯這段代碼并把它反編譯為Java代碼:
fun main(args: Array<String>) {var a = 0repeat(100_000_000) {a += 1}var b = 0noinlineRepeat(100_000_000) {b += 1} } 復(fù)制代碼你會(huì)發(fā)現(xiàn)一些相似的東西:
/ Java public static final void main(@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;}})); } 復(fù)制代碼在filter函數(shù)例子中,使用內(nèi)聯(lián)函數(shù)改進(jìn)效果不是那么明顯,這是因?yàn)閘ambda表達(dá)式在非內(nèi)聯(lián)函數(shù)中是編譯成普通的類而非匿名類。所以它的創(chuàng)建和使用效率還算比較高,但仍有性能開銷,所以也就證明了最開始那個(gè)filter例子為什么只有10%的運(yùn)行速度差異。
集合流處理方式與經(jīng)典處理方式
內(nèi)聯(lián)修飾符是一個(gè)非常關(guān)鍵的元素,它能使集合流處理的方式與基于循環(huán)的經(jīng)典處理方式一樣高效。它經(jīng)過一次又一次的測試,在代碼可讀性和性能方面已經(jīng)優(yōu)化到極點(diǎn)了,并且相比之下經(jīng)典處理方式總是有很大的成本。例如,下面的代碼:
return data.filter { filterLoad(it) }.map { mapLoad(it) } 復(fù)制代碼工作原理與下面代碼相同并具有相同的執(zhí)行時(shí)間:
val list = ArrayList<String>() for (it in data) {if (filterLoad(it)) {val value = mapLoad(it)list.add(value)} } return list 復(fù)制代碼基準(zhǔn)測量的具體結(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/opfilterAndMapManual 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 復(fù)制代碼從程序的角度來看,這兩個(gè)函數(shù)幾乎相同。盡管從可讀性的角度來看第一種方式要好很多,這就是為什么我們應(yīng)該總是寧愿使用智能的集合流處理函數(shù)而不是自己去實(shí)現(xiàn)整個(gè)處理過程。此外如果stalib庫中集合處理函數(shù)不能滿足我們的需求時(shí),請不要猶豫,自己動(dòng)手編寫集合處理函數(shù)。例如,當(dāng)我需要轉(zhuǎn)置集合中的集合時(shí),這是我在上一個(gè)項(xiàng)目中添加的函數(shù):
fun <E> List<List<E>>.transpose(): List<List<E>> {if (isEmpty()) return thisval width = first().sizeif (any { it.size != width }) {throw IllegalArgumentException("All nested lists must have the same size, but sizes were ${map { it.size }}")}return (0 until width).map { col ->(0 until size).map { row -> this[row][col] }} } 復(fù)制代碼記得寫一些單元測試:
class TransposeTest {private val list = listOf(listOf(1, 2, 3), listOf(4, 5, 6))fun `Transposition of transposition is identity`() {Assert.assertEquals(list, list.transpose().transpose())}fun `Simple transposition test`() {val transposed = listOf(listOf(1, 4), listOf(2, 5), listOf(3, 6))assertEquals(transposed, list.transpose())} } 復(fù)制代碼內(nèi)聯(lián)修飾符的成本
內(nèi)聯(lián)不應(yīng)該被過度使用,因?yàn)樗彩怯谐杀镜摹N蚁朐诖a中打印出更多的數(shù)字2, 所以我就定義了下面這個(gè)函數(shù):
inline fun twoPrintTwo() {print(2)print(2) } 復(fù)制代碼這對我來說可能還不夠,所以我添加了這個(gè)函數(shù):
inline fun twoTwoPrintTwo() {twoPrintTwo()twoPrintTwo() } 復(fù)制代碼還是不滿意。我又定義了以下這兩個(gè)函數(shù):
inline fun twoTwoTwoPrintTwo() {twoTwoPrintTwo()twoTwoPrintTwo() }fun twoTwoTwoTwoPrintTwo() {twoTwoTwoPrintTwo()twoTwoTwoPrintTwo() } 復(fù)制代碼然后我決定檢查編譯后的代碼中發(fā)生了什么,所以我將編譯為JVM字節(jié)碼然后將它反編譯成Java代碼。twoTwoPrintTwo函數(shù)已經(jīng)很長了:
public static final void twoTwoPrintTwo() {byte var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1); } 復(fù)制代碼但是twoTwoTwoTwoPrintTwo就更加恐怖了
public static final void twoTwoTwoTwoPrintTwo() {byte var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1);var1 = 2;System.out.print(var1); } 復(fù)制代碼這說明了內(nèi)聯(lián)函數(shù)的主要問題: 當(dāng)我們過度使用它們時(shí),會(huì)使得代碼體積不斷增大。這實(shí)際上就是為什么當(dāng)我們使用他們時(shí)IntelliJ會(huì)給出警告提示。
內(nèi)聯(lián)修飾符在不同方面的用法
內(nèi)聯(lián)修飾符因?yàn)樗厥獾恼Z法特性而發(fā)生的變化遠(yuǎn)遠(yuǎn)超過我們在本篇文章中看到的內(nèi)容。它可以實(shí)化泛型類型。但是它也有一些局限性。雖然這與Effective Kotlin系列無關(guān)并且屬于是另外一個(gè)話題。如果你想要我闡述更多有關(guān)它,請?jiān)赥witter或評論中表達(dá)你的想法。
一般來說,我們應(yīng)該什么時(shí)候使用內(nèi)聯(lián)修飾符呢?
我們使用內(nèi)聯(lián)修飾符時(shí)最常見的場景就是把函數(shù)作為另一個(gè)函數(shù)的參數(shù)時(shí)(高階函數(shù))。集合或字符串處理(如filter,map或者joinToString)或者一些獨(dú)立的函數(shù)(如repeat)就是很好的例子。
這就是為什么inline修飾符經(jīng)常被庫開發(fā)人員用來做一些重要優(yōu)化的原因了。他們應(yīng)該知道它是如何工作的,哪里還需要被改進(jìn)以及使用成本是什么。當(dāng)我們使用函數(shù)類型作為參數(shù)來定義自己的工具類函數(shù)時(shí),我們也需要在項(xiàng)目中使用inline修飾符。當(dāng)我們沒有函數(shù)類型作為參數(shù),沒有reified實(shí)化類型參數(shù)并且也不需要非本地返回時(shí),那么我們很可能不應(yīng)該使用inline修飾符了。這就是為什么我們在非上述情況下使用inline修飾符會(huì)在Android Studio或IDEA IntelliJ得到一個(gè)警告原因。
譯者有話說
這是Effective Kotlin系列第三篇文章,講得是inline內(nèi)聯(lián)函數(shù)存在使用時(shí)潛在隱患,一旦使用不當(dāng)或者過度使用就會(huì)造成性能上損失。基于這一點(diǎn)原作者從發(fā)現(xiàn)問題到剖析整個(gè)inline內(nèi)聯(lián)函數(shù)原理以及最后如何去選擇在哪種場景下使用內(nèi)聯(lián)函數(shù)。我相信有了這篇文章,你對Kotlin中的內(nèi)聯(lián)函數(shù)應(yīng)該是了然于胸了吧。后面會(huì)繼續(xù)Effective Kotlin翻譯系列,歡迎繼續(xù)關(guān)注~~~
Kotlin系列文章,歡迎查看:
Effective Kotlin翻譯系列
- [譯]Effective Kotlin系列之遇到多個(gè)構(gòu)造器參數(shù)要考慮使用構(gòu)建器(二)
- [譯]Effective Kotlin系列之考慮使用靜態(tài)工廠方法替代構(gòu)造器(一)
原創(chuàng)系列:
- Jetbrains開發(fā)者日見聞(三)之Kotlin1.3新特性(inline class篇)
- JetBrains開發(fā)者日見聞(二)之Kotlin1.3的新特性(Contract契約與協(xié)程篇)
- JetBrains開發(fā)者日見聞(一)之Kotlin/Native 嘗鮮篇
- 教你如何攻克Kotlin中泛型型變的難點(diǎn)(實(shí)踐篇)
- 教你如何攻克Kotlin中泛型型變的難點(diǎn)(下篇)
- 教你如何攻克Kotlin中泛型型變的難點(diǎn)(上篇)
- Kotlin的獨(dú)門秘籍Reified實(shí)化類型參數(shù)(下篇)
- 有關(guān)Kotlin屬性代理你需要知道的一切
- 淺談Kotlin中的Sequences源碼解析
- 淺談Kotlin中集合和函數(shù)式API完全解析-上篇
- 淺談Kotlin語法篇之lambda編譯成字節(jié)碼過程完全解析
- 淺談Kotlin語法篇之Lambda表達(dá)式完全解析
- 淺談Kotlin語法篇之?dāng)U展函數(shù)
- 淺談Kotlin語法篇之頂層函數(shù)、中綴調(diào)用、解構(gòu)聲明
- 淺談Kotlin語法篇之如何讓函數(shù)更好地調(diào)用
- 淺談Kotlin語法篇之變量和常量
- 淺談Kotlin語法篇之基礎(chǔ)語法
翻譯系列:
- [譯]Kotlin中內(nèi)聯(lián)類的自動(dòng)裝箱和高性能探索(二)
- [譯]Kotlin中內(nèi)聯(lián)類(inline class)完全解析(一)
- [譯]Kotlin的獨(dú)門秘籍Reified實(shí)化類型參數(shù)(上篇)
- [譯]Kotlin泛型中何時(shí)該用類型形參約束?
- [譯] 一個(gè)簡單方式教你記住Kotlin的形參和實(shí)參
- [譯]Kotlin中是應(yīng)該定義函數(shù)還是定義屬性?
- [譯]如何在你的Kotlin代碼中移除所有的!!(非空斷言)
- [譯]掌握Kotlin中的標(biāo)準(zhǔn)庫函數(shù): run、with、let、also和apply
- [譯]有關(guān)Kotlin類型別名(typealias)你需要知道的一切
- [譯]Kotlin中是應(yīng)該使用序列(Sequences)還是集合(Lists)?
- [譯]Kotlin中的龜(List)兔(Sequence)賽跑
實(shí)戰(zhàn)系列:
- 用Kotlin擼一個(gè)圖片壓縮插件ImageSlimming-導(dǎo)學(xué)篇(一)
- 用Kotlin擼一個(gè)圖片壓縮插件-插件基礎(chǔ)篇(二)
- 用Kotlin擼一個(gè)圖片壓縮插件-實(shí)戰(zhàn)篇(三)
- 淺談Kotlin實(shí)戰(zhàn)篇之自定義View圖片圓角簡單應(yīng)用
歡迎關(guān)注Kotlin開發(fā)者聯(lián)盟,這里有最新Kotlin技術(shù)文章,每周會(huì)不定期翻譯一篇Kotlin國外技術(shù)文章。如果你也喜歡Kotlin,歡迎加入我們~~~
總結(jié)
以上是生活随笔為你收集整理的[译]Effective Kotlin系列之探索高阶函数中inline修饰符(三)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JavaScript四舍五入的改进
- 下一篇: 【漏洞复现】ThinkPHP5 5.x