Java8 的 Stream API 的確牛X,但性能究竟如何呢?
Stream Performance
已經(jīng)對(duì) Stream API 的用法鼓吹夠多了,用起簡(jiǎn)潔直觀,但性能到底怎么樣呢?會(huì)不會(huì)有很高的性能損失?本節(jié)我們對(duì) Stream API 的性能一探究竟。
為保證測(cè)試結(jié)果真實(shí)可信,我們將 JVM 運(yùn)行在 -server
模式下,測(cè)試數(shù)據(jù)在 GB 量級(jí),測(cè)試機(jī)器采用常見(jiàn)的商用服務(wù)器,配置如下:
OSCentOS 6.7 x86_64CPUIntel Xeon X5675, 12M Cache 3.06 GHz, 6 Cores 12 Threads內(nèi)存96GBJDKjava version 1.8.0_91, Java HotSpot(TM) 64-Bit Server VM
測(cè)試方法和測(cè)試數(shù)據(jù)
性能測(cè)試并不是容易的事,Java 性能測(cè)試更費(fèi)勁,因?yàn)樘摂M機(jī)對(duì)性能的影響很大,JVM 對(duì)性能的影響有兩方面:
GC 的影響。GC 的行為是 Java 中很不好控制的一塊,為增加確定性,我們手動(dòng)指定使用 CMS 收集器,并使用 10GB 固定大小的堆內(nèi)存。具體到 JVM 參數(shù)就是
-XX:+UseConcMarkSweepGC-Xms10G-Xmx10G
JIT(Just-In-Time) 即時(shí)編譯技術(shù)。即時(shí)編譯技術(shù)會(huì)將熱點(diǎn)代碼在 JVM 運(yùn)行的過(guò)程中編譯成本地代碼,測(cè)試時(shí)我們會(huì)先對(duì)程序預(yù)熱,觸發(fā)對(duì)測(cè)試函數(shù)的即時(shí)編譯。相關(guān)的 JVM 參數(shù)是
-XX:CompileThreshold=10000
。
Stream 并行執(zhí)行時(shí)用到 ForkJoinPool.commonPool()
得到的線程池,為控制并行度我們使用 Linux 的 taskset
命令指定 JVM 可用的核數(shù)。
測(cè)試數(shù)據(jù)由程序隨機(jī)生成。為防止一次測(cè)試帶來(lái)的抖動(dòng),測(cè)試 4 次求出平均時(shí)間作為運(yùn)行時(shí)間。
實(shí)驗(yàn)一 基本類(lèi)型迭代
測(cè)試內(nèi)容:找出整型數(shù)組中的最小值。對(duì)比 for 循環(huán)外部迭代和 Stream API 內(nèi)部迭代性能。
測(cè)試程序 IntTest,測(cè)試結(jié)果如下圖:

圖中展示的是 for 循環(huán)外部迭代耗時(shí)為基準(zhǔn)的時(shí)間比值。分析如下:
對(duì)于基本類(lèi)型 Stream 串行迭代的性能開(kāi)銷(xiāo)明顯高于外部迭代開(kāi)銷(xiāo)(兩倍);
Stream 并行迭代的性能比串行迭代和外部迭代都好。
并行迭代性能跟可利用的核數(shù)有關(guān),上圖中的并行迭代使用了全部 12 個(gè)核,為考察使用核數(shù)對(duì)性能的影響,我們專(zhuān)門(mén)測(cè)試了不同核數(shù)下的 Stream 并行迭代效果:

分析,對(duì)于基本類(lèi)型:
使用 Stream 并行 API 在單核情況下性能很差,比 Stream 串行 API 的性能還差;
隨著使用核數(shù)的增加,Stream 并行效果逐漸變好,比使用 for 循環(huán)外部迭代的性能還好。
以上兩個(gè)測(cè)試說(shuō)明,對(duì)于基本類(lèi)型的簡(jiǎn)單迭代,Stream 串行迭代性能更差,但多核情況下 Stream 迭代時(shí)性能較好。
實(shí)驗(yàn)二 對(duì)象迭代
再來(lái)看對(duì)象的迭代效果。
測(cè)試內(nèi)容:找出字符串列表中最小的元素(自然順序),對(duì)比 for 循環(huán)外部迭代和 Stream API 內(nèi)部迭代性能。
測(cè)試程序 StringTest,測(cè)試結(jié)果如下圖:

