[譯]探索Kotlin中隱藏的性能開銷-Part 2

翻譯說明:

原標(biāo)題: Exploring Kotlin’s hidden costs?—?Part 2

原文地址: https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-2-324a4a50b70

原文作者: Christophe Beyls

這是關(guān)于探索Kotlin中隱藏的性能開銷的第2部分契吉,如果你還沒有看到第1部分抖拦,不要忘記閱讀第1部分。

讓我們一起從底層重新探索和發(fā)現(xiàn)更多有關(guān)Kotlin語法實現(xiàn)細(xì)節(jié)桨武。

image

局部函數(shù)

這是我們之前第一篇文章中沒有介紹過的一種函數(shù): 就是像正常定義普通函數(shù)的語法一樣,在其他函數(shù)體內(nèi)部聲明該函數(shù)趋观。這些被稱為局部函數(shù)奴紧,它們能訪問到外部函數(shù)的作用域。

fun someMath(a: Int): Int {
    fun sumSquare(b: Int) = (a + b) * (a + b)

    return sumSquare(1) + sumSquare(2)
}

我們首先來說下局部函數(shù)最大的局限性:
局部函數(shù)不能被聲明成內(nèi)聯(lián)的(inline)并且函數(shù)體內(nèi)含有局部函數(shù)的函數(shù)也不能被聲明成內(nèi)聯(lián)的(inline). 在這種情況下沒有任何有效的方法可以幫助你避免函數(shù)調(diào)用的開銷巷查。

經(jīng)過編譯后,這些局部函數(shù)會將被轉(zhuǎn)化成Function對象, 就類似lambda表達(dá)式一樣抹腿,并且同樣具有上篇文章part1中講到的關(guān)于非內(nèi)聯(lián)函數(shù)存在很多的限制岛请。反編譯后的java代碼:

public static final int someMath(final int a) {
   Function1 sumSquare$ = new Function1(1) {
      // $FF: synthetic method
      // $FF: bridge method
      //注: 這是Function1接口生成的泛型合成方法invoke
      public Object invoke(Object var1) {
         return Integer.valueOf(this.invoke(((Number)var1).intValue()));
      }

      //注: 實例的特定方法invoke
      public final int invoke(int b) {
         return (a + b) * (a + b);
      }
   };
   return sumSquare$.invoke(1) + sumSquare$.invoke(2);
}

但是與lambda表達(dá)式相比,它對性能的影響要小得多: 由于該函數(shù)的實例對象是從調(diào)用方就知道的警绩,所以它將直接調(diào)用該實例的特定方法invoke而不是從Function接口直接調(diào)用其泛型合成方法invoke崇败。這就意味著從外部函數(shù)調(diào)用局部函數(shù)時,不會進(jìn)行基本類型的轉(zhuǎn)換或裝箱操作. 我們可以通過看下字節(jié)碼來驗證一下:

   ALOAD 1
   ICONST_1
   INVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke    (I)I
   ALOAD 1
   ICONST_2
   INVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke    (I)I
   IADD //加法操作
   IRETURN

我們可以看到被調(diào)用兩次的函數(shù)是接收一個 Int 類型的參數(shù)并且返回一個 Int 類型的函數(shù)肩祥,并且加法操作是立即執(zhí)行的后室,而無需任何中間的裝箱、拆箱操作混狠。

當(dāng)然咧擂,在每次方法被調(diào)用期間仍會創(chuàng)建一個新的Function對象。但是這個可以通過將局部函數(shù)改寫為非捕獲的方式來避免這種情況:

fun someMath(a: Int): Int {
    fun sumSquare(a: Int, b: Int) = (a + b) * (a + b)

    return sumSquare(a, 1) + sumSquare(a, 2)
}

現(xiàn)在相同的Function實例將會被復(fù)用檀蹋,仍然不會進(jìn)行強(qiáng)制的轉(zhuǎn)換或裝箱操作松申。與普通的私有函數(shù)相比,此局部函數(shù)的唯一劣勢就是使用一些方法生成額外的類俯逾。

局部函數(shù)是私有函數(shù)的替代品贸桶,其附加好處是能夠訪問外部函數(shù)的局部變量。然而這種好處會伴隨著為外部函數(shù)每次調(diào)用創(chuàng)建Function對象的隱性成本桌肴,因此首選使用非捕獲的局部函數(shù)皇筛。

空安全

Kotlin語言的最好特性之一就是,它在可空類型和非空類型之間做出了明顯清晰的界限區(qū)分坠七。這使得編譯可以通過在運行時禁止將非null或可為null的值分配給非null變量的任何代碼來有效防止意外的NullPointerException.

非空參數(shù)的運行時檢查

下面我們來聲明一個使用非null字符串作為采納數(shù)的公有函數(shù):

