[譯]Kotlin中的龜(List)兔(Sequence)賽跑

極客熊貓發表於2018-06-19

翻譯說明:

原標題: Kotlin : Slow List and Lazy Sequence

原文地址: medium.com/@elye.proje…

原文作者: Elye

自從Kotlin可以相容Java7上後,很高興的是我們可以輕鬆地在List上使用各種集合操作符並且可以鏈式呼叫它們。但是我們知道在某些情況下使用List的迭代器並不是最好的方式,那麼還有另一種方式就是使用序列(sequence)

沒有背景只能辛苦工作的List列表

在我們瞭解序列在某些情況下為什麼更好之前,讓我告訴你一些關於List的內容。

List內部使用Iterator進行操作。這是一個非常勤奮的群體,我鏈式呼叫它的每一個操作,它都能確保沒有任何遺漏的完成。

val list = listOf(1, 2, 3, 4, 5, 6)
list.map{ it * 2 }.filter { it % 3  == 0 }.average()
複製程式碼

[譯]Kotlin中的龜(List)兔(Sequence)賽跑

正如你在上面的插圖中看到的,對於每一步操作,List的每個元素都需要被處理。

為了證明這一點,讓我們輸出一些log日誌:

val list = listOf(1, 2, 3, 4, 5, 6)
val result = list
        .map{ println("In Map"); it * 2 }
        .filter { println("In Filter");it % 3  == 0 }
println("Before Average")
println(result.average())
複製程式碼

結果如下:

In Map
In Map
In Map
In Map
In Map
In Map
In Filter
In Filter
In Filter
In Filter
In Filter
In Filter
Before Average
9.0
複製程式碼

很棒。勤奮努力地工作,並完成所有的過程。

懶惰的傢伙,Sequence序列...

好的,現在讓我們通過呼叫asSequence()擴充套件函式來將List轉化成一個序列(Sequence)。

val list = listOf(1, 2, 3, 4, 5, 6)
val result = list.asSequence()
        .map{ println("In Map"); it * 2 }
        .filter { println("In Filter");it % 3  == 0 }
println("Before Average")
println(result.average())
複製程式碼

結果如下:

Before Average
In Map
In Filter
In Map
In Filter
In Map
In Filter
In Map
In Filter
In Map
In Filter
In Map
In Filter
9.0
複製程式碼

哇,有趣...,注意到 "Before Average" 是最先輸出的,換句話說,如果我不呼叫 average() 函式,那麼序列(sequence)就沒有做任何操作。

它很懶,不想做任何工作,直到終端連線到它。終端就像是一種操作,實際上就是一個操作符擴充套件函式,會返回其他型別結果(除了Sequence<T>之外),例如 sum(),average(),first()等...。甚至toList()用於將Sequence轉換為List

除此之外,你會注意到它輸出的In MapIn Filter交叉出現。這意味著它會在通過鏈條之前一個接一個地通過鏈條,直到它通過終端,即平均操作,然後通過下一個元素。

[譯]Kotlin中的龜(List)兔(Sequence)賽跑

那麼,序列Sequence到底有什麼好處呢?

如果你這樣想,想象你想要拿到集合變換後的第一個元素。

讓我們看下List處理方式:

val list = listOf(1, 2, 3, 4, 5, 6)
val result = list
        .map{ println("In Map $it"); it * 2 }
        .filter { println("In Filter $it");it % 3  == 0 }
println(result.first())
複製程式碼

結果如下:

In Map 1
In Map 2
In Map 3
In Map 4
In Map 5
In Map 6
In Filter 2
In Filter 4
In Filter 6
In Filter 8
In Filter 10
In Filter 12
6
複製程式碼

所有在一起總共13行,這意味著13次操作。

讓我們看下Sequence處理方式:

val sequence = sequenceOf(1, 2, 3, 4, 5, 6)
val result = sequence
        .map{ println("In Map $it"); it * 2 }
        .filter { println("In Filter $it");it % 3  == 0 }
println(result.first())
複製程式碼

結果是:

In Filter 2
In Map 2
In Filter 4
In Map 3
In Filter 6
6
複製程式碼

僅僅7行即7次操作。這意味著它只要找到第一個元素的那一刻,就會終止整個過程。

你可以想像,這會加快整個執行的過程。

加速僅僅只適用於first()操作嗎?

讓我們做一些試驗。

試驗Map操作

val sequence = generateSequence(1) { it + 1 }.take(50000000)
val list = sequence.toList()

println("List Map Sum= " 
        + measureNanoTime { list.map { it * 2 }.sum() })
println("Sequence Map Sum " 
        + measureNanoTime { sequence.map { it * 2 }.sum() })

println("List Map Average " 
        + measureNanoTime { list.map { it * 2 }.average() })
println("Sequence Map Average " 
        + measureNanoTime { sequence.map { it * 2 }.average() })
複製程式碼

結果是:

List Map Sum 14727907362
Sequence Map Sum 2074397969
List Map Average 11460520785
Sequence Map Average 3268960487
複製程式碼
  • List: 在Map:Sum操作上花費了14.7s,在Map:Average操作上花費了11.5s
  • Sequence: 在Map:Sum操作上花費了2.1s, 在Map:Average操作上花費了3.3s

