Stream Performance
已經對 Stream API 的用法鼓吹夠多了,用起簡潔直觀,但性能到底怎麼樣呢?會不會有很高的性能損失?
本節我們對 Stream API 的性能一探究竟。
為保證測試結果真實可信,我們將 JVM 運行在-server模式下,測試數據在 GB 量級,測試機器採用常見的商用服務器,配置如下:
測試方法和測試數據
性能測試並不是容易的事,Java 性能測試更費勁,因為虛擬機對性能的影響很大,JVM 對性能的影響有兩方面:
- GC 的影響。GC 的行為是 Java 中很不好控制的一塊,為增加確定性,我們手動指定使用 CMS 收集器,並使用 10GB 固定大小的堆內存。具體到 JVM 參數就是-XX:+UseConcMarkSweepGC-Xms10G-Xmx10G
- JIT(Just-In-Time) 即時編譯技術。即時編譯技術會將熱點代碼在 JVM 運行的過程中編譯成本地代碼,測試時我們會先對程序預熱,觸發對測試函數的即時編譯。相關的 JVM 參數是-XX:CompileThreshold=10000。
Stream 並行執行時用到ForkJoinPool.commonPool()得到的線程池,為控制並行度我們使用 Linux 的taskset命令指定 JVM 可用的核數。
測試數據由程序隨機生成。為防止一次測試帶來的抖動,測試 4 次求出平均時間作為運行時間。
實驗一 基本類型迭代
測試內容:找出整型數組中的最小值。對比 for 循環外部迭代和 Stream API 內部迭代性能。
測試程序 IntTest,測試結果如下圖:
圖中展示的是 for 循環外部迭代耗時為基準的時間比值。分析如下:
- 對於基本類型 Stream 串行迭代的性能開銷明顯高於外部迭代開銷(兩倍);
- Stream 並行迭代的性能比串行迭代和外部迭代都好。
並行迭代性能跟可利用的核數有關,上圖中的並行迭代使用了全部 12 個核,為考察使用核數對性能的影響,我們專門測試了不同核數下的 Stream 並行迭代效果:
分析,對於基本類型:
- 使用 Stream 並行 API 在單核情況下性能很差,比 Stream 串行 API 的性能還差;
- 隨著使用核數的增加,Stream 並行效果逐漸變好,比使用 for 循環外部迭代的性能還好。
以上兩個測試說明,對於基本類型的簡單迭代,Stream 串行迭代性能更差,但多核情況下 Stream 迭代時性能較好。
實驗二 對象迭代
再來看對象的迭代效果。
測試內容:找出字符串列表中最小的元素(自然順序),對比 for 循環外部迭代和 Stream API 內部迭代性能。
測試程序 StringTest,測試結果如下圖:
結果分析如下:
- 對於對象類型 Stream 串行迭代的性能開銷仍然高於外部迭代開銷(1.5 倍),但差距沒有基本類型那麼大。
- Stream 並行迭代的性能比串行迭代和外部迭代都好。
再來單獨考察 Stream 並行迭代效果:
分析,對於對象類型:
- 使用 Stream 並行 API 在單核情況下性能比 for 循環外部迭代差;
- 隨著使用核數的增加,Stream 並行效果逐漸變好,多核帶來的效果明顯。
以上兩個測試說明,對於對象類型的簡單迭代,Stream 串行迭代性能更差,但多核情況下 Stream 迭代時性能較好。
實驗三 複雜對象歸約
從實驗一、二的結果來看,Stream 串行執行的效果都比外部迭代差(很多),是不是說明 Stream 真的不行了?先別下結論,我們再來考察一下更復雜的操作。
測試內容:給定訂單列表,統計每個用戶的總交易額。對比使用外部迭代手動實現和 Stream API 之間的性能。
我們將訂單簡化為<username>構成的元組,並用Order對象來表示。測試程序 ReductionTest,測試結果如下圖:/<username>
分析,對於複雜的歸約操作:
- Stream API 的性能普遍好於外部手動迭代,並行 Stream 效果更佳;
再來考察並行度對並行效果的影響,測試結果如下:
分析,對於複雜的歸約操作:
- 使用 Stream 並行歸約在單核情況下性能比串行歸約以及手動歸約都要差,簡單說就是最差的;
- 隨著使用核數的增加,Stream 並行效果逐漸變好,多核帶來的效果明顯。
以上兩個實驗說明,對於複雜的歸約操作,Stream 串行歸約效果好於手動歸約,在多核情況下,並行歸約效果更佳。我們有理由相信,對於其他複雜的操作,Stream API 也能表現出相似的性能表現。
結論
上述三個實驗的結果可以總結如下:
- 對於簡單操作,比如最簡單的遍歷,Stream 串行 API 性能明顯差於顯示迭代,但並行的 Stream API 能夠發揮多核特性。
- 對於複雜操作,Stream 串行 API 性能可以和手動實現的效果匹敵,在並行執行時 Stream API 效果遠超手動實現。
所以,如果出於性能考慮,
- 對於簡單操作推薦使用外部迭代手動實現,
- 對於複雜操作,推薦使用 Stream API,
- 在多核情況下,推薦使用並行 Stream API 來發揮多核優勢,
- 單核情況下不建議使用並行 Stream API。
如果出於代碼簡潔性考慮,使用 Stream API 能夠寫出更短的代碼。即使是從性能方面說,儘可能的使用 Stream API 也另外一個優勢,那就是隻要 Java Stream 類庫做了升級優化,代碼不用做任何修改就能享受到升級帶來的好處。
閱讀更多 Java的小本家 的文章