Kotlin 效能優化利器 —— Sqeuence 原理淺析

0xCAFEBOY發表於2019-05-25

前言

本文將介紹 Kotlin 中 序列(Sequence)的概念及使用,並介紹該惰性集合操作對集合鏈式呼叫效能優化背後的原理。

閱讀本文大概需要 5 分鐘,寫作本文大概消耗 7 個小時

目錄

目錄

序列(Sequence)

概念

在使用 Kotlin 集合操作符進行鏈式呼叫時,例如 mapfilter 時,都會在函式內部建立中間集合,比如下面的例子,使用 map 和 filter 在 User 集合中篩選出性別為男的成員,返回結果是一個集合。

users.map(User :: sex)
     .filter {it.sex.equals("male")}
複製程式碼

序列的用法

序列的用法很簡單,只需要再集合後新增asSeqence() 函式即可

users.asSequence()
     .map(User :: sex)
     .filter {it.sex.equals("male")}
複製程式碼

這裡插播一個概念,其中 User :: user 是成員引用,具體介紹如下

成員引用(Member References)

概念

成員引用可以使你方便的呼叫某個類的成員,這個成員包括對應類的屬性或方法.雙冒號前的是被引用的類,雙冒號後是需要返回的屬性或方法名,如下所示是返回 User 成員的 sex 屬性:

User :: sex
複製程式碼

成員引用可以方便的賦值給其他變數或函式,例如上述尋找性別為 male 的例子,也可以用稍微複雜的寫法,如下:

users.map(user : User -> user.sex)
     .filter {it.sex.equals("male")}
複製程式碼

可見成員引用的寫法可讀性更強。

再談序列

讓我們回到序列介紹。上文提到使用 mapfilter 時,都會在函式內部建立中間集合,這會導致一個問題,如果源列表,就是 users 中元素特別多,集合的鏈式處理會變得十分低效,原因是建立了多次中間集合。而如果先將待處理集合通過 asSequence() 方法轉換為序列,再進行 mapfilter 操作,就會變得十分高效。對於是否使用序列進行集合操作,有幾個前提,如果使用不當,反而會造成效能損失。這裡總結一下使用場景:

使用場景對比介紹

序列效能測試

上文提到,是否使用序列的條件之一是處理大量資料,那麼這個閾值究竟是多少?下面來進行一個效能測試,構造一個商品列表,其中每個商品包含商品名和價格兩個屬性,現在要求出對每個商品的價格加價 100 後,價格為奇數 的商品的個數,這裡用到了 count() 方法,作用是求得集合內滿足 count 條件的元素的個數,程式碼如下:

/**
 * 商品類
 */
data class Commodity(var name: String, var price: String)
複製程式碼
import java.util.*

fun main(args: Array<String>) {

    val commodityList = ArrayList<Commodity>()

    for (i in 0..1000000) {
        val goods = Commodity("商品 $i", i * 5)
        commodityList.add(goods)
    }

    val startTime = System.currentTimeMillis()

    commodityList
            .asSequence() // 使用此函式代表使用 Kotlin 序列功能
            .map { it.price + 100 }
            .count { it % 2 != 0 }

    println("consume time is ${System.currentTimeMillis() - startTime} ms")

}
複製程式碼

測試結果折線圖如下,其中橫座標為集合內元素的個數,縱座標為程式碼執行時間,橙色線代表未使用序列,藍色線代表使用序列:

消耗時間折線對比圖
表格對比如下:

表格對比

由圖可得出如下結論:

  • 上文提到的閾值大致為「一百萬」個元素,大於該閾值時,使用序列大致能帶來 90 % 的效能提升
  • 在小於「十萬」個元素時,使用序列反而會造成效能下降

為什麼序列會提高集合操作的效能?

  1. 序列對集合的操作是惰性的。
  2. 不需要額外的建立中間集合儲存鏈式操作的中間結果

對於第一點,惰性這個詞可能給人帶來迷惑,這和使用序列後,集合的計算方式有關,下面來介紹這一點,在下面這個例子中,你需要在一個整型資料集合,將每個數乘以 2 ,並找出集合中小於 10 的第一個元素。

var result = listOf(2,4,6,8,10).asSequence
                  .map(it * 2)
                  .find(it > 10)
複製程式碼

find() 方法的作用是在集合中查詢滿足條件的第一個元素,並返回查詢到的值。下圖是 Kotlin 使用序列進行處理

序列使用對比圖

由圖可知,所謂惰性,就是在使用 mapfilter 等操作符的時候,在程式碼的執行順序上,不會優先遍歷所有的集合,即偷個懶,先對集合中第一個元素進行 map 和 filter, 然後對該元素執行 find 操作,發現滿足 find 的條件,就立刻返回結果,由此可見,在有 map 和 find 或 last 等操作符組合時,序列的效能優化能發生最大的作用。

小結

  1. 在進行集合操作時,使用序列操作符,可以降低集合操作的時間和佔用的空間,降低時間的原因是惰性操作,降低空間佔用的原因是序列在執行操作時不會建立中間集合。
  2. 序列操作雖好,但也要視業務場景決定是否使用,否則反而會降低效率。

參考文獻

您的點贊或評論是對我寫作最大的鼓勵 !

相關文章