那么,Stream API的性能到底如何呢侠鳄,代碼整潔的背后是否意味著性能的損耗呢埠啃?本文我們對Stream API的性能一探究竟。
為保證測試結(jié)果真實可信伟恶,我們將JVM運行在 -server 模式下碴开,測試數(shù)據(jù)在GB量級,測試機器采用常見的商用服務(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
測試方法和測試數(shù)據(jù)
性能測試并不是容易的事潦牛,Java性能測試更費勁,因為虛擬機對性能的影響很大挡育,JVM對性能的影響有兩方面:
-XX:+UseConcMarkSweepGC -Xms10G -Xmx10G-XX:CompileThreshold=10000
Stream并行執(zhí)行時用到 ForkJoinPool.commonPool() 得到的線程池巴碗,為控制并行度我們使用Linux的 taskset 命令指定JVM可用的核數(shù)。
測試數(shù)據(jù)由程序隨機生成即寒。為防止一次測試帶來的抖動橡淆,測試4次求出平均時間作為運行時間。
實驗一 基本類型迭代
測試內(nèi)容:找出整型數(shù)組中的最小值母赵。對比for循環(huán)外部迭代和Stream API內(nèi)部迭代性能逸爵。
測試程序代碼:
/**
* java -server -Xms10G -Xmx10G -XX:+PrintGCDetails
* -XX:+UseConcMarkSweepGC -XX:CompileThreshold=1000 lee/IntTest
* taskset -c 0-[0,1,3,7] java ...
* @author CarpenterLee
*/publicclassIntTest{publicstaticvoidmain(String[] args){newIntTest().doTest(); }publicvoiddoTest(){ warmUp();int[] lengths = {10000,100000,1000000,10000000,100000000,1000000000};for(intlength : lengths){ System.out.println(String.format("---array length: %d---", length));int[] arr =newint[length]; randomInt(arr);inttimes =4;intmin1 =1;intmin2 =2;intmin3 =3;longstartTime; startTime = System.nanoTime();for(inti=0; i
測試結(jié)果如下圖:
圖中展示的是for循環(huán)外部迭代耗時為基準的時間比值。分析如下:
對于基本類型Stream串行迭代的性能開銷明顯高于外部迭代開銷(兩倍)凹嘲;
Stream并行迭代的性能比串行迭代和外部迭代都好师倔。
并行迭代性能跟可利用的核數(shù)有關(guān),上圖中的并行迭代使用了全部12個核周蹭,為考察使用核數(shù)對性能的影響溯革,我們專門測試了不同核數(shù)下的Stream并行迭代效果:
分析,對于基本類型:
使用Stream并行API在單核情況下性能很差谷醉,比Stream串行API的性能還差致稀;
隨著使用核數(shù)的增加,Stream并行效果逐漸變好俱尼,比使用for循環(huán)外部迭代的性能還好抖单。
以上兩個測試說明,對于基本類型的簡單迭代遇八,Stream串行迭代性能更差矛绘,但多核情況下Stream迭代時性能較好。
實驗二 對象迭代
再來看對象的迭代效果刃永。
測試內(nèi)容:找出字符串列表中最小的元素(自然順序)货矮,對比for循環(huán)外部迭代和Stream API內(nèi)部迭代性能。
測試程序代碼:
/**
* java -server -Xms10G -Xmx10G -XX:+PrintGCDetails
* -XX:+UseConcMarkSweepGC -XX:CompileThreshold=1000 lee/StringTest
* taskset -c 0-[0,1,3,7] java ...
* @author CarpenterLee
*/publicclass StringTest {publicstaticvoidmain(String[] args) {newStringTest().doTest(); }publicvoiddoTest(){ warmUp();int[] lengths = {10000,100000,1000000,10000000,20000000,40000000};for(intlength : lengths){ System.out.println(String.format("---List length: %d---", length)); ArrayList list = randomStringList(length);inttimes =4;Stringmin1 ="1";Stringmin2 ="2";Stringmin3 ="3";longstartTime; startTime = System.nanoTime();for(inti=0; i list = randomStringList(10);for(inti=0; i<20000; i++){ minStringForLoop(list); minStringStream(list); minStringParallelStream(list); } }privateStringminStringForLoop(ArrayList list){StringminStr =null;booleanfirst =true;for(Stringstr: list){if(first){ first =false; minStr =str; }if(minStr.compareTo(str)>0){ minStr =str; } }returnminStr; }privateStringminStringStream(ArrayList list){returnlist.stream().min(String::compareTo).get(); }privateStringminStringParallelStream(ArrayList list){returnlist.stream().parallel().min(String::compareTo).get(); }privateArrayList randomStringList(intlistLength){ ArrayList list =newArrayList<>(listLength); Random rand =newRandom();intstrLength =10; StringBuilder buf =newStringBuilder(strLength);for(inti=0; i
測試結(jié)果如下圖:
結(jié)果分析如下:
對于對象類型Stream串行迭代的性能開銷仍然高于外部迭代開銷(1.5倍)斯够,但差距沒有基本類型那么大囚玫。
Stream并行迭代的性能比串行迭代和外部迭代都好喧锦。
再來單獨考察Stream并行迭代效果:
分析,對于對象類型:
使用Stream并行API在單核情況下性能比for循環(huán)外部迭代差抓督;
隨著使用核數(shù)的增加燃少,Stream并行效果逐漸變好,多核帶來的效果明顯铃在。
以上兩個測試說明阵具,對于對象類型的簡單迭代,Stream串行迭代性能更差定铜,但多核情況下Stream迭代時性能較好阳液。
實驗三 復(fù)雜對象歸約
從實驗一、二的結(jié)果來看揣炕,Stream串行執(zhí)行的效果都比外部迭代差(很多)趁舀,是不是說明Stream真的不行了?先別下結(jié)論祝沸,我們再來考察一下更復(fù)雜的操作。
測試內(nèi)容:給定訂單列表越庇,統(tǒng)計每個用戶的總交易額罩锐。對比使用外部迭代手動實現(xiàn)和Stream API之間的性能。
我們將訂單簡化為 <userName, price, timeStamp> 構(gòu)成的元組卤唉,并用 Order 對象來表示涩惑。
測試程序代碼:
/**
* java -server -Xms10G -Xmx10G -XX:+PrintGCDetails
* -XX:+UseConcMarkSweepGC -XX:CompileThreshold=1000 lee/ReductionTest
* taskset -c 0-[0,1,3,7] java ...
* @author CarpenterLee
*/publicclass ReductionTest {publicstaticvoidmain(String[] args) {newReductionTest().doTest(); }publicvoiddoTest(){ warmUp();int[] lengths = {10000,100000,1000000,10000000,20000000,40000000};for(intlength : lengths){ System.out.println(String.format("---orders length: %d---", length)); List orders = Order.genOrders(length);inttimes =4; Map map1 =null; Map map2 =null; Map map3 =null;longstartTime; startTime = System.nanoTime();for(inti=0; i orders = Order.genOrders(10);for(inti=0; i<20000; i++){ sumOrderForLoop(orders); sumOrderStream(orders); sumOrderParallelStream(orders); } }privateMap sumOrderForLoop(List orders){ Mapmap=newHashMap<>();for(Order od : orders){StringuserName = od.getUserName(); Double v;if((v=map.get(userName)) !=null){map.put(userName, v+od.getPrice()); }else{map.put(userName, od.getPrice()); } }returnmap; }privateMap sumOrderStream(List orders){returnorders.stream().collect( Collectors.groupingBy(Order::getUserName,? Collectors.summingDouble(Order::getPrice))); }privateMap sumOrderParallelStream(List orders){returnorders.parallelStream().collect( Collectors.groupingBy(Order::getUserName,? Collectors.summingDouble(Order::getPrice))); }}class Order{privateStringuserName;privatedoubleprice;privatelongtimestamp;publicOrder(StringuserName,doubleprice,longtimestamp) {this.userName = userName;this.price = price;this.timestamp = timestamp; }publicStringgetUserName() {returnuserName; }publicdoublegetPrice() {returnprice; }publiclonggetTimestamp() {returntimestamp; }publicstaticList genOrders(intlistLength){ ArrayList list =newArrayList<>(listLength); Random rand =newRandom();intusers = listLength/200;// 200 orders per userusers = users==0? listLength : users; ArrayList userNames =newArrayList<>(users);for(inti=0; i
測試結(jié)果如下圖:
分析,對于復(fù)雜的歸約操作:
Stream API的性能普遍好于外部手動迭代桑驱,并行Stream效果更佳竭恬;
再來考察并行度對并行效果的影響,測試結(jié)果如下:
分析熬的,對于復(fù)雜的歸約操作:
使用Stream并行歸約在單核情況下性能比串行歸約以及手動歸約都要差痊硕,簡單說就是最差的;
隨著使用核數(shù)的增加押框,Stream并行效果逐漸變好岔绸,多核帶來的效果明顯。
以上兩個實驗說明橡伞,對于復(fù)雜的歸約操作盒揉,Stream串行歸約效果好于手動歸約,在多核情況下兑徘,并行歸約效果更佳刚盈。我們有理由相信,對于其他復(fù)雜的操作挂脑,Stream API也能表現(xiàn)出相似的性能表現(xiàn)藕漱。
結(jié)論
上述三個實驗的結(jié)果可以總結(jié)如下:
對于簡單操作欲侮,比如最簡單的遍歷,Stream串行API性能明顯差于顯示迭代谴分,但并行的Stream API能夠發(fā)揮多核特性锈麸。
對于復(fù)雜操作,Stream串行API性能可以和手動實現(xiàn)的效果匹敵牺蹄,在并行執(zhí)行時Stream API效果遠超手動實現(xiàn)忘伞。
所以,如果出于性能考慮沙兰,1. 對于簡單操作推薦使用外部迭代手動實現(xiàn)氓奈,2. 對于復(fù)雜操作,推薦使用Stream API鼎天, 3. 在多核情況下舀奶,推薦使用并行Stream API來發(fā)揮多核優(yōu)勢,4.單核情況下不建議使用并行Stream API斋射。
如果出于代碼簡潔性考慮育勺,使用Stream API能夠?qū)懗龈痰拇a。即使是從性能方面說罗岖,盡可能的使用Stream API也另外一個優(yōu)勢涧至,那就是只要Java Stream類庫做了升級優(yōu)化,代碼不用做任何修改就能享受到升級帶來的好處桑包。