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

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

翻譯說明:html

原標題# Exploring Kotlin’s hidden costs — Part 3java

原文地址: medium.com/@BladeCoder…算法

原文做者: Christophe Beyls編程

代理屬性和Range

在發佈有關Kotlin編程語言的性能開銷系列的前兩篇文章以後,我收到了不少不錯的反饋,甚至還包括 Jake Wharton 大神他本身。因此你還沒看前兩篇文章,千萬不要錯過哦。設計模式

在第3部分中,咱們將揭開更多有關Kotlin編譯器的祕密,並提供如何編寫更高效代碼的新技巧。api

1、代理屬性

代理屬性是一種其getter和可選的setter的內部實現可由代理的外部對象提供的屬性。它能夠容許複用自定義屬性的內部實現。數組

class Example {
    var p: String by Delegate()
}
複製代碼

這個代理對象必須實現一個 operator getVlue()函數,以及一個 setValue()函數來用於屬性的讀/寫. 這些函數將接收包含對象實例 以及屬性的metadata元數據 做爲額外參數(好比它的屬性名)。緩存

當類中聲明一個代理屬性時,編譯將生成如下代碼(下面是反編譯後的Java代碼):安全

public final class Example {
   @NotNull
   private final Delegate p$delegate = new Delegate();
   // $FF: synthetic field
   static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Example.class), "p", "getP()Ljava/lang/String;"))};

   @NotNull
   public final String getP() {
      return this.p$delegate.getValue(this, $$delegatedProperties[0]);
   }

   public final void setP(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.p$delegate.setValue(this, $$delegatedProperties[0], var1);
   }
}
複製代碼

一些靜態屬性metadata元數據被添加到類中。代理將在類的構造器中進行初始化,而後在每次讀取或寫入屬性時都調用該代理。數據結構

代理實例

在上述例子中,將會建立一個新的代理對象的實例來實現該屬性。當代理實例是有狀態的時候, 這就是必需的,例如在計算本地緩存屬性的值時.

class StringDelegate {
    private var cache: String? = null

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        var result = cache
        if (result == null) {
            result = someOperation()
            cache = result
        }
        return result
    }
}
複製代碼

若是還須要經過其構造函數傳遞的額外參數,則還須要建立一個新的代理實例:

class Example {
    private val nameView by BindViewDelegate<TextView>(R.id.name)
}
複製代碼

可是在某些狀況下,只須要一個代理實例就能夠實現任意屬性: 當代理實例是無狀態的時候,而且它執行所需的惟一變量就是對象實例和屬性名稱(然而這些編譯器都直接提供了)。在這種狀況下,能夠經過將代理實例聲明成object對象表達式而不是一個來使得成爲單例

例如,下面的代理單例實例檢索其標記名稱與Android Activity 中的屬性名稱來匹配Fragment.

object FragmentDelegate {
    operator fun getValue(thisRef: Activity, property: KProperty<*>): Fragment? {
        return thisRef.fragmentManager.findFragmentByTag(property.name)
    }
}
複製代碼

一樣,任意的對象均可以擴展成代理。此外getValue()setValue()還能夠聲明成擴展函數。Kotlin中已經提供了內置的擴展函數,例如容許將MapMutableMap實例做爲代理實例,並將屬性的名稱做爲key.

若是你選擇在同一個類中實現多個屬性複用同一個局部代理實例的話,那麼須要在類的構造器中初始化此實例。

注意: 從Kotlin1.1開始,也能夠在函數中聲明局部變量做爲代理屬性。那麼在這種狀況下,代理實例能夠延遲初始化,直到在函數中聲明變量爲止。

在類中聲明的每一個代理屬性都涉及到其關聯的代理對象建立的性能開銷,並向該類中添加一些metadata元數據。必要的時候,能夠嘗試爲不一樣屬性複用同一個代理實例。在你聲明大量代理屬性的時候,還須要考慮代理屬性是否你的最佳選擇。

泛型代理

還能夠以泛型的方式聲明代理函數,所以同一個代理類能夠用任意的屬性類型。

private var maxDelay: Long by SharedPreferencesDelegate<Long>()
複製代碼

可是,若是像上面例子那樣使用具備原生類型屬性的泛型代理的話,即使聲明的原生類型爲非null,每次讀取或寫入該屬性時都避免不了裝箱和拆箱的發生

對於非null原生類型的代理屬性,最好使用爲該特定值類型建立特定的代理類,而不是泛型代理,以免在每次訪問該屬性時產生的裝箱開銷

標準庫代理: lazy()

Kotlin內置了一些標準庫代理函數來覆蓋常見的狀況,例如 Delegates.notNull(),Delegates.observable()lazy().

lazy(initializer: () -> T) 是一個爲只讀屬性返回代理對象的函數,該屬性是經過在其首次被讀取的時,lazy函數參數lambda initializer執行來初始化的。

private val dateFormat: DateFormat by lazy {
    SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
}
複製代碼

這是一種將昂貴的初始化操做延遲到實際須要使用以前的巧妙方法,能夠在保持代碼可讀性的同時又提升了性能。

