翻譯說明:
原標(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é)桨武。
局部函數(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ù)類型(例如int或float), 而是使用裝箱的引用類型(例如Integer
或Float
),這會避免裝箱和拆想操作帶來的額外開銷弊攘。
與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)與算法系列:
翻譯系列:
- [譯] [譯]探索Kotlin中隱藏的性能開銷-Part 1
- [譯] Kotlin中關(guān)于Companion Object的那些事
- [譯]記一次Kotlin官方文檔翻譯的PR(內(nèi)聯(lián)類)
- [譯]Kotlin中內(nèi)聯(lián)類的自動裝箱和高性能探索(二)
- [譯]Kotlin中內(nèi)聯(lián)類(inline class)完全解析(一)
- [譯]Kotlin的獨門秘籍Reified實化類型參數(shù)(上篇)
- [譯]Kotlin泛型中何時該用類型形參約束?
- [譯] 一個簡單方式教你記住Kotlin的形參和實參
- [譯]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)賽跑
原創(chuàng)系列:
- 教你如何完全解析Kotlin中的注解
- 教你如何完全解析Kotlin中的類型系統(tǒng)
- 如何讓你的回調(diào)更具Kotlin風(fēng)味
- Jetbrains開發(fā)者日見聞(三)之Kotlin1.3新特性(inline class篇)
- JetBrains開發(fā)者日見聞(二)之Kotlin1.3的新特性(Contract契約與協(xié)程篇)
- JetBrains開發(fā)者日見聞(一)之Kotlin/Native 嘗鮮篇
- 教你如何攻克Kotlin中泛型型變的難點(實踐篇)
- 教你如何攻克Kotlin中泛型型變的難點(下篇)
- 教你如何攻克Kotlin中泛型型變的難點(上篇)
- Kotlin的獨門秘籍Reified實化類型參數(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ǔ)語法
Effective Kotlin翻譯系列
- [譯]Effective Kotlin系列之考慮使用原始類型的數(shù)組優(yōu)化性能(五)
- [譯]Effective Kotlin系列之使用Sequence來優(yōu)化集合的操作(四)
- [譯]Effective Kotlin系列之探索高階函數(shù)中inline修飾符(三)
- [譯]Effective Kotlin系列之遇到多個構(gòu)造器參數(shù)要考慮使用構(gòu)建器(二)
- [譯]Effective Kotlin系列之考慮使用靜態(tài)工廠方法替代構(gòu)造器(一)
實戰(zhàn)系列: