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

有許許多多關於 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 一直使用的命令式風格。

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

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

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

小貼士:

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

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

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

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

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

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

命令式風格

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

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)

public int forEachLoopMaxInteger() {
int max = Integer.MIN_VALUE;
for (Integer n : integers) {
max = Integer.max(max, n);
}
return max;
}

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

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 並行流遍歷列表:

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

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

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

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

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 的流遍歷列表:

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

優化後的基準測試

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

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

修改總結:

列表不再用 volatile 修飾。

新方法 forMax2 刪除對成員變量的訪問。

刪除 forEachLambda 中的冗餘 helper 函數。現在 lambda 表達式作為一個值賦給變量。可讀性有所降低,但是速度更快。

消除自動裝箱。如果你在 Eclipse 中打開項目的自動裝箱警告,舊的代碼會有 15 處警告。

優化流代碼,在 reduce 前先使用 mapToInt。

順便做一下免責申明:基準測試往往不是完全可信的,也很難保證絕對正確。雖然我們試圖以最準確的方式來運行,但仍然建議接受結果時抱有懷疑的態度。

最後的思考

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

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


分享到:


相關文章: