歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> Java8 Lambda表達式和流操作如何讓你的代碼變慢5倍

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

日期:2017/3/1 9:20:02   编辑:Linux編程

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

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

最大錯誤記錄是並行流上的 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(); }

優化後的基准測試

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

修改總結:

列表不再用 volatile 修飾。
新方法 forMax2 刪除對成員變量的訪問。
刪除 forEachLambda 中的冗余 helper 函數。現在 lambda 表達式作為一個值賦給變量。可讀性有所降低,但是速度更快。
消除自動裝箱。如果你在 Eclipse 中打開項目的自動裝箱警告,舊的代碼會有 15 處警告。
優化流代碼,在 reduce 前先使用 mapToInt。
非常感謝 Patrick Reinhart, Richard Warburton, Yan Bonnel, Sergey Kuksenko, Jeff Maxwell, Henrik Gustafsson 以及每個 Twitter 上評論的人,感謝你們的貢獻。

測試基礎

我們使用 JMH(Java Microbenchmarking Harness) 執行基准測試。如果想知道怎麼將其應用在你自己的項目中,可以參考這篇文章,我們通過一個自己寫的實例來演示 JMH 的主要特性。

基礎測試的配置包含 2 個JVM、5 次預熱迭代和 5 次測量迭代。該測試運行在 c3.xlarge Amazon EC2 實例上(CPU:4 核,內存:7.5G,存儲:2 x 40 GB SSD),采用 Java 8u66 和 JMH 1.11.2。所有的源代碼都在 GitHub 上,你可以在這裡看到原始的輸出結果。

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

最後的思考

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

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

Copyright © Linux教程網 All Rights Reserved