[譯]Kotlin中的龜(List)兔(Sequence)賽跑

看上去像前面的有一個Map操作時,Sequence的效能會比List更快。也許它不需要像List那樣儲存map操作後的中間結果,從而會更快。

試驗Filter操作

val sequence = generateSequence(1) { it + 1 }.take(50000000)
val list = sequence.toList()

println("List Filter Sum " 
        + measureNanoTime { list.filter { it % 3 == 0 }.sum() })
println("Sequence Filter Sum " 
        + measureNanoTime { sequence.filter { it % 3 == 0 }.sum() })

println("List Filter Average " 
        + measureNanoTime { list.filter { it % 3 == 0 }.average() })
println("Sequence Filter Average " 
        + measureNanoTime { sequence.filter { it % 3 == 0 }.average() })
複製程式碼

結果是:

List Filter Sum 506351694
Sequence Filter Sum 873175271
List Filter Average 391790033
Sequence Filter Average 838510968
複製程式碼
  • List: 在Filter:Sum操作上花費了0.5s,在Filter:Average操作上花費了0.4s
  • Sequence: 在Filter:Sum操作上花費了0.9s, 在Filter:Average操作上花費了0.8s

[譯]Kotlin中的龜(List)兔(Sequence)賽跑

對於前面的Filter操作,Sequence比List更慢。 深入瞭解函式,看起來像Sequence的Filter操作需要有更多的開銷來檢查某些狀態,而List的Filter則是一個簡單的檢查並收集新的元素。

試驗Map和Filter操作

val sequence = generateSequence(1) { it + 1 }.take(50000000)
val list = sequence.toList()

println("List Map Filter Sum\t\t " + measureNanoTime { 
    list.map { it * 2 }.filter { it % 3 == 0 }.sum() })
println("Sequence Map Filter Sum\t " + measureNanoTime { 
    sequence.map { it * 2 }.filter { it % 3 == 0 }.sum() })

println("List Map Filter Average\t\t " + measureNanoTime { 
    list.map { it * 2 }.filter { it % 3 == 0 }.average() })
println("Sequence Map Filter Average\t " + measureNanoTime { 
    sequence.map { it * 2 }.filter { it % 3 == 0 }.average() })
複製程式碼

結果是:

List Map Filter Sum 34845242323
Sequence Map Filter Sum 2820436086
List Map Filter Average 2328258876
Sequence Map Filter Average 18618444560
複製程式碼
  • List: 在Map:Filter:Sum操作上花費了34.8s,在Map:Filter:Average操作上花費了2.3s
  • Sequence: 在Map:Filter:Sum操作上花費了2.8s, 在Map:Filter:Average操作上花費了18.6s

[譯]Kotlin中的龜(List)兔(Sequence)賽跑

一個相對令人驚訝的結果,如Map:Filter:Sum,Sequence比List快得多,而Map:Filter:Average,List比Sequence要快得多。

試驗直接使用Sequence和List

val sequence = generateSequence(1) { it + 1 }.take(50000000)
val list = sequence.toList()

println("List Sum " + measureNanoTime { list.sum() })
println("Sequence Sum " + measureNanoTime { sequence.sum() })

println("List Average " + measureNanoTime { list.average() })
println("Sequence Average " + measureNanoTime { sequence.average() })
複製程式碼

結果是:

List Sum 91726022
Sequence Sum 592771887
List Average 101141460
Sequence Average 622616340
複製程式碼
  • List: 在Sum操作上花費了0.1s,在Average操作上花費了0.1s
  • Sequence: 在Sum操作上花費了0.5s, 在Average操作上花費了0.6s

沒有任何中間操作,明顯列表List比序列Sequence要快。

總結:

  • 1、當不需要中間操作時,使用List
  • 2、當僅僅只有map操作時,使用sequence
  • 3、當僅僅只有filter操作時,使用List
  • 4、如果末端操作是first時,使用sequence
  • 5、對於沒有提及的其他操作符或者其他操作符的組合,請嘗試使用例子去驗證一下

譯者有話說:

首先,說下為什麼要翻譯這篇部落格?關於Kotlin中的Sequence和List的使用以及原始碼解析相關的文章我已經寫過兩篇了,這篇部落格主要吸引我的一點就是以更多執行的例子試驗和相關幽默的配圖更加形象地描述了Sequence,List的區別以及各自的使用場景。

然而,這篇部落格並沒有深入原始碼去講解Sequence的實現,這篇之前寫的部落格 淺談Kotlin中的序列(Sequences)原始碼完全解析(十) 從原始碼角度帶你一步步分析Sequence序列背後的原理,關於如何正確使用Sequence和List以及各自使用場景,之前翻譯的一篇部落格 [譯]Kotlin中是應該使用序列(Sequences)還是集合(Lists)? 會有更加全面的介紹。

最後,有了這三篇文章應該更加全面理解了Sequence的原理和使用。

[譯]Kotlin中的龜(List)兔(Sequence)賽跑

歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~

相關文章