Java 8 的 Stream API 這麼牛X,性能如何呢?

Stream Performance

已經對 Stream API 的用法鼓吹夠多了,用起簡潔直觀,但性能到底怎麼樣呢?會不會有很高的性能損失?

本節我們對 Stream API 的性能一探究竟。

為保證測試結果真實可信,我們將 JVM 運行在-server模式下,測試數據在 GB 量級,測試機器採用常見的商用服務器,配置如下:


Java 8 的 Stream API 這麼牛X,性能如何呢?

測試方法和測試數據

性能測試並不是容易的事,Java 性能測試更費勁,因為虛擬機對性能的影響很大,JVM 對性能的影響有兩方面:

  1. GC 的影響。GC 的行為是 Java 中很不好控制的一塊,為增加確定性,我們手動指定使用 CMS 收集器,並使用 10GB 固定大小的堆內存。具體到 JVM 參數就是-XX:+UseConcMarkSweepGC-Xms10G-Xmx10G
  2. JIT(Just-In-Time) 即時編譯技術。即時編譯技術會將熱點代碼在 JVM 運行的過程中編譯成本地代碼,測試時我們會先對程序預熱,觸發對測試函數的即時編譯。相關的 JVM 參數是-XX:CompileThreshold=10000。

Stream 並行執行時用到ForkJoinPool.commonPool()得到的線程池,為控制並行度我們使用 Linux 的taskset命令指定 JVM 可用的核數。

測試數據由程序隨機生成。為防止一次測試帶來的抖動,測試 4 次求出平均時間作為運行時間。

實驗一 基本類型迭代

測試內容:找出整型數組中的最小值。對比 for 循環外部迭代和 Stream API 內部迭代性能。

測試程序 IntTest,測試結果如下圖:

Java 8 的 Stream API 這麼牛X,性能如何呢?

圖中展示的是 for 循環外部迭代耗時為基準的時間比值。分析如下:

  1. 對於基本類型 Stream 串行迭代的性能開銷明顯高於外部迭代開銷(兩倍);
  2. Stream 並行迭代的性能比串行迭代和外部迭代都好。

並行迭代性能跟可利用的核數有關,上圖中的並行迭代使用了全部 12 個核,為考察使用核數對性能的影響,我們專門測試了不同核數下的 Stream 並行迭代效果:

Java 8 的 Stream API 這麼牛X,性能如何呢?


分析,對於基本類型:

  1. 使用 Stream 並行 API 在單核情況下性能很差,比 Stream 串行 API 的性能還差;
  2. 隨著使用核數的增加,Stream 並行效果逐漸變好,比使用 for 循環外部迭代的性能還好。

以上兩個測試說明,對於基本類型的簡單迭代,Stream 串行迭代性能更差,但多核情況下 Stream 迭代時性能較好。

實驗二 對象迭代

再來看對象的迭代效果。

測試內容:找出字符串列表中最小的元素(自然順序),對比 for 循環外部迭代和 Stream API 內部迭代性能。

測試程序 StringTest,測試結果如下圖:

Java 8 的 Stream API 這麼牛X,性能如何呢?


結果分析如下:

  1. 對於對象類型 Stream 串行迭代的性能開銷仍然高於外部迭代開銷(1.5 倍),但差距沒有基本類型那麼大。
  2. Stream 並行迭代的性能比串行迭代和外部迭代都好。

再來單獨考察 Stream 並行迭代效果:

Java 8 的 Stream API 這麼牛X,性能如何呢?

分析,對於對象類型:

  1. 使用 Stream 並行 API 在單核情況下性能比 for 循環外部迭代差;
  2. 隨著使用核數的增加,Stream 並行效果逐漸變好,多核帶來的效果明顯。

以上兩個測試說明,對於對象類型的簡單迭代,Stream 串行迭代性能更差,但多核情況下 Stream 迭代時性能較好。

實驗三 複雜對象歸約

從實驗一、二的結果來看,Stream 串行執行的效果都比外部迭代差(很多),是不是說明 Stream 真的不行了?先別下結論,我們再來考察一下更復雜的操作。

測試內容:給定訂單列表,統計每個用戶的總交易額。對比使用外部迭代手動實現和 Stream API 之間的性能。

我們將訂單簡化為<username>構成的元組,並用Order對象來表示。測試程序 ReductionTest,測試結果如下圖:/<username>

Java 8 的 Stream API 這麼牛X,性能如何呢?

分析,對於複雜的歸約操作:

  1. Stream API 的性能普遍好於外部手動迭代,並行 Stream 效果更佳;

再來考察並行度對並行效果的影響,測試結果如下:

Java 8 的 Stream API 這麼牛X,性能如何呢?

分析,對於複雜的歸約操作:

  1. 使用 Stream 並行歸約在單核情況下性能比串行歸約以及手動歸約都要差,簡單說就是最差的;
  2. 隨著使用核數的增加,Stream 並行效果逐漸變好,多核帶來的效果明顯。

以上兩個實驗說明,對於複雜的歸約操作,Stream 串行歸約效果好於手動歸約,在多核情況下,並行歸約效果更佳。我們有理由相信,對於其他複雜的操作,Stream API 也能表現出相似的性能表現。

結論

上述三個實驗的結果可以總結如下:

  1. 對於簡單操作,比如最簡單的遍歷,Stream 串行 API 性能明顯差於顯示迭代,但並行的 Stream API 能夠發揮多核特性。
  2. 對於複雜操作,Stream 串行 API 性能可以和手動實現的效果匹敵,在並行執行時 Stream API 效果遠超手動實現。

所以,如果出於性能考慮,

  1. 對於簡單操作推薦使用外部迭代手動實現,
  2. 對於複雜操作,推薦使用 Stream API,
  3. 在多核情況下,推薦使用並行 Stream API 來發揮多核優勢,
  4. 單核情況下不建議使用並行 Stream API。

如果出於代碼簡潔性考慮,使用 Stream API 能夠寫出更短的代碼。即使是從性能方面說,儘可能的使用 Stream API 也另外一個優勢,那就是隻要 Java Stream 類庫做了升級優化,代碼不用做任何修改就能享受到升級帶來的好處。


分享到:


相關文章: