Kotlin最佳實踐:在高階函數中使用inline - 碼農登陸

2020-12-17 碼農登陸

前言

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

尾聲

到此這篇文章就結束了。

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

相關焦點

  • Kotlin 中的 inline ,noinline 和 crossinline
    noinline 需要配合 inline 使用,使用在函數型參上,告訴編譯器當前這個函數不能內聯crossinline 需要配合 inline 使用,告訴編譯器不能使用 return,否則就跳出調用處函數,但是可以使用 return@label 跳出指定外層。
  • Kotlin 高階函數、內聯
    然後再次編譯下看看:如果你只是想內聯函數的某些 lamda  表達式被內聯,其他的不允許內聯操作的話可以使用 noinline 。那問題又來了,直接改成 noinline 的話,那不是又變成搞了一堆對象出來了?還有沒其他方法?Kotlin 還提供了一個 crossline 關鍵字。把上面的 noinline 的例子改成 crossline ,來對比下編譯後的文件從上面可以看得出,crossline  不會搞出一堆對象。
  • Kotlin入門教程,快使用Kotlin吧
    有通配符和邊界的概念比如Class,表示上界通配符,它代表T以及T的子類,上限是T;在kotlin中可以使用out來替代例如clazz: Class<out T>in:同樣也有下屆通配符比如,它表示T以及T的超類,下限是T;在kotlin中可以使用in來代替例如clazz: Class<in T>靜態類和靜態方法 object(全局):使用object
  • Kotlin函數式編程
    在Kotlin中函數式的地位和對象一樣高,你可以在方法中輸入函數,也可以返回函數。函數式編程FP特徵:函數式編程核心概念:函數是「一等公民」:是指函數與其他數據類型是一樣的,處於平等地位。函數可以作為其他函數的參數傳入,也可以作為其他函數的返回值返回。
  • Kotlin 怎麼學 ?遇到過哪些坑?
    = null){        ((ViewGroup)view.getParent()).removeView(view);    }}但是在 kotlin 中,使用擴展函數,可以更巧妙更直接的實現這個功能。
  • 新手入門:關於C++中的內聯函數(inline)
    正在閱讀:新手入門:關於C++中的內聯函數(inline)新手入門:關於C++中的內聯函數(inline)2005-03-01 10:10出處:PConline作者:管寧>   在c++中,為了解決一些頻繁調用的小函數大量消耗棧空間或者是叫棧內存的問題,特別的引入了inline修飾符,表示為內聯函數。
  • 問:談談 Kotlin 範型與逆變協變?
    extends E> items);}void addAll(Collection<Object> to, Collection<String> from) {    //通配符保證了編譯通過,也可以安全的從 to 中讀取元素,然後當作 Object 類型來使用。
  • C語言陷阱與技巧第2節,使用inline函數可以提升程序效率,但是讓...
    inline 關鍵字的作用在C語言程序開發中,inline 一般用於定義函數,inline 函數也被稱作「內聯函數」,C99 和 GNU C 均支持內聯函數。那麼在C語言中,內聯函數和普通函數有什麼不同呢?
  • 幾個特性,快速上手Kotlin
    不不不,往下看,kotlin中的屬性大有文章。1.1、可觀察屬性Observable這個語言特性,是非常非常用意思,且實用的。不信?更多有趣的符號用法,可以參考官網:https://www.kotlincn.net/docs/reference/keyword-reference.html1.2、關於return既然我們從if中,了解了if中的隱式return,那這裡可能會有一個疑問,
  • Kotlin使用指南之類篇特殊的類
    componentN()中的N是指參數的個數,如果主構造函數有兩個參數,那麼系統會為我們生成兩個component1(),component2()方法,這兩個方法是用來對數據類進行解構。使用方法如下。- 密封類不允許有非-private 構造函數(其構造函數默認為 private)。- 密封類子類的子類可以放在任何位置,而無需在同一個文件中。
  • 一些 Kotlin 小技巧及解析
    結合著 Kotlin 的高級函數的特性可以讓代碼可讀性更強,更加簡潔,但是呢簡潔的背後是有代價的,使用不當對性能可能會有損耗,這塊往往很容易被我們忽略,這就需要我們去研究 kotlin 語法糖背後的魔法,當我們在開發的時候,選擇合適的語法糖,儘量避免這些錯誤,關於 Kotlin 性能損失那些事,可以看一下我另外兩篇文章。
  • Python基礎教程——高階函數
    Python的高階函數,就是map、filter、reduce,說它們是高階函數,只是因為我們平時用的少,所以理解起來也有點費勁,事實上,它們功能很強大,也很好用易用。一起來看看吧。篩選函數例1:對一個數字列表,篩選出其中的偶數,常規的方法是使用for循環,再在for循環中判斷它,符合條件的保存到新列表中,最後再使用新列表,真的是太煩瑣了,我們使用filter函數來個簡單的:
  • vlookup函數的高階學習,對參數的處理技巧
    我們在實際工作中,我們經常使用excel表格處理和分析數據,我們為了提高工作效率,我們通常會在平時積累各種各樣的實用小技巧,我們這次還是分享有關查找函數的使用技巧。我們之前學習過vlookup函數的基礎操作技能,這次我們學習一下vlookup函數的高階技能,有關對vlookup函數參數的處理小技巧,下面我們就一起學習一下。
  • 為數不多的人知道的 Kotlin 技巧及解析(一)
    文章中沒有奇淫技巧,都是一些在實際開發中常用,但很容易被我們忽略的一些常見問題,源於平時的總結,這篇文章主要對這些常見問題進行分析。這篇文章主要分析一些常見問題的解決方案,如果使用不當會對 性能 和 內存 造成的那些影響以及如何規避這些問題,文章中涉及的案例來自 Kotlin 官方、Stackoverflow、Medium 等等網站。
  • Android 開發者自述:為什麼我要改用 Kotlin?
    類的繼承和實現很簡單,使用:即可Kotlin每個句子都不需要加分號(;)空指針安全空指針(NullPointerException或NPE)是我們使用Java開發程序中最常見的崩潰了。因為在Java中我們不得不寫很多防禦性的代碼,比如這樣:在Kotlin中空指針異常得到了很好的解決。
  • Kotlin學習筆記——基礎篇
    >主要涉及的知識點:(1)基礎語法包 package函數 fun變量 var, val字符串模塊 $a, ${a.b}條件表達式 if else, when 二元條件優先使用if可空值及null檢測 Int?
  • C++中inline, extern, static潛在的陷阱
    相信inline, extern, static這三個關鍵字對於C++程式設計師是非常熟悉的,但有些時候,其中隱藏的陷阱,可能會給你的程序帶來一些很難診斷的問題。1. inline我們先聚焦於inline函數(內聯函數)。inline可以與名稱空間一起使用,但這種用法並不常見。
  • 使用 Kotlin 進行 Android 測試
    使用 Kotlin 進行 Android 測試事實上,這也不是一個新話題,因為 Kotlin 在程式語言世界裡攻城略地,尤其在 Android 中。Kotlin 是函數式。 Functions and properties 是第一公民。Kotlin 是友好的。Kotlin 和 Java 幾乎可以完美共存。為什麼要在測試中使用 Kotlin?我們有一個使用 Java 寫的 Android 代碼庫,我們想逐步引入這門帥氣的語言,所以為啥不從測試開始呢?
  • 程式語言 Kotlin 1.4 將推新的編譯器:今年春季發布
    此編譯器的這一部分也可以在 IDE 中使用,來高亮顯示語法錯誤、導航到定義並搜索項目中的符號用法。這是 kotlinc 如今花費最多時間的步驟,因此開發團隊希望使其更快。當前的實現尚未完成,並且不會在 1.4 中到來。但是,大多耗時的工作都是由它完成,因此我們可以預期提速的效果。
  • 使用Kotlin來開發Android,愛上它的優雅
    之前聽說了這個傳奇的語言,但是並沒有認真的去嘗試使用他用到開發中,現在已經成為官方語言了,學習是必須的了!首先看看他的一些特性吧,之前看了一些Swift語言的特點,就先拿Kotlin和Swift和Java的對比來看看他的優勢吧。