fun sayHello(who: String) {
    println("Hello $who")
}

現(xiàn)在來看下對應(yīng)的反編譯后Java代碼:

public static final void sayHello(@NotNull String who) {
   Intrinsics.checkParameterIsNotNull(who, "who");//執(zhí)行靜態(tài)函數(shù)進(jìn)行非空檢查
   String var1 = "Hello " + who;
   System.out.println(var1);
}

請注意水醋,Kotlin編譯器對Java是非常友好的旗笔,可以看到在函數(shù)參數(shù)上自動添加了@NotNull注解,因此Java工具可以使用此注解在傳遞空值的時候顯示警告拄踪。

但是蝇恶,注解不足以強(qiáng)制外部調(diào)用者傳入非null的值。因此惶桐,編譯器還在函數(shù)的開頭添加一個靜態(tài)方法調(diào)用撮弧,該方法將檢查參數(shù),如果為null姚糊,則拋出IllegalArgumentException. 為了使不安全的調(diào)用者代碼更易于修復(fù)贿衍,該函數(shù)將盡早且持續(xù)拋出異常,而不是將它置后拋出運行時的NullPointerException.

實際上救恨,每個公有的函數(shù)都有一個對Intrinsics.checkParameterIsNotNull()的靜態(tài)調(diào)用贸辈,該調(diào)用為每個非null引用參數(shù)添加。這些檢查不會被添加到私有函數(shù)中肠槽,因為編譯器保證了Kotlin類中的代碼為null安全的擎淤。

這些靜態(tài)調(diào)用對性能的影響幾乎可以忽略不計,并且在調(diào)試和測試應(yīng)用程序的時候非常有幫助署浩。話雖如此,如果對于release版本來說你可能認(rèn)為這是沒必要的額外開銷扫尺。在這種情況下筋栋,可以使用-Xno-param-assertions編譯器選項或添加以下Proguard規(guī)則來禁止運行時的空檢查:

-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
    static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}

可空的原生類型

有一點似乎眾所周知,但還是在這里提醒下: 可空類型始終是引用類型正驻。將原生類型的變量聲明成可空類型可以防止Kotlin使用Java基本數(shù)據(jù)類型(例如intfloat), 而是使用裝箱的引用類型(例如IntegerFloat),這會避免裝箱和拆想操作帶來的額外開銷弊攘。

與Java相反的是它允許你草率地使用幾乎像int變量的Integer變量,這都要歸功于自動裝箱和忽略了null的安全性姑曙,可是Kotlin則會強(qiáng)制你在使用可null的類型時編寫空安全的代碼襟交,因次使用非null類型的好處就變得更顯而易見了:

fun add(a: Int, b: Int): Int {
    return a + b
}
fun add(a: Int?, b: Int?): Int {
    return (a ?: 0) + (b ?: 0)
}

盡可能使用非null的原生類型,以此來提高代碼可讀性和性能伤靠。

關(guān)于數(shù)組

在Kotlin中存在3種類型的數(shù)組:

  • IntArray,FloatArray以及其他原生類型的數(shù)組捣域。

    最終會編譯成 int[],float[]以及其他對應(yīng)基本數(shù)據(jù)類型的數(shù)組

  • Array<T>: 非空對象引用類型的數(shù)組

    這里會涉及到原生類型的裝箱過程

  • Array<T?>: 可空對象引用類型的數(shù)組

    很明顯,這里也會涉及到原生類型的裝箱過程

如果你需要一個非null原生類型的數(shù)組宴合,最好使用IntArray而不是Array<Int>以避免裝箱過程帶來性能開銷

可變數(shù)量的參數(shù)(Varargs)

類似Java, Kotlin允許使用可變數(shù)量的參數(shù)聲明函數(shù)焕梅。只是聲明的語法有點不一樣而已:

fun printDouble(vararg values: Int) {
    values.forEach { println(it * 2) }
}

就像在Java中一樣,vararg參數(shù)實際上被編譯為給定類型的數(shù)組參數(shù)卦洽。然后贞言,可以通過三種不同的方式調(diào)用這些函數(shù):

1.傳遞多個參數(shù)

printDouble(1, 2, 3)

Kotlin編譯器將將此代碼轉(zhuǎn)換為新數(shù)組的創(chuàng)建和初始化,就像Java編譯器一樣:

printDouble(new int[]{1, 2, 3});

所以阀蒂,創(chuàng)建新數(shù)組會產(chǎn)生開銷该窗,但是與Java相比弟蚀,這并不是什么新鮮事。

2.傳遞單個數(shù)組

這里不同之處就是酗失,在Java中义钉,可以直接將現(xiàn)有的數(shù)組引用作為vararg參數(shù)傳遞。在Kotlin中级零,則需要使用伸展(spread)操作符:

