Java8 Lambda表達式和流操作如何讓你的代碼變慢5倍


原文出處:ImportNew

有許許多多關於 Java 8 中流效率的討論,但根據 Alex Zhitnitsky 的測試結果顯示:堅持使用傳統的 Java 編程風格——iterator 和 for-each 循環——比 Java 8 的實現性能更佳。

Java 8 中的 Lambda 表達式和流(Stream)受到了熱烈歡迎。這是 Java 迄今為止最令人激動的特征。這些新的語言特征允許采用函數式風格來進行編碼,我們可以用這些特性完成許多有趣的功能。這些特性如此有趣以至於被認為是不合理的。我們對此表示懷疑,於是決定對這些特性進行測試。

我們創建一個簡單的任務:從一個 ArrayList 找出最大值,將傳統方式與 Java 8 中的新方式進行測試比較。說實話,測試的結果讓我感到非常驚訝。

命令式風格與 Java 8 函數式編程風格比較

我喜歡直接進入主題,所以先看一下結果。為了做這次基准測試,我們先創建了一個 ArrayList,並插入一個 100000 個隨機整數,並通過 7 種不同的方式遍歷所有的值來查找最大值。實現分為兩組:Java 8 中引入的函數式風格與 Java 一直使用的命令式風格。

這是每個方法耗費的時長:
functional.benchmark

最大錯誤記錄是並行流上的 0.042,完整輸出結果在這篇文章結尾部分可以看到。

小貼士:

哇哦!Java 8 中提供的任何一種新方式都會產生約 5 倍的性能差異。有時使用簡單迭代器循環比混合 lambda 表達式和流更有效,即便這樣需要多寫幾行代碼,且需要跳過甜蜜的語法糖(syntactic suger)。

使用迭代器或 for-each 循環是遍歷 ArrayList 最有效的方式,性能比采用索引值的傳統 for 循環方式好兩倍。

在 Java 8 的方法中,並行流的性能最佳。但是請小心,在某些情況下它也可能會導致程序運行得更慢。

Lambda 表達式的速度介於流與並行流之間。這個結果確實挺令人驚訝的,因為 lambda 表達式的實現方式是基於流的 API 來實現的。

不是所有的情況都如上所示:當我們想演示在 lambda 表達式和流中很容易犯錯時,我們收到了很多社區的反饋,要求我們優化基准測試代碼,如消除整數的自動裝包和解包操作。第二次測試(已優化)的結果在這篇文章結束位置可以看到。

讓我們快速看一下每個方法,按照運行速度由快到慢:

命令式風格

iteratorMaxInteger()——使用迭代器遍歷列表:

1
2
3
4
5
6
7
public int iteratorMaxInteger() {
     int max = Integer.MIN_VALUE;
     for (Iterator it = integers.iterator(); it.hasNext(); ) {
         max = Integer.max(max, it.next());
     }
     return max;
}

forEachLoopMaxInteger()——不使用迭代器,使用 For-Each 循環遍歷列表(不要誤用 Java 8 的 forEach)

1
2
3
4
5
6
7
public int forEachLoopMaxInteger() {
     int max = Integer.MIN_VALUE;
     for (Integer n : integers) {
         max = Integer.max(max, n);
     }
     return max;
}

forMaxInteger()——使用簡單的 for 循環和索引遍歷列表:

1
2
3
4
5
6
7
public int forMaxInteger() {
     int max = Integer.MIN_VALUE;
     for ( int i = 0 ; i < size; i++) {
         max = Integer.max(max, integers.get(i));
     }
     return max;
}

函數式風格

parallelStreamMaxInteger()——使用 Java 8 並行流遍歷列表:

1
2
3
4
public int parallelStreamMaxInteger() {
     Optional max = integers.parallelStream().reduce(Integer::max);
     return max.get();
}

lambdaMaxInteger()——使用 lambda 表達式及流遍歷列表。優雅的一行代碼:

1
2
3
public int lambdaMaxInteger() {
     return integers.stream().reduce(Integer.MIN_VALUE, (a, b) -> Integer.max(a, b));
}

forEachLambdaMaxInteger()——這個用例有點混亂。可能是因為 Java 8 的 forEach 特性有一個很煩人的東西:只能使用 final 變量,所以我們創建一個 final 包裝類來解決該問題,這樣我們就能訪問到更新后的最大值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int forEachLambdaMaxInteger() {
     final Wrapper wrapper = new Wrapper();
     wrapper.inner = Integer.MIN_VALUE;
 
     integers.forEach(i -> helper(i, wrapper));
     return wrapper.inner.intValue();
}
 
public static class Wrapper {
     public Integer inner;
}
 
private int helper( int i, Wrapper wrapper) {
     wrapper.inner = Math.max(i, wrapper.inner);
     return wrapper.inner;
}

順便提一下,如果要討論 forEach,我們提供了一些有趣的關於它的缺點的見解,答案參見StackOverflow

streamMaxInteger()——使用 Java 8 的流遍歷列表:

1
2
3
4
public int streamMaxInteger() {
     Optional max = integers.stream().reduce(Integer::max);
     return max.get();
}

優化后的基准測試

根據這篇文章的反饋,我們創建另一個版本的基准測試。源代碼的不同之處可以在這里查看。下面是測試結果:
remake

最后的思考

開始使用 Java 8 的第一件事情是在實踐中使用 lambda 表達式和流。但是請記住:它確實非常好,好到可能會讓你上癮!但是,我們也看到了,使用傳統迭代器和 for-each 循環的 Java 編程風格比 Java 8 中的新方式性能高很多。

當然,這也不是絕對的。但這確實是一個相當常見的例子,它顯示可能會有大約 5 倍的性能差距。如果這影響到系統的核心功能或成為系統一個新的瓶頸,那就相當可怕了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM