使用Java 8 Streams进行编程对算法性能的影响
多年來(lái),使用Java進(jìn)行多范例編程已經(jīng)成為可能,它支持面向服務(wù),面向?qū)ο蠛兔嫦蚍矫娴木幊痰幕旌稀?帶有l(wèi)ambda和java.util.stream.Stream類的Java 8是個(gè)好消息,因?yàn)樗刮覀兛梢詫⒐δ苄跃幊谭独砑拥交旌现小?確實(shí),lambda周圍有很多炒作。 但是,改變我們的習(xí)慣和編寫(xiě)代碼的方式是明智的選擇,而無(wú)需先了解可能隱患的危險(xiǎn)嗎?
Java 8的Stream類很簡(jiǎn)潔,因?yàn)樗鼓梢允占瘮?shù)據(jù)并將該數(shù)據(jù)上的多個(gè)功能調(diào)用鏈接在一起,從而使代碼整潔。 映射/歸約算法是一個(gè)很好的例子,您可以通過(guò)首先從復(fù)雜域中選擇或修改數(shù)據(jù)并對(duì)其進(jìn)行簡(jiǎn)化(“映射”部分),然后將其縮減為一個(gè)有用的值來(lái)收集數(shù)據(jù)并進(jìn)行匯總。
以以下數(shù)據(jù)類為例(用Groovy編寫(xiě),這樣我就可以免費(fèi)生成構(gòu)造函數(shù),訪問(wèn)器,哈希/等于和toString方法的代碼!):
//Groovy @Immutable class City {String nameList<Temperature> temperatures } @Immutable class Temperature {Date dateBigDecimal reading }我可以使用這些類在“ City對(duì)象列表中構(gòu)造一些隨機(jī)天氣數(shù)據(jù),例如:
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行使用同樣來(lái)自Java 8的IntStream類來(lái)構(gòu)造第8-13行進(jìn)行迭代的范圍,從而將新的城市添加到第6行中構(gòu)建的列表中。第22-30行在隨機(jī)日期生成隨機(jī)溫度。
如果要計(jì)算所有城市8月的平均氣溫,可以編寫(xiě)以下函數(shù)算法:
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行用于啟動(dòng)時(shí)鐘。 然后,代碼在第2行上從城市列表中創(chuàng)建一個(gè)流。然后,我使用flatMap方法(也在第2行)通過(guò)創(chuàng)建所有溫度的單個(gè)長(zhǎng)列表來(lái)對(duì)數(shù)據(jù)進(jìn)行扁平化,并在第3行上將其傳遞給lambda,從而返回每個(gè)以流的形式列出溫度, flatMap方法可以將其附加在一起。 完成此操作后,我將在第4行使用filter方法丟棄所有非8月份以來(lái)的數(shù)據(jù)。 然后,我在第11行調(diào)用map方法,將每個(gè)Temperature對(duì)象轉(zhuǎn)換為一個(gè)
BigDecimal以及生成的流,我在第13行使用了collect方法以及一個(gè)計(jì)算平均值的收集器。 第15行需要一個(gè)輔助函數(shù)來(lái)將BigDecimal實(shí)例轉(zhuǎn)換為double ,因?yàn)榈?4行使用double而不是 BigDecimal :
上面清單中的數(shù)字運(yùn)算部分可以替代地以命令式方式編寫(xiě),如下所示:
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;在命令式的命令式版本中,我以不同的順序進(jìn)行映射,過(guò)濾和歸約,但是結(jié)果是相同的。 您認(rèn)為哪種風(fēng)格(功能性或命令性)更快,并且提高了多少?
為了更準(zhǔn)確地讀取性能數(shù)據(jù),我需要多次運(yùn)行算法,以便熱點(diǎn)編譯器有時(shí)間進(jìn)行預(yù)熱。 我以偽隨機(jī)順序多次運(yùn)行算法,我能夠測(cè)量出以功能樣式編寫(xiě)的代碼平均大約需要0.93秒(使用一千個(gè)城市,每個(gè)城市的溫度為一千;使用英特爾筆記本電腦進(jìn)行計(jì)算i5 2.40GHz 64位處理器(4核)。 以命令式方式編寫(xiě)的代碼花費(fèi)了0.70秒,速度提高了25%。
所以我問(wèn)自己,命令式代碼是否總是比功能代碼更快。 讓我們嘗試簡(jiǎn)單地計(jì)算8月記錄的溫度數(shù)。 功能代碼如下所示:
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();功能代碼涉及過(guò)濾,然后調(diào)用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++;}} }在此示例中,運(yùn)行的數(shù)據(jù)集與用于計(jì)算平均8月溫度的數(shù)據(jù)集不同,命令性代碼的平均時(shí)間為1.80秒,而功能代碼的平均時(shí)間略短。 因此,我們無(wú)法推斷出功能性代碼比命令性代碼更快或更慢。 這實(shí)際上取決于用例。 有趣的是,我們可以使用parallelStream()方法而不是stream()方法來(lái)使計(jì)算并行運(yùn)行。 在計(jì)算平均溫度的情況下,使用并行流意味著計(jì)算平均時(shí)間為0.46秒而不是0.93秒。 并行計(jì)算溫度需要0.90秒,而不是連續(xù)1.80秒。 嘗試編寫(xiě)命令式代碼,該命令式命令將數(shù)據(jù)分割,在內(nèi)核之間分布計(jì)算并將結(jié)果匯??總為一個(gè)平均溫度,這將需要大量工作! 正是這是想要向Java 8中添加函數(shù)式編程的主要原因之一。它如何工作? 拆分器和完成器用于在默認(rèn)的ForkJoinPool中分發(fā)工作,默認(rèn)情況下,該ForkJoinPool已優(yōu)化為使用與內(nèi)核一樣多的線程。 從理論上講,僅使用與內(nèi)核一樣多的線程就意味著不會(huì)浪費(fèi)時(shí)間進(jìn)行上下文切換,但這取決于所完成的工作是否包含任何阻塞的I / O –這就是我在有關(guān)Scala的書(shū)中所討論的。
在使用Java EE應(yīng)用程序服務(wù)器時(shí),生成線程是一個(gè)有趣的主題,因?yàn)閲?yán)格來(lái)說(shuō),不允許您生成線程。 但是由于創(chuàng)建并行流不會(huì)產(chǎn)生任何線程,因此無(wú)需擔(dān)心! 在Java EE環(huán)境中,使用并行流完全合法!
您也可以使用地圖/減少算法來(lái)計(jì)算8月的溫度總數(shù):
int count = cities.stream().map(c ->c.getTemperatures().size() ).reduce(Integer::sum ).get();第1行從列表中創(chuàng)建流,并使用第2行的lambda將城市映射(轉(zhuǎn)換)為城市的溫度數(shù)量。第3行通過(guò)使用總和將“溫度數(shù)量”流減少為單個(gè)值第4行上的Integer類的method。由于流可能不包含任何元素, reduce方法返回Optional ,我們調(diào)用get方法來(lái)獲取總數(shù)。 我們可以安全地這樣做,因?yàn)槲覀冎莱鞘兄邪瑪?shù)據(jù)。 如果您正在使用可能為空的數(shù)據(jù),則可以調(diào)用orElse(T)方法,該方法允許您指定默認(rèn)值(如果沒(méi)有可用結(jié)果時(shí)使用)。
就編寫(xiě)功能代碼而言,還有另一種編寫(xiě)此算法的方法:
long count = cities.stream().map(c ->c.getTemperatures().stream().count() ).reduce(Long::sum ).get();使用上述方法,第2行上的lambda通過(guò)將溫度列表轉(zhuǎn)換為蒸汽并調(diào)用count方法來(lái)count溫度列表的大小。 就性能而言, 這是獲取列表大小的一種不好的方法。 在每個(gè)城市有1000個(gè)城市和1000個(gè)溫度的情況下,使用第一種算法在160毫秒內(nèi)計(jì)算了總數(shù)。 第二種算法將時(shí)間增加到280ms! 原因是ArrayList知道其大小,因?yàn)樗谔砑踊騽h除元素時(shí)對(duì)其進(jìn)行跟蹤。 另一方面,流首先通過(guò)將每個(gè)元素映射到值1L ,然后使用Long::sum方法減少1L的流來(lái)計(jì)算大小。 在較長(zhǎng)的數(shù)據(jù)列表上,與僅從列表中的屬性查找大小相比,這是相當(dāng)大的開(kāi)銷。
將功能代碼所需的時(shí)間與以下命令代碼所需的時(shí)間進(jìn)行比較,可以看出該功能代碼的運(yùn)行速度慢了一倍–命令代碼計(jì)算的平均溫度總數(shù)僅為80ms。
long count = 0; for(City c : cities){count += c.getTemperatures().size(); }通過(guò)使用并行流而不是順序流,再次通過(guò)在上面的三個(gè)清單中簡(jiǎn)單地在第1行上調(diào)用parallelStream()方法而不是stream()方法,導(dǎo)致該算法平均需要90毫秒,即比命令性代碼略長(zhǎng)。
計(jì)算溫度的第三種方法是使用收集器 。 在這里,我使用了一百萬(wàn)個(gè)城市,每個(gè)城市只有兩個(gè)溫度。 該算法是:
int count = cities.stream().collect(Collectors.summingInt(c -> c.getTemperatures().size()) );等效的命令性代碼為:
long count = 0; for(City c : cities){count += c.getTemperatures().size(); }平均而言,功能性列表花費(fèi)了100毫秒,這與命令性列表花費(fèi)的時(shí)間相同。 另一方面,使用并行流將計(jì)算時(shí)間減少了一半,僅為50ms。
我問(wèn)自己的下一個(gè)問(wèn)題是,是否有可能確定需要處理多少數(shù)據(jù),因此使用并行流值得嗎? 拆分?jǐn)?shù)據(jù),將其提交給ForkJoinPool類的ExecutorService并在計(jì)算后將結(jié)果匯總在一起并不是免費(fèi)的-它會(huì)降低性能。 當(dāng)可以并行處理數(shù)據(jù)時(shí),當(dāng)然可以進(jìn)行計(jì)算,答案通常是取決于用例。
在此實(shí)驗(yàn)中,我計(jì)算了一個(gè)數(shù)字列表的平均值。 我NUM_RUNS地重復(fù)工作( NUM_RUNS次),以獲得可測(cè)量的值,因?yàn)橛?jì)算三個(gè)數(shù)字的平均值太快了,無(wú)法可靠地進(jìn)行測(cè)量。 我將列表的大小從3個(gè)數(shù)字更改為3百萬(wàn)個(gè),以確定列表需要多大才能使用并行流計(jì)算平均值才能得到回報(bào)。
使用的算法是:
double avg = -1.0; for(int i = 0; i < NUM_RUNS; i++){avg = numbers.stream().collect(Collectors.averagingInt(n->n)); }只是為了好玩,這是另一種計(jì)算方法:
double avg = -1.0; for(int i = 0; i < NUM_RUNS; i++){avg = numbers.stream().mapToInt(n->n).average().getAsDouble(); }結(jié)果如下。 僅使用列表中的三個(gè)數(shù)字,我就進(jìn)行了100,000次計(jì)算。 多次運(yùn)行測(cè)試表明,平均而言,串行計(jì)算花費(fèi)了20ms,而并行計(jì)算則花費(fèi)了370ms。 因此,在這種情況下,使用少量數(shù)據(jù)樣本,不值得使用并行流。
另一方面,列表中有300萬(wàn)個(gè)數(shù)字,串行計(jì)算花費(fèi)了1.58秒,而并行計(jì)算僅花費(fèi)了0.93秒。 因此,在這種情況下,對(duì)于大量數(shù)據(jù)樣本,值得使用并行流。 請(qǐng)注意,隨著數(shù)據(jù)集大小的增加,運(yùn)行次數(shù)減少了,因此我不必等待很長(zhǎng)的時(shí)間(我不喝咖啡!)。
| 列表中的#個(gè)數(shù)字 | 平均 時(shí)間序列 | 平均 時(shí)間平行 | 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萬(wàn) | 1.71秒 | 1.98秒 | 1,000 |
| 3,000,000 | 1.58秒 | 0.93秒 | 100 |
這是否意味著并行流僅對(duì)大型數(shù)據(jù)集有用? 沒(méi)有! 這完全取決于手頭的計(jì)算強(qiáng)度。 下面的無(wú)效算法只是加熱CPU,但演示了復(fù)雜的計(jì)算。
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); }我們可以使用以下清單生成兩個(gè)可運(yùn)行對(duì)象的列表,它們將完成這項(xiàng)繁重的工作:
private List<Runnable> generateRunnables() {Runnable r = () -> {doIntensiveWork();};return Arrays.asList(r, r); }最后,我們可以測(cè)量運(yùn)行兩個(gè)可運(yùn)行對(duì)象所花費(fèi)的時(shí)間,例如,并行運(yùn)行(請(qǐng)參見(jiàn)第3行對(duì)parallelStream()方法的調(diào)用):
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));使用并行流平均要花費(fèi)260毫秒來(lái)完成兩次密集工作。 使用串行流,平均耗時(shí)460毫秒,即幾乎翻倍。
從所有這些實(shí)驗(yàn)中我們可以得出什么結(jié)論? 好吧,不可能最終說(shuō)出功能代碼比命令性代碼慢,也不能說(shuō)使用并行流比使用串行流快。 我們可以得出的結(jié)論是,程序員在編寫(xiě)對(duì)性能至關(guān)重要的代碼時(shí),需要嘗試不同的解決方案并測(cè)量編碼風(fēng)格對(duì)性能的影響。 但是說(shuō)實(shí)話,這不是什么新鮮事! 對(duì)我來(lái)說(shuō),閱讀這篇文章后,您應(yīng)該帶走的是,總是有很多方法可以編寫(xiě)算法,并且選擇正確的方法很重要。 知道哪種方法是對(duì)的,這是經(jīng)驗(yàn)的結(jié)合,但更重要的是,嘗試使用代碼并嘗試不同的解決方案。 最后,盡管如此,還是不??要過(guò)早地進(jìn)行優(yōu)化!
翻譯自: https://www.javacodegeeks.com/2014/05/the-effects-of-programming-with-java-8-streams-on-algorithm-performance.html
總結(jié)
以上是生活随笔為你收集整理的使用Java 8 Streams进行编程对算法性能的影响的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 使用LinkedHashMap的Code
- 下一篇: 部落冲突华为版电脑(部落冲突华为最新版)