val values = intArrayOf(1, 2, 3)
printDouble(*values)

在Java中断医,數(shù)組引用按原樣傳遞給函數(shù),而無需分配額外的數(shù)組空間奏纪。然而鉴嗤,如你在反編譯后java代碼中所見,Kotlin伸展(spread)操作符的編譯方式有所不同:

int[] values = new int[]{1, 2, 3};
printDouble(Arrays.copyOf(values, values.length));

調(diào)用函數(shù)時序调,始終會復(fù)制現(xiàn)有數(shù)組醉锅。好處是代碼更安全:它允許函數(shù)修改數(shù)組而不影響調(diào)用者代碼。但是它會分配額外的內(nèi)存发绢。

請注意硬耍,使用Kotlin代碼中可變數(shù)量的參數(shù)調(diào)用Java方法具有相同的效果。

3.傳遞數(shù)組和參數(shù)的混合

Kotlin伸展(spread)運算符的主要好處是它還允許在同一調(diào)用中將數(shù)組與其他參數(shù)混合在一起边酒。

val values = intArrayOf(1, 2, 3)
printDouble(0, *values, 42)

上述代碼將會怎樣編譯呢经柴?生成代碼會十分有趣:

int[] values = new int[]{1, 2, 3};
IntSpreadBuilder var10000 = new IntSpreadBuilder(3);
var10000.add(0);
var10000.addSpread(values);
var10000.add(42);
printDouble(var10000.toArray());

除了創(chuàng)建新數(shù)組之外,還使用一個臨時生成器對象來計算最終數(shù)組大小并填充它墩朦。這給方法調(diào)用又增加了另一筆小開銷坯认。

即使在使用現(xiàn)有數(shù)組中的值時,在Kotlin中調(diào)用具有可變數(shù)量參數(shù)的函數(shù)也會增加創(chuàng)建新臨時數(shù)組的成本氓涣。對于重復(fù)調(diào)用該函數(shù)的性能至關(guān)重要的代碼牛哺,請考慮添加具有實際數(shù)組參數(shù)而不是vararg的方法

感謝您的閱讀,如果喜歡劳吠,請分享這篇文章引润。

繼續(xù)閱讀第3部分委托的屬性范圍

讀者有話說

大概隔了很久很久之前痒玩,我好像寫了一篇探索Kotlin中隱藏的性能開銷系列的Part1. 如果沒有讀過第1篇建議也去讀下第1篇淳附,因為這個系列確實對你寫出高效的Kotlin代碼十分有幫助,也能幫助你從源碼蠢古,編譯層面認(rèn)清Kotlin語法背后的原理燃观。我更喜歡把這些寫Kotlin代碼技巧稱為Effective Kotlin, 這也是我最初翻譯這個系列文章的初衷便瑟。關(guān)于這篇文章缆毁,有幾點我需要補(bǔ)充下:

1、為什么非捕獲局部函數(shù)可以減少開銷

其實關(guān)于捕獲和非捕獲的概念到涂,在之前文章中也有所提及脊框,比如在講變量的捕獲颁督,lambda的捕獲和非捕獲。

這里就以上述局部函數(shù)舉例浇雹,下面對比下這兩個函數(shù):

//改寫前的捕獲局部函數(shù)
fun someMath(a: Int): Int {
    fun sumSquare(b: Int) = (a + b) * (a + b)//注意:局部函數(shù)這里的a是直接引用外部函數(shù)的參數(shù)a, 
    //因為局部函數(shù)特性可以訪問外部函數(shù)的作用域沉御,這里實際上就存在了變量的捕獲,所以這里sumSquare稱為捕獲局部函數(shù)

    return sumSquare(1) + sumSquare(2)
}
//改寫前反編譯后代碼
 public static final int someMath(final int a) {
      //創(chuàng)建Function1對象$fun$sumSquare$1昭灵,所以每調(diào)用一次someMath都會創(chuàng)建一個Function1對象
      <undefinedtype> $fun$sumSquare$1 = new Function1() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object var1) {
            return this.invoke(((Number)var1).intValue());
         }

         public final int invoke(int b) {
            return (a + b) * (a + b);
         }
      };
      return $fun$sumSquare$1.invoke(1) + $fun$sumSquare$1.invoke(2);
   }

捕獲局部函數(shù)會生成額外的Function對象吠裆,所以我們?yōu)榱藴p少性能的開銷盡量使用非捕獲局部函數(shù)。

