[譯]Effective Kotlin系列之使用Sequence來優化集合的操做(四)

簡述: 今天迎來了Effective Kotlin系列的第四篇文章: 使用Sequence序列來優化大集合的頻繁操做.關於Sequence這個主題應該你們都不陌生,我寫過幾篇有關它的文章,能夠說得上很詳細了。若是你對它的使用不太熟悉,歡迎查看下面幾篇有關文章:數組

翻譯說明:app

原標題: Effective Kotlin: Use Sequence for bigger collections with more than one processing step函數

原文地址: blog.kotlin-academy.com/effective-k…工具

原文做者: Marcin Moskalapost

開發者常常會忽略了Iterable(迭代器)與Sequence(序列)的區別。這其實也很正常,特別是當你去比較IterableSequence的接口定義時,它們長得幾乎同樣。性能

interface Iterable<out T> {
    operator fun iterator(): Iterator<T>
}
interface Sequence<out T> {
    operator fun iterator(): Iterator<T>
}
複製代碼

對比上面代碼,你只能說出它們之間惟一不同就是接口名不一樣而已。可是IterableSequence卻有着徹底不一樣的用法,所以它們操做集合的函數的工做原理也是徹底不一樣的。測試

序列是基於惰性的工做原理,所以處理序列的中間操做函數是不進行任何計算的。相反,它們會返回上一個中間操做處理後產生的新序列。全部這些一系列中間計算都將在終端操做執行中被肯定,例如常見的終端操做toListcount.在另外一方面,處理Iterable的每一箇中間操做函數都是會返回一個新的集合。優化

fun main(args: Array<String>) {
    val seq = sequenceOf(1,2,3)
    print(seq.filter { it % 2 == 1 }) 
    // Prints: kotlin.sequences.FilteringSequence@XXXXXXXX
    print(seq.filter { it % 2 == 1 }.toList()) // Prints: [1, 3]
    val list = listOf(1,2,3)
    print(list.filter { it % 2 == 1 }) // Prints: [1, 3]
}
複製代碼

序列的filter函數是一箇中間操做,因此它不會作任何的計算,而是通過新的處理步驟對序列進行了加工。最終的計算將在終端操做中完成,如toList函數ui

正由於這樣,處理操做的順序也是不一樣的。在處理序列過程當中,咱們一般會對單個元素進行一系列的總體操做,而後再對下一個元素作進行一系列的總體操做,直處處理完集合中全部元素爲止。在處理iterable過程當中,咱們是每一步操做都是針對整個集合進行,直到全部操做步驟執行完畢爲止。spa

sequenceOf(1,2,3)
        .filter { println("Filter $it, "); it % 2 == 1 }
        .map { println("Map $it, "); it * 2 }
        .toList()
// Prints: Filter 1, Map 1, Filter 2, Filter 3, Map 3,
listOf(1,2,3)
        .filter { println("Filter $it, "); it % 2 == 1 }
        .map { println("Map $it, "); it * 2 }
// Prints: Filter 1, Filter 2, Filter 3, Map 1, Map 3,
複製代碼

因爲這種惰性處理方式以及針對每一個元素進行處理的順序,咱們能夠生成一個不定長的Sequence序列

generateSequence(1) { it + 1 }
        .map { it * 2 }
        .take(10)
        .forEach(::print)
// Prints: 2468101214161820
複製代碼

對於具備必定經驗的Kotlin開發人員來講,這應該不陌生吧,可是在大多數文章或書籍中並無說起到有關序列一個更重要的知識點: 對於處理多個單一處理步驟的集合使用序列更高效

多個處理步驟是什麼意思?個人意思不只僅是多個處理集合的單一函數。因此若是你比較這兩個函數:

fun singleStepListProcessing(): List<Product> {
    return productsList.filter { it.bought }
}

fun singleStepSequenceProcessing(): List<Product> {
    return productsList.asSequence()
            .filter { it.bought }
            .toList()
}
複製代碼

你會注意到它們性能對比幾乎沒有任何差別,或者說處理簡單的集合速度更快(由於它是內聯的)。假如你對比了多個處理步驟的函數,好比先是filter處理而後進行了map處理的twoStepListProcessing函數,那麼差別將是很明顯了。

fun twoStepListProcessing(): List<Double> {
    return productsList
            .filter { it.bought }
            .map { it.price }
}

fun twoStepSequenceProcessing(): List<Double> {
    return productsList.asSequence()
            .filter { it.bought }
            .map { it.price }
            .toList()
}

fun threeStepListProcessing(): Double {
    return productsList
            .filter { it.bought }
            .map { it.price }
            .average()
}

