与长期的实现相比,Java 8 lambda和流的性能如何?
Lambda表达式和流在Java 8中受到了热烈的欢迎。这些是迄今为止很激动人心的功能,很长一段时间以来,它们就已经应用到Java中了。 新的语言功能使我们可以在代码中采用更具功能性的样式,并且在其中玩耍也很有趣。 非常有趣,应该是非法的。 然后我们变得可疑 ,并决定对它们进行测试。
我们已经完成了一个简单的任务,即在ArrayList中找到最大值,并测试了长期的实现与Java 8中可用的新方法的对比。老实说,结果令人惊讶。
Java 8中的命令式与功能式编程
我们喜欢直截了当,所以让我们看一下结果。 对于此基准,我们创建了一个ArrayList,为其中填充了100,000个随机整数,并实现了7种不同的方式来遍历所有值以找到最大值。 这些实现分为两类:具有Java 8中引入的新语言功能的功能样式和具有长期Java方法的命令式样式。
这是每种方法花费的时间:
外卖
- 哎呀! 使用Java 8提供的任何新方法来实现解决方案,都会使性能下降5倍左右。 有时,使用带有迭代器的简单循环比将lambda和流混入混合要好。 即使这意味着编写更多代码行并跳过那种甜蜜的语法糖。
- 使用迭代器或for-each循环是遍历ArrayList的最有效方法。 比具有索引int的传统for循环好两倍。
- 在Java 8方法中,使用并行流被证明更有效。 但是要当心, 在某些情况下,它实际上可能会使您减速。
- Lambas取代了它们在流和parallelStream实现之间的位置。 这是令人惊讶的,因为它们的实现是基于流API的。
- [编辑]事情并非总是如此:尽管我们想展示在lambda和流中引入错误有多么容易,但我们收到了很多社区反馈,要求为基准代码添加更多优化并删除对它们的装箱/拆箱。整数。 包括优化在内的第二组结果可在本文的底部获得。
等一下,我们到底在这里测试了什么?
让我们快速浏览一下每种方法,从最快到最慢:
命令式
forMaxInteger() –使用简单的for循环和int索引遍历列表:
public int forMaxInteger() {
int max = Integer.MIN_VALUE;
for (int i = 0; i < size; i++) {
max = Integer.max(max, integers.get(i));
}
return max;
}
iteratorMaxInteger() –使用迭代器遍历列表:
public int iteratorMaxInteger() {
int max = Integer.MIN_VALUE;
for (Iterator<Integer> 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;
}
功能风格
parallelStreamMaxInteger() –在并行模式下使用Java 8流浏览列表:
public int parallelStreamMaxInteger() {
Optional<Integer> 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功能可能最令人讨厌的是它只能使用最终变量,因此我们为最终包装器类创建了一些变通方法,该类可以访问我们正在更新的最大值:
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<Integer> max = integers.stream().reduce(Integer::max);
return max.get();
}
优化基准
遵循本文的反馈意见,我们创建了基准的另一个版本。 与原始代码的所有差异都可以在此处查看 。 结果如下:
TL; DR:更改摘要
- 该列表不再易失。
- forMax2的新方法删除了字段访问。
- forEachLambda中的冗余帮助程序功能已修复。 现在,lambda也正在分配一个值。 可读性较差,但速度更快。
- 自动装箱消除了。 如果您在Eclipse中为项目打开自动装箱警告,则旧代码中有15条警告。
- 在reduce之前使用mapToInt修复流代码。
感谢Patrick Reinhart , Richard Warburton , Yan Bonnel , Sergey Kuksenko , Jeff Maxwell , Henrik Gustafsson以及所有在Twitter上发表评论的人!
基础
为了运行此基准测试,我们使用了Java Microbenchmarking Harness JMH。 如果您想了解更多有关如何在自己的项目中使用它的信息, 请查看这篇文章 ,我们将通过动手示例来了解它的一些主要功能。
基准配置包括JVM的2个分支,5个预热迭代和5个测量迭代。 这些测试是使用Java 8u66和JMH 1.11.2在c3.xlarge Amazon EC2实例(4个vCPU,7.5 Mem(GiB),2 x 40 GB SSD存储)上运行的。 完整的源代码可在GitHub上找到 ,您可以在此处查看原始结果输出。
话虽这么说,但有一点免责声明:基准往往非常危险,要正确地制定基准则非常困难。 尽管我们尝试以最准确的方式运行它,但始终建议您先花一点盐。
最后的想法
使用Java 8时,要做的第一件事是尝试使用lambda表达式和流。 但是要当心:感觉真的很好,很甜,所以您可能会上瘾! 我们已经看到,坚持使用迭代器和for-each循环的更传统的Java编程风格,明显优于Java 8提供的新实现。当然,情况并非总是如此,但是在这个非常常见的示例中,它表明可以大约差5倍。 如果它影响系统的核心部分或创建新的瓶颈,这会变得非常可怕。