//改寫后的非捕獲局部函數(shù)
fun someMath(a: Int): Int {
    //注意: 可以明顯發(fā)現(xiàn)改寫后a參數(shù)烂完,直接由函數(shù)參數(shù)傳入试疙,而不是在局部函數(shù)直接引用外部函數(shù)的參數(shù)變量,這就是非捕獲局部函數(shù)
    fun sumSquare(a: Int, b: Int) = (a + b) * (a + b)
    return sumSquare(a,1) + sumSquare(a,2)
}

//改寫后反編譯后代碼
public static final int someMath(int a) {
    //注意:可以看到非捕獲的局部函數(shù)實例是一個單例抠蚣,多次調(diào)用都只會復(fù)用之前的實例不會重新創(chuàng)建祝旷。
    <undefinedtype> $fun$sumSquare$1 = null.INSTANCE;
    return $fun$sumSquare$1.invoke(a, 1) $fun$sumSquare$1.invoke(a, 2);
}

通過上述對比,應(yīng)該很清楚知道了什么是捕獲什么是非捕獲以及為什么非捕獲局部函數(shù)會減少性能的開銷嘶窄。

2怀跛、總結(jié)下提高Kotlin代碼性能開銷幾個點

  • 局部函數(shù)是私有函數(shù)的替代品,其附加好處是能夠訪問外部函數(shù)的局部變量柄冲。然而這種好處會伴隨著為外部函數(shù)每次調(diào)用創(chuàng)建Function對象的隱性成本吻谋,因此首選使用非捕獲的局部函數(shù)。
  • 對于release版本應(yīng)用來說现横,特別是Android應(yīng)用漓拾,可以使用-Xno-param-assertions編譯器選項或添加以下Proguard規(guī)則來禁止運行時的空檢查:
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
    static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}
  • 需要使用非null原生類型的數(shù)組時,最好使用IntArray而不是Array<Int>以避免裝箱過程帶來性能開銷

最后

首先想和一直關(guān)注我公眾號和技術(shù)博客的老鐵們說聲抱歉长赞,因為中間已經(jīng)很久沒更新技術(shù)文章晦攒,因此有很多人也離開了闽撤,但也有人一直默默支持得哆。所以從今天起我又準(zhǔn)備開始更新了文章。近期研究dart和flutter也有一段時間了哟旗,沉淀了一些技術(shù)心得贩据,所以會不定期更新有關(guān)一些Dart和Flutter的文章,感謝關(guān)注闸餐,感謝理解饱亮。

<div align="center"><img src="https://user-gold-cdn.xitu.io/2018/5/14/1635c3fb0ba21ec1?w=430&h=430&f=jpeg&s=39536" width="200" height="200"></div>

歡迎關(guān)注Kotlin開發(fā)者聯(lián)盟,這里有最新Kotlin技術(shù)文章舍沙,每周會不定期翻譯一篇Kotlin國外技術(shù)文章近上。如果你也喜歡Kotlin,歡迎加入我們~~~

Kotlin系列文章拂铡,歡迎查看:

Kotlin邂逅設(shè)計模式系列:

數(shù)據(jù)結(jié)構(gòu)與算法系列:

翻譯系列:

原創(chuàng)系列:

Effective Kotlin翻譯系列

實戰(zhàn)系列:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市岖是,隨后出現(xiàn)的幾起案子帮毁,更是在濱河造成了極大的恐慌,老刑警劉巖豺撑,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烈疚,死亡現(xiàn)場離奇詭異,居然都是意外死亡前硫,警方通過查閱死者的電腦和手機(jī)胞得,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屹电,“玉大人阶剑,你說我怎么就攤上這事∥:牛” “怎么了牧愁?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長外莲。 經(jīng)常有香客問我猪半,道長,這世上最難降的妖魔是什么偷线? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任磨确,我火速辦了婚禮,結(jié)果婚禮上声邦,老公的妹妹穿的比我還像新娘乏奥。我一直安慰自己,他們只是感情好亥曹,可當(dāng)我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布邓了。 她就那樣靜靜地躺著,像睡著了一般媳瞪。 火紅的嫁衣襯著肌膚如雪骗炉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天蛇受,我揣著相機(jī)與錄音句葵,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛乍丈,可吹牛的內(nèi)容都是我干的熊响。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼诗赌,長吁一口氣:“原來是場噩夢啊……” “哼汗茄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起铭若,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤洪碳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后叼屠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瞳腌,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年镜雨,在試婚紗的時候發(fā)現(xiàn)自己被綠了嫂侍。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡荚坞,死狀恐怖挑宠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情颓影,我是刑警寧澤各淀,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站诡挂,受9級特大地震影響碎浇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜璃俗,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一奴璃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧城豁,春花似錦苟穆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽剖膳。三九已至魏颓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間吱晒,已是汗流浹背甸饱。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人叹话。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓偷遗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親驼壶。 傳聞我的和親對象是個殘疾皇子氏豌,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,573評論 2 353

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