須要注意到的是,lazy()函數不是內聯函數,而且做爲參數傳遞的lambda將編譯成獨立的Function類,而且不會在返回的代理對象內進行內聯。

一般會被人忽略的是lazy()另外一重載函數實際上還隱藏一個可選的模式參數來肯定應該返回3種不一樣類型的代理中的一種:

public fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
        when (mode) {
            LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
            LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
            LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
        }
複製代碼

默認的模式是 LazyThreadSafetyMode.SYNCHRONIZED 將執行相對開銷昂貴的雙重鎖的檢查,這是爲了保證在多線程環境下讀取屬性時,初始化塊能夠安全運行。

若是你明確知道當前環境是單線程(例如主線程)訪問屬性,那麼能夠經過顯式使用 LazyThreadSafetyMode.NONE 來徹底避免雙重鎖的檢查所帶來昂貴的開銷。

val dateFormat: DateFormat by lazy(LazyThreadSafetyMode.NONE) {
    SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
}
複製代碼

使用lazy()代理能夠按需延遲昂貴的初始化,此外能夠指定線程安全的模式以免沒必要要的雙重鎖檢查。

2、Ranges(區間)

區間是一種用於表示Kotlin中的一組有限值的特殊表達式。這些值能夠是任意Comparable類型。這些表達式由建立用於實現ClosedRange對象的函數造成。用於建立區間的主要函數是 ..操做符。

區間包含的測試

區間表達式主要目的是使用in!in 運算符來判斷是否包含某個值

if (i in 1..10) {
    println(i)
}
複製代碼

該實現特意針對非null原生類型區間(有: Int, Long, Byte, Short, Float, Double或Char)進行了優化,所以上面例子能夠高效編譯成以下形式:

if(1 <= i && i <= 10) {
   System.out.println(i);
}
複製代碼

性能開銷幾乎爲0,沒有額外的對象分配。區間也能夠和任意其餘非原生Comparable類型一塊兒使用。

if (name in "Alfred".."Alicia") {
    println(name)
}
複製代碼

在Kotlin 1.1.50以前,編譯以上示例時始終會建立一個臨時的ClosedRange對象。可是從1.1.50以後,已經對它的實現進行了優化,以免Comparable類型額外開銷分配:

if(name.compareTo("Alfred") >= 0) {
   if(name.compareTo("Alicia") <= 0) {
      System.out.println(name);
   }
}
複製代碼

此外,區間檢查還包括應用再 when 表達式中

val message = when (statusCode) {
    in 200..299 -> "OK"
    in 300..399 -> "Find it somewhere else"
    else -> "Oops"
}
複製代碼

這使代碼比一系列if {...} else if {...}語句更具可讀性,而且效率更高。

可是,在區間包含檢查中,當區間的聲明之間至少存在一個間接過程時,會有一個小的性能開銷。 好比下面這段Kotlin代碼:

private val myRange get() = 1..10

fun rangeTest(i: Int) {
    if (i in myRange) {
        println(i)
    }
}
複製代碼

上述代碼會形成在編譯後額外建立一個IntRange對象:

private final IntRange getMyRange() {
   return new IntRange(1, 10);
}

public final void rangeTest(int i) {
   if(this.getMyRange().contains(i)) {
      System.out.println(i);
   }
}
複製代碼

即便將屬性getter聲明成內聯函數也不能避免建立IntRange對象。在這種狀況下,Kotlin 1.1編譯器已經改進了。 因爲這些特定的區間類存在,至少在比較原生類型時不會出現裝箱過程。

嘗試在沒有間接聲明過程區間檢查中使用直接聲明區間的方式,來避免額外區間對象的建立分配,另外,能夠將它們聲明成常量以此來複用他們。

迭代: for循環

整數類型區間(除Float或Double以外的任何原生類型的區間)也是級數: 能夠對其進行迭代。這容許用較短的語法替換經典的Java for循環。

for (i in 1..10) {
    println(i)
}
複製代碼

這能夠以零開銷方式編譯爲可比較的優化代碼:

int i = 1;
for(byte var2 = 11; i < var2; ++i) {
   System.out.println(i);
}
複製代碼

若是向後迭代,請使用 downTo() 中綴函數來替代

for (i in 10 downTo 1) {
    println(i)
}
複製代碼

一樣,使用此構造進行編譯後的開銷爲零:

int i = 10;
byte var1 = 1;
while(true) {
   System.out.println(i);
   if(i == var1) {
      return;
   }
   --i;
}
複製代碼

還有一個有用的until()中綴函數能夠迭代直到但不包括區間上限值。

for (i in 0 until size) {
    println(i)
}
複製代碼

當本文的原始版本發佈時,調用此函數用於生成次優代碼。自Kotlin 1.1.4起,狀況已大大改善,而且編譯器如今生成等效的Java for循環:

int i = 0;
for(int var2 = size; i < var2; ++i) {
   System.out.println(i);
}
複製代碼

可是,其餘迭代變體的優化效果也不佳

這是另外一種使用reversed() 函數與區間組合的方法,能夠向後迭代併產生與downTo()徹底相同的結果。

for (i in (1..10).reversed()) {
    println(i)
}
複製代碼

