java8 streams_使用Java 8 Streams进行编程对算法性能的影响
java8 streams
多年來,使用Java進行多范式編程已經成為可能,它支持面向服務,面向對象和面向方面的編程的混合。 帶有lambda和java.util.stream.Stream類的Java 8是個好消息,因為它使我們可以將功能性編程范例添加到混合中。 確實,lambda周圍有很多炒作。 但是,改變我們的習慣和編寫代碼的方式是明智的選擇,而無需先了解可能隱患的危險嗎?
Java 8的Stream類很簡潔,因為它使您可以收集數據并將該數據上的多個功能調用鏈接在一起,從而使代碼整潔。 映射/歸約算法是一個很好的例子,您可以通過首先從復雜域中選擇或修改數據并對其進行簡化(“映射”部分),然后將其縮減為一個有用的值來收集數據并將其聚合。
以以下數據類為例(用Groovy編寫,這樣我就可以免費生成構造函數,訪問器,哈希/等于和toString方法的代碼!):
//Groovy @Immutable class City {String nameList<Temperature> temperatures } @Immutable class Temperature {Date dateBigDecimal reading }我可以使用這些類在City對象列表中構造一些隨機天氣數據,例如:
private static final long ONE_DAY_MS = 1000*60*60*24; private static final Random RANDOM = new Random();public static List<City> prepareData(int numCities, int numTemps) {List<City> cities = new ArrayList<>();IntStream.range(0, numCities).forEach( i ->cities.add(new City(generateName(), generateTemperatures(numTemps))));return cities; }private static List<Temperature> generateTemperatures(int numTemps) {List<Temperature> temps = new ArrayList<>();for(int i = 0; i < numTemps; i++){long when = System.currentTimeMillis();when += ONE_DAY_MS*RANDOM.nextInt(365);Date d = new Date(when);Temperature t = new Temperature(d, new BigDecimal(RANDOM.nextDouble()));temps.add(t);}return temps; }private static String generateName() {char[] chars = new char[RANDOM.nextInt(5)+5];for(int i = 0; i < chars.length; i++){chars[i] = (char)(RANDOM.nextInt(26) + 65);}return new String(chars); }第7行使用同樣來自Java 8的IntStream類來構造第8-13行進行迭代的范圍,從而將新的城市添加到第6行構建的列表中。第22-30行在隨機的日期生成隨機溫度。
如果要計算所有城市在八月記錄的平均溫度,則可以編寫以下函數算法:
Instant start = Instant.now(); Double averageTemperature = cities.stream().flatMap(c ->c.getTemperatures().stream() ).filter(t -> {LocalDate ld = LocalDateTime.ofEpochSecond(t.getDate().getTime(), 0, ZoneOffset.UTC).toLocalDate();return ld.getMonth() == Month.AUGUST; }).map(t ->t.getReading() ).collect(Collectors.averagingDouble(TestFilterMapReducePerformance::toDouble) );Instant end = Instant.now(); System.out.println("functional calculated in " + Duration.between(start, end) + ": " + averageTemperature); 第1行用于啟動時鐘。 然后,代碼在第2行從城市列表中創建一個流。然后,我使用flatMap方法(也在第2行)通過創建所有溫度的單個長列表來flatMap ,并在第3行傳遞一個lambda,該lambda返回每個以流的形式列出溫度, flatMap方法可以將其附加在一起。 完成此操作后,我將在第4行使用filter方法丟棄所有非8月份以來的數據。 然后,我在第11行調用map方法,將每個Temperature對象轉換為一個
BigDecimal以及生成的流,我在第13行使用了collect方法以及一個計算平均值的收集器。 第15行需要一個輔助函數來將BigDecimal實例轉換為double ,因為第14行使用double而不是
BigDecimal :
上面清單中的數字運算部分可以用命令式編寫,如下所示:
BigDecimal total = BigDecimal.ZERO; int count = 0; for(City c : cities){for(Temperature t : c.getTemperatures()){LocalDate ld = LocalDateTime.ofEpochSecond(t.getDate().getTime(), 0, ZoneOffset.UTC).toLocalDate();if(ld.getMonth() == Month.AUGUST){total = total.add(t.getReading());count++;}} } double averageTemperature = total.doubleValue() / count;在命令式的命令式版本中,我以不同的順序進行映射,過濾和歸約,但是結果是相同的。 您認為哪種風格(功能性或命令性)更快,并且提高了多少?
為了更準確地讀取性能數據,我需要多次運行算法,以便熱點編譯器有時間進行預熱。 以偽隨機順序多次運行算法,我能夠測量出以功能樣式編寫的代碼平均大約需要0.93秒(使用一千個城市,每個城市的溫度為一千;使用英特爾筆記本電腦進行計算i5 2.40GHz 64位處理器(4核)。 以命令式風格編寫的代碼花費了0.70秒,速度提高了25%。
所以我問自己,命令式代碼是否總是比功能代碼更快。 讓我們嘗試簡單地計算8月記錄的溫度數。 功能代碼如下所示:
long count = cities.stream().flatMap(c ->c.getTemperatures().stream() ).filter(t -> {LocalDate ld = LocalDateTime.ofEpochSecond(t.getDate().getTime(), 0, ZoneOffset.UTC).toLocalDate();return ld.getMonth() == Month.AUGUST; }).count();功能代碼涉及過濾,然后調用count方法。 另外,等效的命令性代碼可能如下所示:
long count = 0; for(City c : cities){for(Temperature t : c.getTemperatures()){LocalDate ld = LocalDateTime.ofEpochSecond(t.getDate().getTime(), 0, ZoneOffset.UTC).toLocalDate();if(ld.getMonth() == Month.AUGUST){count++;}} }在此示例中,運行的數據集與用于計算平均8月溫度的數據集不同,命令性代碼的平均時間為1.80秒,而功能代碼的平均時間略短。 因此,我們無法推斷出功能性代碼比命令性代碼更快或更慢。 這實際上取決于用例。 有趣的是,我們可以使用parallelStream()方法而不是stream()方法來使計算并行運行。 在計算平均溫度的情況下,使用并行流意味著計算平均時間為0.46秒而不是0.93秒。 并行計算溫度需要0.90秒,而不是連續1.80秒。 嘗試編寫命令式代碼,該命令將數據分割,在內核之間分布計算并將結果匯??總為一個平均溫度,這將需要大量工作! 正是這是想要向Java 8中添加函數式編程的主要原因之一。它如何工作? 拆分器和完成器用于在默認的ForkJoinPool中分發工作,默認情況下,該ForkJoinPool已優化為使用與內核一樣多的線程。 從理論上講,只使用與內核一樣多的線程就意味著不會浪費任何時間進行上下文切換,但這取決于所完成的工作是否包含任何阻塞的I / O –這就是我在有關Scala的書中所討論的。
在使用Java EE應用程序服務器時,生成線程是一個有趣的主題,因為嚴格來說,不允許您生成線程。 但是由于創建并行流不會產生任何線程,因此無需擔心! 在Java EE環境中,使用并行流完全合法!
您也可以使用地圖/減少算法來計算8月的溫度總數:
int count = cities.stream().map(c ->c.getTemperatures().size() ).reduce(Integer::sum ).get();第1行從列表中創建流,并使用第2行上的lambda將城市映射(轉換)為城市的溫度數。第3行通過使用總和將“溫度數”流減少為單個值第4行上的Integer類的method。由于流可能不包含任何元素, reduce方法返回Optional ,我們調用get方法獲取總數。 我們可以安全地這樣做,因為我們知道城市中包含數據。 如果您正在使用可能為空的數據,則可以調用orElse(T)方法,該方法允許您指定默認值(如果沒有可用結果時使用)。
就編寫功能代碼而言,還有另一種編寫此算法的方法:
long count = cities.stream().map(c ->c.getTemperatures().stream().count() ).reduce(Long::sum ).get();使用上述方法,第2行上的lambda通過將溫度列表轉換為蒸汽并調用count方法來count溫度列表的大小。 就性能而言, 這是獲取列表大小的一種不好的方法。 在第一個算法中,每個城市有1000個城市,溫度有1000個,總計數在160毫秒內計算。 第二種算法將時間增加到280ms! 原因是ArrayList知道其大小,因為它在添加或刪除元素時對其進行跟蹤。 另一方面,流首先通過將每個元素映射到值1L ,然后使用Long::sum方法減少1L的流來計算大小。 在較長的數據列表上,與僅從列表中的屬性查找大小相比,這是相當大的開銷。
將功能代碼所需的時間與以下命令代碼所需的時間進行比較,可以看出該功能代碼的運行速度慢了一倍–命令代碼計算的平均溫度總數僅為80ms。
long count = 0; for(City c : cities){count += c.getTemperatures().size(); }通過使用并行流而不是順序流,再次通過簡單地在上面三個清單中的第1行上調用parallelStream()方法而不是stream()方法,結果是該算法平均需要90毫秒,即比命令性代碼略長。
計算溫度的第三種方法是使用收集器 。 在這里,我使用了一百萬個城市,每個城市只有兩個溫度。 該算法是:
int count = cities.stream().collect(Collectors.summingInt(c -> c.getTemperatures().size()) );等效的命令性代碼為:
long count = 0; for(City c : cities){count += c.getTemperatures().size(); }平均而言,功能性列表花費了100毫秒,這與命令性列表花費的時間相同。 另一方面,使用并行流將計算時間減少了一半,僅為50ms。
我問自己的下一個問題是,是否有可能確定需要處理多少數據,因此使用并行流值得嗎? 拆分數據,將其提交給ForkJoinPool類的ExecutorService并在計算后將結果收集在一起并不是免費的-它會降低性能。 當可以并行處理數據時,肯定可以計算出來,通常的答案是,這取決于用例。
在此實驗中,我計算了一個數字列表的平均值。 我NUM_RUNS地重復工作( NUM_RUNS次),以獲得可測量的值,因為計算三個數字的平均值太快了,無法可靠地進行測量。 我將列表的大小從3個數字更改為3百萬個,以確定列表需要多大才能使用并行流計算平均值才能得到回報。
使用的算法是:
double avg = -1.0; for(int i = 0; i < NUM_RUNS; i++){avg = numbers.stream().collect(Collectors.averagingInt(n->n)); }只是為了好玩,這是另一種計算方法:
double avg = -1.0; for(int i = 0; i < NUM_RUNS; i++){avg = numbers.stream().mapToInt(n->n).average().getAsDouble(); }結果如下。 僅使用列表中的三個數字,我就運行了100,000次計算。 多次運行測試表明,平均而言,串行計算花費了20ms,而并行計算則花費了370ms。 因此,在這種情況下,使用少量數據樣本,不值得使用并行流。
另一方面,列表中有300萬個數字,串行計算花費了1.58秒,而并行計算僅花費了0.93秒。 因此,在這種情況下,對于大量數據樣本,值得使用并行流。 請注意,隨著數據集大小的增加,運行次數減少了,因此我不必等待很長的時間(我不喝咖啡!)。
| 列表中的#個數字 | 平均 時間序列 | 平均 時間平行 | NUM_RUNS |
| 3 | 0.02秒 | 0.37秒 | 100,000 |
| 30 | 0.02秒 | 0.46秒 | 100,000 |
| 300 | 0.07秒 | 0.53秒 | 100,000 |
| 3,000 | 1.98秒 | 2.76秒 | 100,000 |
| 30,000 | 0.67秒 | 1.90秒 | 10,000 |
| 30萬 | 1.71秒 | 1.98秒 | 1,000 |
| 3,000,000 | 1.58秒 | 0.93秒 | 100 |
這是否意味著并行流僅對大型數據集有用? 沒有! 這完全取決于手頭的計算強度。 以下無效的算法只是加熱CPU,但演示了復雜的計算。
private void doIntensiveWork() {double a = Math.PI;for(int i = 0; i < 100; i++){for(int j = 0; j < 1000; j++){for(int k = 0; k < 100; k++){a = Math.sqrt(a+1);a *= a;}}}System.out.println(a); }我們可以使用以下清單生成兩個可運行對象的列表,它們將完成這項繁重的工作:
private List<Runnable> generateRunnables() {Runnable r = () -> {doIntensiveWork();};return Arrays.asList(r, r); }最后,我們可以測量運行兩個可運行對象的時間,例如,并行運行(請參見第3行對parallelStream()方法的調用):
List<Runnable> runnables = generateRunnables(); Instant start = Instant.now(); runnables.parallelStream().forEach(r -> r.run()); Instant end = Instant.now(); System.out.println("functional parallel calculated in " + Duration.between(start, end));使用并行流平均要花費260毫秒來完成兩次密集的工作。 使用串行流,平均花費460毫秒,即幾乎翻倍。
從所有這些實驗中我們可以得出什么結論? 好吧,不可能最終說出功能代碼比命令性代碼慢,也不能說使用并行流比使用串行流快。 我們可以得出的結論是,程序員在編寫對性能至關重要的代碼時,需要試驗不同的解決方案并測量編碼風格對性能的影響。 但是說實話,這不是什么新鮮事! 對我而言,閱讀本文后您應該帶走的是,總是有很多方法可以編寫算法,并且選擇正確的方法很重要。 知道哪種方法是對的,這是經驗的結合,但更重要的是,嘗試使用代碼并嘗試不同的解決方案。 最后,盡管如此,還是不??要過早地進行優化!
翻譯自: https://www.javacodegeeks.com/2014/05/the-effects-of-programming-with-java-8-streams-on-algorithm-performance.html
java8 streams
總結
以上是生活随笔為你收集整理的java8 streams_使用Java 8 Streams进行编程对算法性能的影响的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 印章备案资料(印章备案档)
- 下一篇: 腾讯云被ddos流量怎么算的(腾讯云被d