結(jié)果分析如下:
對(duì)于對(duì)象類(lèi)型 Stream 串行迭代的性能開(kāi)銷(xiāo)仍然高于外部迭代開(kāi)銷(xiāo)(1.5 倍),但差距沒(méi)有基本類(lèi)型那么大。
Stream 并行迭代的性能比串行迭代和外部迭代都好。
再來(lái)單獨(dú)考察 Stream 并行迭代效果:

分析,對(duì)于對(duì)象類(lèi)型:
使用 Stream 并行 API 在單核情況下性能比 for 循環(huán)外部迭代差;
隨著使用核數(shù)的增加,Stream 并行效果逐漸變好,多核帶來(lái)的效果明顯。
以上兩個(gè)測(cè)試說(shuō)明,對(duì)于對(duì)象類(lèi)型的簡(jiǎn)單迭代,Stream 串行迭代性能更差,但多核情況下 Stream 迭代時(shí)性能較好。
實(shí)驗(yàn)三 復(fù)雜對(duì)象歸約
從實(shí)驗(yàn)一、二的結(jié)果來(lái)看,Stream 串行執(zhí)行的效果都比外部迭代差(很多),是不是說(shuō)明 Stream 真的不行了?先別下結(jié)論,我們?cè)賮?lái)考察一下更復(fù)雜的操作。
測(cè)試內(nèi)容:給定訂單列表,統(tǒng)計(jì)每個(gè)用戶(hù)的總交易額。對(duì)比使用外部迭代手動(dòng)實(shí)現(xiàn)和 Stream API 之間的性能。
我們將訂單簡(jiǎn)化為 <userName,price,timeStamp>
構(gòu)成的元組,并用 Order
對(duì)象來(lái)表示。測(cè)試程序 ReductionTest,測(cè)試結(jié)果如下圖:

分析,對(duì)于復(fù)雜的歸約操作:
Stream API 的性能普遍好于外部手動(dòng)迭代,并行 Stream 效果更佳;
再來(lái)考察并行度對(duì)并行效果的影響,測(cè)試結(jié)果如下:

分析,對(duì)于復(fù)雜的歸約操作:
使用 Stream 并行歸約在單核情況下性能比串行歸約以及手動(dòng)歸約都要差,簡(jiǎn)單說(shuō)就是最差的;
隨著使用核數(shù)的增加,Stream 并行效果逐漸變好,多核帶來(lái)的效果明顯。
以上兩個(gè)實(shí)驗(yàn)說(shuō)明,對(duì)于復(fù)雜的歸約操作,Stream 串行歸約效果好于手動(dòng)歸約,在多核情況下,并行歸約效果更佳。我們有理由相信,對(duì)于其他復(fù)雜的操作,Stream API 也能表現(xiàn)出相似的性能表現(xiàn)。
結(jié)論
上述三個(gè)實(shí)驗(yàn)的結(jié)果可以總結(jié)如下:
對(duì)于簡(jiǎn)單操作,比如最簡(jiǎn)單的遍歷,Stream 串行 API 性能明顯差于顯示迭代,但并行的 Stream API 能夠發(fā)揮多核特性。
對(duì)于復(fù)雜操作,Stream 串行 API 性能可以和手動(dòng)實(shí)現(xiàn)的效果匹敵,在并行執(zhí)行時(shí) Stream API 效果遠(yuǎn)超手動(dòng)實(shí)現(xiàn)。
所以,如果出于性能考慮,1. 對(duì)于簡(jiǎn)單操作推薦使用外部迭代手動(dòng)實(shí)現(xiàn),2. 對(duì)于復(fù)雜操作,推薦使用 Stream API, 3. 在多核情況下,推薦使用并行 Stream API 來(lái)發(fā)揮多核優(yōu)勢(shì),4. 單核情況下不建議使用并行 Stream API。
如果出于代碼簡(jiǎn)潔性考慮,使用 Stream API 能夠?qū)懗龈痰拇a。即使是從性能方面說(shuō),盡可能的使用 Stream API 也另外一個(gè)優(yōu)勢(shì),那就是只要 Java Stream 類(lèi)庫(kù)做了升級(jí)優(yōu)化,代碼不用做任何修改就能享受到升級(jí)帶來(lái)的好處。