Java 8 初探 - Lazy Evaluation & Parallel Stream

閱讀時間約 8 分鐘
此為過去的舊文,2014 年 4 月 20 日初次發表於 logdown。

除了 Pipe 的設計外,Stream 另外讓我好奇的二個特色:lazy evaluation parallel stream。lazy evaluation 可以想成是延後運算到真正必要的時候,而 parallel stream 則是將 Pipe 以平行運算的方式進行,最重要的是這二個特色都是針對 JVM 最佳化過的,應該比我們自己寫來的更有效率。

當看到 lazy evaluation 我最先想到的是,應該對載入大型檔案有幫助,例如:減少記憶體使用量,但我沒把握這想法是否正確,所以設計了一個實驗試試看我的想法。首先,設計一個 FileSearchStrategy 介面,可以輸入檔案(目錄)、關鍵字和結果收集器 (SearchResultCollector),每個實作可以用不同的方式從檔案中搜尋關鍵字,並將結果 <檔案名稱、行數、該行內容> 存放到收集器中。

由於 Java 的 File 可能指向一個檔案或目錄,因此 AbstractSearchStrategy 實作 FileSearchStrategysearch(File, String, SearchResultCollector),以遞迴的方式走訪每一層目錄,並留一個 hook method 讓繼承者提供實際掃描檔案的實作。

基礎架構完成後,第一個預設實作 DefaultSearchStrategy 是在還沒有 Stream API 之前,針對文字檔案常用的演算法:逐行掃描。這個實作的結果即是實驗的基準值。

接著 AllLinesSearchStrategy 的實作,使用在Java 7推出的 NIO 2 (New I/O 2) 套件所提供的 FilesreadAllLines(Path)函式,事實上,在 Java Doc 的說明中,這個函式只適合用在簡單的案例,並不適合用在大檔案上,因此,這個實作應該會得到實驗中最差的結果。

為了方便後續平行處理的實驗,StreamSearchStrategy 的實作,將實際 Pipe 的運算組成放到另一個函式:scanStream(Stream, String, SearchResultCollector)中,然後使用 BufferedReaderlines() 函式取得 Stream 物件進行運算。為了取得行號,Pipe 的第一個 intermediate operation 使用 map(Function),將字串轉成同時帶有行號與字串內容的物件 (使用 KeywordSearchResult 只是簡化實作),然後再用 filter(Predicate) 過濾掉不要的物件。

為了取得行號,所以 StreamSearchStrategyscanStream(Stream, String, SearchResultCollector) 中 Pipe 組成是先用 map(Function) 再用 filter(Predicate)。若忽略行號,改成 StreamSearchStrategyV2,先使用 filter(Predicate) 再使用 map(Function),對結果會有影響嗎?

目前系統記憶體越來越大,作業系統常會將檔案內容快取在系統記憶體中,當第二次讀取相同檔案時,速度可以加速許多,但對實驗來說,這會是個影響數據的關鍵,為了避免讀到快取的檔案內容,實驗準備了三個資料夾,每個資料夾放入 Table 1 所述相同的檔案結構,以 A 子資料夾為例,一個檔案有 100k 行 (筆) 資料,大小約 3.62 MB,這樣的檔案有 10 個檔案,B、C 等子資料夾依此類推,G 子資料夾則是將 A~F 子資料夾複製一份放入,120 個檔案合計 4.45 GB。


Table 1 - Test data

Table 1 - Test data


Table 2 - Test environment

Table 2 - Test environment


當然,這是否可行要看系統記憶體的多寡,實驗的環境如 Table 2 所列,而測試方法如下,每種 strategy 依序掃描三個資料夾,當第二個 strategy 開始掃描第一個資料夾時,因其他兩個資料夾在前一個 strategy 載入,總量有 8.9 GB,應該要超過系統記憶體,第一個資料夾內的內容應該已不在快取中。實驗使用一個 MemoryUsageMonitor 的物件,定期監控JVM的記憶體使用量,並記錄每次掃描的峰值。


好啦!該是公布測試結果 (Table 3) 的時候了,All Lines 果然如預期般使用最多的記憶體(超過1GB),所花費的時間也是最長的,多了 20 秒左右,但 Default、Stream 和 Stream v2 之間的差異不大,就執行時間上,三者的差距大約3秒,而記憶體的使用量 Stream v2 和 Default 幾乎是一樣,但 Stream 一開始的 map(Function) 似乎是致命傷。

Table 3 - Test result of four strategies

Table 3 - Test result of four strategies


先前 AbstractSearchStrategy 使用的是傳統 foreach 方式走訪所有的檔案,如果使用 Stream的parallel 會有幫助嗎?還是更糟?所以,將 AbstractSearchStrategysearch(File, String, SearchResultCollector) 改成如下,再次執行測試。

原先期望透過 parallel stream 的方式加快執行速度,但從 Table 4 看來,幾乎沒有加速,反而還帶來了反效果:記憶體使用量暴增。令人意外的是 Stream v2 的記憶體使用量比 Stream 還多,不知該怎麼解釋。


Table 4 - Test result of four strategies with parallel directories traversal

Table 4 - Test result of four strategies with parallel directories traversal


似乎只要和 I/O 扯上關係,即使用平行運算的方式也沒有減少太多的執行時間,有時候反而還更慢,那如果資料不在硬碟的檔案裡,都已經在記憶體中,那效果會如何?因此,設計了第三個實驗:100k ~ 6400k 筆資料存放在 ArrayList 中,然後根據參數使用 parallelStream()stream() 作為 scanStream(Stream, String, SearchResultCollector) 的輸入。

實驗結果列於 Table 5,100k 一欄,不論是 Stream 或 Stream v2 哪個先執行都會得到不理想的數據,可能是程式冷啟動所引起,所以忽略 100k 欄的數值。從 200k 開始,不論使用 Stream 或 Stream v2,都可看到當使用 parallelStream() 時,執行時間有明顯的減少,在 Stream v2 的 6400k 一欄,節省了 136 ms。而且整體來說,Stream v2 也明顯比 Stream 要好。


Table 5 - The execution time (ms) with parallel stream

Table 5 - The execution time (ms) with parallel stream


該是結論的時候了,首先,lazy evaluation 的效益必須是在 pipe 的組合上有最佳化過的,若組合的不好反而更糟糕,且在 I/O 上幫助似乎也不大。parallel stream 要能發揮效果必須看資料的來源類型,I/O 類型或是存取上有競爭現象的資料較難發揮出效益,但若是在記憶體當中的資料,彼此無存取競爭 (不用使用 lock) 的現象,那 parallel stream 的效果就相當明顯,不過要注意的是 parallel stream 也會使記憶體的使用量增加,使用上也要小心。

    avatar-img
    53會員
    104內容數
    這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
    留言0
    查看全部
    avatar-img
    發表第一個留言支持創作者!
    Spirit的沙龍 的其他內容
    老實說,看到 Java Sream API 讓我感到相當親切,這應該跟我研究所多年的研究題目是 visual dataflow language 有關,Java Stream API 把迴圈給內化了,每個 operation 的重點是要做什麼,大大提高了程式的抽象化程度和可讀性。
    最後,Java 8 雖然支援 Lambda,但我覺得 Closure 某種程度上還不稱不上是 Java 的第一級居民,我還是比較喜歡寫一些小而易測的 class,而不是使用 Lambda,至於捕捉變數,透過建構子將變數帶入物件也是一種方式。
    Java 8 終於在 2014 的 3 月 18 日正式釋出了,不過自從用 Objective C 開發 iOS App後,我已經有好一陣子沒碰 Java,期間曾經有短暫寫一點點,但卻沒有時間去用 beta 版的 Java 8,直到最近才又開始玩一下。
    內容十分精實,一百多頁很薄的一本書,但含了很多有用的資訊,就算不是開發微服務,書中的內容也可以用在很多雲端服務的開發與維運上。中文版唯一可惜的地方,翻譯非常不通順,很多不像中文的句子,會看到好幾個「與」連在一起用,標點符號的用法也有點怪,閱讀的痛苦指數有點高...
    3/5高品質微服務
    今天的任務是找到獨一無案的方法創造新事物,不只讓未來變得不一樣,而且要更好,所以我們要從 0 到 1。最重要的第一步是自己獨立思考。唯有重新看待世界,像古人首次見到它那樣覺得新鮮古怪,我們才能重新創造,並將更好的未來留給後世。
    5/5從 0 到 1
    這陣子比較有空可以去天瓏書局晃晃,正好看到這本剛上市不久的書,整體上大多數守則,也是我自己一直在遵循的,是相當不錯的一本總結書。但真的要仔細看每一節的內容,理解每個原則背後的情境與想要改善的問題是什麼。如果只是把每一節的標題拿來使用,很容易就會發現衝突的部分。
    5/5程式設計守則
    老實說,看到 Java Sream API 讓我感到相當親切,這應該跟我研究所多年的研究題目是 visual dataflow language 有關,Java Stream API 把迴圈給內化了,每個 operation 的重點是要做什麼,大大提高了程式的抽象化程度和可讀性。
    最後,Java 8 雖然支援 Lambda,但我覺得 Closure 某種程度上還不稱不上是 Java 的第一級居民,我還是比較喜歡寫一些小而易測的 class,而不是使用 Lambda,至於捕捉變數,透過建構子將變數帶入物件也是一種方式。
    Java 8 終於在 2014 的 3 月 18 日正式釋出了,不過自從用 Objective C 開發 iOS App後,我已經有好一陣子沒碰 Java,期間曾經有短暫寫一點點,但卻沒有時間去用 beta 版的 Java 8,直到最近才又開始玩一下。
    內容十分精實,一百多頁很薄的一本書,但含了很多有用的資訊,就算不是開發微服務,書中的內容也可以用在很多雲端服務的開發與維運上。中文版唯一可惜的地方,翻譯非常不通順,很多不像中文的句子,會看到好幾個「與」連在一起用,標點符號的用法也有點怪,閱讀的痛苦指數有點高...
    3/5高品質微服務
    今天的任務是找到獨一無案的方法創造新事物,不只讓未來變得不一樣,而且要更好,所以我們要從 0 到 1。最重要的第一步是自己獨立思考。唯有重新看待世界,像古人首次見到它那樣覺得新鮮古怪,我們才能重新創造,並將更好的未來留給後世。
    5/5從 0 到 1
    這陣子比較有空可以去天瓏書局晃晃,正好看到這本剛上市不久的書,整體上大多數守則,也是我自己一直在遵循的,是相當不錯的一本總結書。但真的要仔細看每一節的內容,理解每個原則背後的情境與想要改善的問題是什麼。如果只是把每一節的標題拿來使用,很容易就會發現衝突的部分。
    5/5程式設計守則
    你可能也想看
    Google News 追蹤
    在現代學術研究中,閱讀大量文獻是一個必要且繁重的任務。然而,通過將學術閱讀流程標準化,能顯著提升效率,使閱讀變得更加有條理。最近,我實踐了兩個已經標準化的動作,每個動作都進行了50次以上的測試,效果顯著。 ▋動作一:將文獻內容轉為資訊塊 這個動作的目的是將閱讀材料中的重要內容轉換為小而具
    Thumbnail
    本章節的目的是介紹Java中的套件使用,包括如何引用第三方套件和自定義模組,如何創建和使用自定義套件,以及介紹一些常見的Java標準庫套件。這些內容將幫助讀者更好地理解和使用Java的套件系統。
    Thumbnail
    此章節的目的是介紹Java程式語言中的流程控制結構,包括條件語句(if, else if, else)、三元運算子、switch語句,以及各種迴圈(for, foreach, while)。同時,也解釋了如何在迴圈中使用控制語句來改變程式的執行流程。每種主題都配有示例程式碼以幫助理解。
    Thumbnail
    最近我人都泡在Threads上做社會觀察。 脆的演算法實在是太特別...
    Thumbnail
    此章節旨在介紹Java的基本語法、註解和變數的使用。透過學習,讀者將了解Java程式的基本結構、程式進入點的定義、如何撰寫單行和多行註解,以及如何宣告和初始化變數。
    Thumbnail
    這篇文章的目的是對Java程式設計語言進行介紹,包括它的特性、應用範疇、主要使用者,以及相關的學習資源和常見的庫與框架。此外,它也提供了一些學習Java的渠道,以及與Java相關的其他知識。
    Thumbnail
    敏捷開發方法已成為現代軟體開發領域的一個關鍵趨勢。其主要目的是通過快速和增量的開發過程,提高開發效率和應對變化的能力。本文將深入探討Scrum和Kanban這兩種流行的敏捷方法的基本原理,實際應用案例,以及實施過程中可能遇到的挑戰和解決策略。
    Thumbnail
    Kafka是一個先進的分佈式流處理平臺,具有高吞吐量、可擴展性、容錯性和低延遲特性,提供瞭解耦、非同步和削峰特點。本文介紹了Kafka的通訊模式、適合的應用場景和未來發展趨勢,旨在幫助使用者更好地理解和應用Kafka。
    Thumbnail
    pipe 代表函數式程式設計中的概念,利用多個功能結合在一起,資料依序通過每個功能進行處理。文章中介紹了 pipe 的優點、兩個等效的程式碼比較以及 remeda 套件的使用。詳細介紹了使用 pipe 的好處,並提供了多個相關的例子,展示了 pipe 可讀性的提升。
    在現代學術研究中,閱讀大量文獻是一個必要且繁重的任務。然而,通過將學術閱讀流程標準化,能顯著提升效率,使閱讀變得更加有條理。最近,我實踐了兩個已經標準化的動作,每個動作都進行了50次以上的測試,效果顯著。 ▋動作一:將文獻內容轉為資訊塊 這個動作的目的是將閱讀材料中的重要內容轉換為小而具
    Thumbnail
    本章節的目的是介紹Java中的套件使用,包括如何引用第三方套件和自定義模組,如何創建和使用自定義套件,以及介紹一些常見的Java標準庫套件。這些內容將幫助讀者更好地理解和使用Java的套件系統。
    Thumbnail
    此章節的目的是介紹Java程式語言中的流程控制結構,包括條件語句(if, else if, else)、三元運算子、switch語句,以及各種迴圈(for, foreach, while)。同時,也解釋了如何在迴圈中使用控制語句來改變程式的執行流程。每種主題都配有示例程式碼以幫助理解。
    Thumbnail
    最近我人都泡在Threads上做社會觀察。 脆的演算法實在是太特別...
    Thumbnail
    此章節旨在介紹Java的基本語法、註解和變數的使用。透過學習,讀者將了解Java程式的基本結構、程式進入點的定義、如何撰寫單行和多行註解,以及如何宣告和初始化變數。
    Thumbnail
    這篇文章的目的是對Java程式設計語言進行介紹,包括它的特性、應用範疇、主要使用者,以及相關的學習資源和常見的庫與框架。此外,它也提供了一些學習Java的渠道,以及與Java相關的其他知識。
    Thumbnail
    敏捷開發方法已成為現代軟體開發領域的一個關鍵趨勢。其主要目的是通過快速和增量的開發過程,提高開發效率和應對變化的能力。本文將深入探討Scrum和Kanban這兩種流行的敏捷方法的基本原理,實際應用案例,以及實施過程中可能遇到的挑戰和解決策略。
    Thumbnail
    Kafka是一個先進的分佈式流處理平臺,具有高吞吐量、可擴展性、容錯性和低延遲特性,提供瞭解耦、非同步和削峰特點。本文介紹了Kafka的通訊模式、適合的應用場景和未來發展趨勢,旨在幫助使用者更好地理解和應用Kafka。
    Thumbnail
    pipe 代表函數式程式設計中的概念,利用多個功能結合在一起,資料依序通過每個功能進行處理。文章中介紹了 pipe 的優點、兩個等效的程式碼比較以及 remeda 套件的使用。詳細介紹了使用 pipe 的好處,並提供了多個相關的例子,展示了 pipe 可讀性的提升。