fun threeStepSequenceProcessing(): Double {
    return productsList.asSequence()
            .filter { it.bought }
            .map { it.price }
            .average()
複製代碼

差別到底有多大呢? 讓咱們對比一下基準測量出來的平均操做時間吧:

twoStepListProcessing                                   81 095 ns/op
twoStepSequenceProcessing                               55 685 ns/op
twoStepListProcessingAndAcumulate                       83 307 ns/op
twoStepSequenceProcessingAndAcumulate                    6 928 ns/op
複製代碼

當咱們使用Sequences時, twoStepSequenceProcessing函數明顯比twoStepListProcessing函數處理集合速度要快不少。在這種狀況下,優化的效率約爲30%左右。

當咱們分別使用SequencesIterable處理集合數據來得到某個具體的數值而不是得到一個新集合時,它們之間的效率差別將會變大。由於在這種狀況下,序列根本就不須要建立任何中間集合。

來看一些典型的現實生活的例子,假設咱們須要計算成年人購買該產品的平均價格:

fun productsListProcessing(): Double {
    return clientsList
            .filter { it.adult }
            .flatMap { it.products }
            .filter { it.bought }
            .map { it.price }
            .average()
}
fun productsSequenceProcessing(): Double {
    return clientsList.asSequence()
            .filter { it.adult }
            .flatMap { it.products.asSequence() }
            .filter { it.bought }
            .map { it.price }
            .average()
}
複製代碼

這是對比結果:

SequencesBenchmark.productsListProcessing             712 434 ns/op
SequencesBenchmark.productsSequenceProcessing         572 012 ns/op
複製代碼

咱們大概提升了20%的優化效率,這比直接處理沒有使用flatMap的狀況要低一點,但這已是一個很大的提高了。

當你一次又一次對比測量的性能數據時,你會發現以下這個規律:

當咱們有多個處理步驟時,使用序列處理集合一般比直接處理集合更快。

哪一種場景下序列處理速度不會更快呢?

在有一些不經常使用的操做中使用序列處理速度並不會更快,由於咱們須要完整地操做整個集合。來自Kotlin stdlib標準庫中的sorted就是一個明顯的例子。

sorted使用的最佳實現:它將Sequence中的元素轉換到List中,而後使用Java stdlib標準庫中的sort函數進行排序操做。這個缺點就在於相比相同操做Collection的過程,中間這個轉換過程是須要花費額外的時間的(儘管若是Iterable不是Collection或數組,那麼差別就並不大,由於它轉換過程也是須要花費時間的)

若是Sequence序列有相似sort這樣的函數是有爭議的,由於它只是固定空間長度上的惰性,而且不能用於不定長的序列。之因此引進它是由於它是一種比較受歡迎的函數,而且以這種方式使用它更容易。可是Kotlin開發人員應該要記住,它不能用於不定長的序列

generateSequence(0) { it + 1 }.sorted().take(10).toList()
// 不定長的計算時間
generateSequence(0) { it + 1 }.take(10).sorted().toList()
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
複製代碼

sorted是一個少見處理步驟的例子,它在Collection上使用的效率會比Sequence更高。當咱們使用多個處理步驟和單個排序函數是,咱們能夠期待使用Sequence序列處理的性能將獲得提高。

// Took around 150 482 ns
fun productsSortAndProcessingList(): Double {
    return productsList
            .sortedBy { it.price }
            .filter { it.bought }
            .map { it.price }
            .average()
}

// Took around 96 811 ns
fun productsSortAndProcessingSequence(): Double {
    return productsList.asSequence()
            .sortedBy { it.price }
            .filter { it.bought }
            .map { it.price }
            .average()
}
複製代碼

Java中的Stream(流)怎麼樣呢?

Java8中引入了流來處理集合。它們表現得看起來和Kotlin中的序列很像。

productsList.asSequence()
    .filter { it.bought }
    .map { it.price }
    .average()

productsList.stream()
    .filter { it.bought }
    .mapToDouble { it.price }
    .average()
    .orElse(0.0)
複製代碼

它們也都是基於惰性求值的原理而且在最後(終端)處理集合。Java中的流對於集合的處理效率幾乎和Kotlin中的序列處理集合同樣高。Java中的Stream流和Kotlin中的Sequence序列二者最大的差異以下所示:

  • Kotlin中Sequence序列有更多的操做符函數(由於它們能夠被定義成擴展函數)而且它們的用法也相對更簡單(這是由於Kotlin的序列是已經在使用的Java流基礎上設計的 - 例如咱們可使用toList()而不是collect(Collectors.toList()))
  • Java的Stream流支持可使用parallel函數以並行模式使用Java流. 當咱們擁有一臺具備多個內核的計算機時,這能夠爲咱們帶來巨大的性能提高。
  • Kotlin的Sequence序列可用於通用模塊、Kotlin/JS模塊和Kotlin/Native模塊中。

除此以外,當咱們不使用並行模式時,要說Java stream和 Kotlin sequence哪一個更高效,這個真的很難說。

個人建議是僅僅將Java中的Stream用於計算量較大的處理以及須要啓用並行模式的場景。不然其餘通常場景使用Kotlin Stdlib標準庫中Sequence序列,能夠給你帶來相同效率而且操做函數使用起來也很簡單,代碼更加簡潔。

譯者有話說

老實說這篇文章,好的地方在於原做者把Kotlin中的序列和Java 8中的流作了一個很好的對比,以及做者給出本身的使用建議以及針對性能效率都是經過實際基準測試結果進行對比的。可是惟一美中不足的是對於Kotlin中哪一種場景下使用sequence更好,並無說的很清楚。關於這點我想補充一下:

  • 一、數據集量級是足夠大,建議使用序列(Sequences)
  • 二、對數據集進行頻繁的數據操做,相似於多個操做符鏈式操做,建議使用序列(Sequences)
  • 三、對於使用first{},last{}建議使用序列(Sequences)。補充一下,細心的小夥伴會發現當你對一個集合使用first{},last{}操做符的時候,咱們IDE工具會提示你建議使用序列(Sequences) 代替 集合(Lists)
  • 四、當僅僅只有map操做時,使用sequence

這裏只是簡單總結了幾點,具體詳細內容可參考以前三篇有關Kotlin中序列的文章。好了第四篇完美收工。

Kotlin系列文章,歡迎查看:

Effective Kotlin翻譯系列

原創系列:

翻譯系列:

實戰系列:

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

相關文章
相關標籤/搜索