使用Java 8 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行在随机日期生成随机温度。

如果要计算所有城市8月的平均气温,可以编写以下函数算法:

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行)通过创建所有温度的单个长列表来对数据进行扁平化,并在第3行上将其传递给lambda,从而返回每个以流的形式列出温度, flatMap方法可以将其附加在一起。 完成此操作后,我将在第4行使用filter方法丢弃所有非8月份以来的数据。 然后,我在第11行调用map方法,将每个Temperature对象转换为一个
BigDecimal以及生成的流,我在第13行使用了collect方法以及一个计算平均值的收集器。 第15行需要一个辅助函数来将BigDecimal实例转换为double ,因为第14行使用double而不是 BigDecimal

/** method to convert to double */
public static Double toDouble(BigDecimal a) {return a.doubleValue();
}

上面清单中的数字运算部分可以替代地以命令式方式编写,如下所示:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/363486.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

bind搭建(二)反向解析

我们在上一节已经知道了怎么建立DNS的服务器端&#xff0c;可以实现了域名到IP之间的转换。那么好我们现在就来了解一下如何实现反向的DNS解析&#xff0c;也就是IP到域名的映射。 步骤如下&#xff1a; l 在/etc/named中声明反向区域 l 在/var/named/chroot/var/named/中创建…

js求渐升数的第100位

我弟考了道数学竞赛题&#xff0c;问我能不能用代码算结果.. 题目是这样的 用 1、2、3、4、5 组合数字&#xff0c;然后排列大小&#xff0c;从小到大&#xff0c;求排在第100位的数值大小 function foo(chars) {var count 0;if (!chars.length) return;var _foo function(c…

[导入]商业智能2.0?(BI 2.0 from Timo Elliott)

译者注: 关于BI2.0的说法很多&#xff0c;不尽一致&#xff0c;目的只是想多了解一些&#xff1b;译文并不代表译者认可原文观点&#xff0c;只是顺便译了以方便不喜欢E文的朋友。本文是一篇充满探讨及疑问的文章&#xff0c;来自Timo Elliott(Business Objects历史上的第8号员…

poj 3259 Wormholes : spfa 双端队列优化 判负环 O(k*E)

1 /**2 problem: http://poj.org/problem?id32593 spfa判负环&#xff1a;4 当有个点被松弛了n次&#xff0c;则这个点必定为负环中的一个点&#xff08;n为点的个数&#xff09;5 spfa双端队列优化&#xff1a;6 维护队列使其dist小的点优先处理7 **/8 #include<stdio.h&g…

编写干净的测试–用特定领域的语言替换断言

很难为干净的代码找到一个好的定义&#xff0c;因为我们每个人都有自己的单词clean的定义。 但是&#xff0c;有一个似乎是通用的定义&#xff1a; 干净的代码易于阅读。 这可能会让您感到有些惊讶&#xff0c;但是我认为该定义也适用于测试代码。 使测试尽可能具有可读性是我…

如何让MFC程序关闭按钮失效,也无法右击任务栏关闭窗口来关闭?

如何让MFC程序关闭按钮失效&#xff0c;也无法右击任务栏关闭窗口来关闭&#xff0c;即右键任务栏的关闭窗口失效呢&#xff1f;很简单&#xff0c;有一个小窍门就是&#xff1a;响应IDCANCEL消息&#xff0c;具体实现如下&#xff1a; 首先定义消息映射&#xff1a;ON_BN_CLIC…

令人眼睛一亮的履历表

令人眼睛一亮的履历表 你辛辛苦苦写的一份简历&#xff0c;可在人事经理眼里最多只是停留几十秒的时间。如果时机拿捏不好&#xff0c;它会给你造成麻烦&#xff1a;它可能暴露你的短处&#xff0c;而且基本目的都是供人淘汰之用。然而&#xff0c;当你必须做出履历表时&#…

angularjs封装bootstrap官网的时间插件datetimepicker

背景:angular与jquery类库的协作 第三方类库中&#xff0c;不得不提的是大名鼎鼎的jquery,现在基本上已经是国内web开发的必修工具了。它灵活的dom操作&#xff0c;让很多web开发人员欲罢不能。再加上已经很成熟的jquery UI 库和大量jquery 插件&#xff0c;几乎是一个取之不尽…

Java中的得墨meter耳定律–最少知识原理–实际示例