不幸的是,生成的編譯代碼就不那麼漂亮:

IntProgression var10000 = RangesKt.reversed((IntProgression)(new IntRange(1, 10)));
int i = var10000.getFirst();
int var3 = var10000.getLast();
int var4 = var10000.getStep();
if(var4 > 0) {
   if(i > var3) {
      return;
   }
} else if(i < var3) {
   return;
}

while(true) {
   System.out.println(i);
   if(i == var3) {
      return;
   }

   i += var4;
}
複製代碼

將會建立一個臨時的IntRange對象來表示區間,而後再建立另外一個IntProgression對象來反轉第一個對象的值。

事實上,建立一個progression的以上功能任何組合都會生成相似的代碼,涉及到建立至少兩個輕量級progression對象的小開銷。

此規則也適用於使用step()中綴函數來修改progression, 即便步長是1:

for (i in 1..10 step 2) {
    println(i)
}
複製代碼

附帶說明下,當生成的代碼讀取IntProgression的最後一個屬性時,這將執行少許計算,以經過考慮邊界和步長來肯定區間的確切最後一個值。在上面的示例中,最後一個值應該爲9。

若要在for循環中進行迭代,最好使用區間表達式,該區間表達式只涉及到對 ..downTo()untill()的單個函數調用,以免建立臨時progression對象的開銷。

迭代: for-each()

與其使用for循環,不如嘗試在區間上使用forEach()內聯擴展函數來達到相同的結果。

(1..10).forEach {
    println(it)
}
複製代碼

可是,若是您仔細查看此處使用的forEach()函數的簽名,你會注意到,它並無針對區間進行優化,而只是針對Iterable進行了優化,所以須要建立一個迭代器。這是反編譯後的Java代碼表示形式:

Iterable $receiver$iv = (Iterable)(new IntRange(1, 10));
Iterator var1 = $receiver$iv.iterator();

while(var1.hasNext()) {
   int element$iv = ((IntIterator)var1).nextInt();
   System.out.println(element$iv);
}
複製代碼

該代碼甚至比之前的示例效率更低,由於除了建立IntRange對象外, 你還必須還有建立一個IntIterator的開銷。至少,這個會生成原生類型的值。

要對範圍進行迭代,最好使用簡單的for循環,而不是在其上調用forEach()函數,以免迭代器對象的開銷。

迭代: collection indices

Kotlin標準庫提供了內置索引擴展屬性,以生成數組索引和Collection索引的區間。

val list = listOf("A", "B", "C")
for (i in list.indices) {
    println(list[i])
}
複製代碼

使人驚訝的是,遍歷 indices 的代碼也被編譯爲優化的代碼

List list = CollectionsKt.listOf(new String[]{"A", "B", "C"});
int i = 0;
for(int var2 = ((Collection)list).size(); i < var2; ++i) {
   Object var3 = list.get(i);
   System.out.println(var3);
}
複製代碼

在這裏,咱們能夠看到根本沒有建立IntRange對象,而且列表迭代儘量高效。

這對於實現Collection的數組和類很是有效, 所以你可能會在本身定義類中定義本身的indices擴展,同時指望能達到相同的迭代性能.

inline val SparseArray<*>.indices: IntRange
    get() = 0 until size()

fun printValues(map: SparseArray<String>) {
    for (i in map.indices) {
        println(map.valueAt(i))
    }
}
複製代碼

可是,在編譯以後,咱們能夠看到效率不高,由於編譯器沒法智能地避免建立區間對象:

public static final void printValues(@NotNull SparseArray map) {
   Intrinsics.checkParameterIsNotNull(map, "map");
   IntRange var10000 = RangesKt.until(0, map.size());
   int i = var10000.getFirst();
   int var2 = var10000.getLast();
   if(i <= var2) {
      while(true) {
         Object $receiver$iv = map.valueAt(i);
         System.out.println($receiver$iv);
         if(i == var2) {
            break;
         }
         ++i;
      }
   }
}
複製代碼

相反,我建議直接在for循環中使用until()函數

fun printValues(map: SparseArray<String>) {
    for (i in 0 until map.size()) {
        println(map.valueAt(i))
    }
}
複製代碼

當遍歷未實現Collection接口的自定義集合時,最好直接在for循環中編寫本身的索引範圍,而不是依靠函數或屬性來生成區間,以免分配區間對象。

我但願這些對你的閱讀和對個人寫做同樣有趣。你可能會在之後看到更多相關內容,可是前三部分涵蓋了我計劃最初編寫的全部內容。若是你喜歡,請分享給他人,謝謝!

總結

到這裏,有關探索Kotlin性能開銷的系列文章終於暫時告於完結,說下本身切身感覺,翻譯這個系列對我平時在用Kotlin開發時有了很大的幫助,能夠寫出更加高效優秀的代碼。因此我以爲有必要把它翻譯出來和你們共享。下一站,咱們將進入Kotlin協程~~~

歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~

Kotlin系列文章,歡迎查看:

Kotlin邂逅設計模式系列:

數據結構與算法系列:

翻譯系列:

原創系列:

Effective Kotlin翻譯系列

實戰系列:

相關文章
相關標籤/搜索