得墨meter耳定律&#xff08;也称为最少知识定律&#xff09;是一种编码原理&#xff0c;它表示模块不应该知道其操作的对象的内部细节。 如果代码依赖于特定对象的内部细节&#xff0c;则很有可能一旦该对象的内部发生更改&#xff0c;它就会被破坏。 由于封装是关于隐藏对象的…

课后作业1

自我介绍 我叫张阔&#xff0c;我的爱好是旅行&#xff0c;游览世界的美好风光&#xff1b; 我的码云个人主页是&#xff1a;https://gitee.com/ZkTt0428&#xff1b; 我的第一个项目地址是&#xff1a;https://gitee.com/ZkTt0428/Frist&#xff1b; 目前代码量有10000行了&am…

记录6月28日的体验,自己现实的感触

2016年6月28日&#xff0c;是我自己要求的要去湖北的日子&#xff0c;可是现在&#xff0c;这个只能成为过去式&#xff0c;只能是提一提&#xff01; 2016年5月17日&#xff0c;我在想&#xff0c;我要通宵加班&#xff0c;做好自己最好&#xff0c;最期待完成的3.0&#xff0…

lucene索引

1。lucene的索引尽量不要频繁而小量的编制&#xff0c;比如&#xff1a;用户每发一个贴子&#xff0c;就加入索引&#xff0c;那样对索引的结构和效率不利。 可以采用定时或者定量&#xff0c;批量处理索引的方式。 2。在批量处理的基础上&#xff0c;解决冲突的问题的方案之一…

针对新手的Java EE7和Maven项目-第4部分-定义Ear模块

从前面的部分恢复 第1部分 第2部分 第3部分 我们正在恢复第四部分&#xff0c;目前我们的简单项目有 Web Maven模块&#xff08;战争&#xff09; 一个ejb模块&#xff08;ejb&#xff09;&#xff0c;其中包含我们的无状态会话bean&#xff08;EJB 3.1&#xff09; 第二…

合并两个有序数组,并输出中间值

示例1&#xff1a; nums1 [1,3] nums2 [2,4] output: (23) / 2 2.5 示例2&#xff1a; nums1 [2,5,7] nums2 [3,6] output:5 Python解决方案&#xff1a; def findMedianSortedArrays(self, nums1, nums2):""":type nums1: List[int]:type nums2: List[int…

Python中关于文件路径的简单操作 [转]

1: os.listdir(path) #path为目录 功能相当于在path目录下执行dir命令&#xff0c;返回为list类型 举例&#xff1a; print os.listdir(..) 输出&#xff1a; [a,b,c,d] 2: os.path.walk(path,visit,arg) path &#xff1a;是将要遍历的目录 visit &#xff1…

生产上完成TopN统计流程

背景 现有城市信息和产品信息两张表在MySQL中&#xff0c;另外有用户点击产品日志以文本形式存在hdfs上&#xff0c;现要求统计每个个城市区域下点击量前三的产品名&#xff0c;具体信息见下方。 mysql> show tables; --------------------------------- | Tables_in_d7 …

最大公因数和最小公倍数

一丶 最大公因数求法&#xff1a;辗转相除法(也称欧几里得算法)原理: 二丶最小公倍数求法&#xff1a;两个整数的最小公倍数等于两整数之积除以最大公约数1 #include <iostream>2 3 using namespace std;4 5 //辗转相除法(欧几里得算法)6 7 int gcd(int a, int b)8 {9…

css实现div内一段文本的两端对齐

在一个固定宽度的div内&#xff0c;使得P标签内的文本两端对齐&#xff1a; text-align: justify;text-justify:inter-ideograph; <!DOCTYPE html><html lang"en"><head><meta charset"UTF-8"><title>justify</title>…

JPA 2.1实体图–第2部分:在运行时定义延迟/急切加载

这是我关于JPA 2.1实体图的第二篇文章。 第一篇文章描述了命名实体图的用法。 这些可用于定义在编译时将使用查找或查询方法获取的实体和/或属性的图形。 动态实体图以相同的方式但以动态方式这样做。 这意味着您可以在运行时使用EntityGraph API定义实体图。 如果您错过了第一…

HDU1166-敌兵布阵

http://acm.hdu.edu.cn/showproblem.php?pid1166 线段树第一题 #include<cstdio> #define lson l,m,rt<<1 #define rson m1,r,rt<<1|1 const int maxn55555; int sum[maxn<<2]; void PushUP(int rt) {sum[rt]sum[rt<<1]sum[rt<<1|1]; } …