为什么使用流的代码在Java 9中的运行速度比Java 8快得多



我在解决Project Euler的问题205时发现了这一点。问题如下:

彼得有九个四边形(金字塔形)骰子,每个骰子的面编号为1、2、3、4。科林有六个六面(立方体)骰子,每个骰子的面编号为1、2、3、4、5、6。

彼得和科林掷骰子,比较总数:最高的总赢。如果总数相等,结果是平局。

金字塔皮特击败立方科林的概率是多少?以0.abcdefg 的形式将你的答案四舍五入到小数点后七位

我用Guava写了一个简单的解决方案:

import com.google.common.collect.Sets;
import com.google.common.collect.ImmutableSet;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
public class Problem205 {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
List<Integer> peter = Sets.cartesianProduct(Collections.nCopies(9, ImmutableSet.of(1, 2, 3, 4)))
.stream()
.map(l -> l
.stream()
.mapToInt(Integer::intValue)
.sum())
.collect(Collectors.toList());
List<Integer> colin = Sets.cartesianProduct(Collections.nCopies(6, ImmutableSet.of(1, 2, 3, 4, 5, 6)))
.stream()
.map(l -> l
.stream()
.mapToInt(Integer::intValue)
.sum())
.collect(Collectors.toList());
long startTime2 = System.currentTimeMillis();
// IMPORTANT BIT HERE! v
long solutions = peter
.stream()
.mapToLong(p -> colin
.stream()
.filter(c -> p > c)
.count())
.sum();
// IMPORTANT BIT HERE! ^
System.out.println("Counting solutions took " + (System.currentTimeMillis() - startTime2) + "ms");
System.out.println("Solution: " + BigDecimal
.valueOf(solutions)
.divide(BigDecimal
.valueOf((long) Math.pow(4, 9) * (long) Math.pow(6, 6)),
7,
RoundingMode.HALF_UP));
System.out.println("Found in: " + (System.currentTimeMillis() - startTime) + "ms");
}
}

我强调的代码使用了简单的filter()count()sum(),在Java 9中的运行速度似乎比Java 8快得多。具体来说,Java 8在我的机器上计算了37465ms的解决方案。Java 9在大约16000ms内完成,无论我运行用Java 8编译的文件还是用Java 9编译的文件,这都是一样的。

如果我用看起来完全等同于预流的东西替换流代码:

long solutions = 0;
for (Integer p : peter) {
long count = 0;
for (Integer c : colin) {
if (p > c) {
count++;
}
}
solutions += count;
}

它在大约35000ms内计算出解决方案,Java 8和Java 9之间没有可测量的差异。

我在这里错过了什么?为什么Java 9中的流代码速度如此之快,为什么for循环没有?


我运行的是Ubuntu 16.04 LTS 64位。我的Java 8版本:

java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

我的Java 9版本:

java version "9"
Java(TM) SE Runtime Environment (build 9+181)
Java HotSpot(TM) 64-Bit Server VM (build 9+181, mixed mode)

1。为什么流在JDK 9上工作得更快

Stream.count()实现在JDK8中相当愚蠢:它只是在整个流中迭代,为每个元素添加1L

这在JDK 9中得到了修复。尽管错误报告中提到了SIZED流,但新代码也改进了非大小流。

如果用Java 8风格的实现.mapToLong(e -> 1L).sum()替换.count(),那么即使在JDK9上,它也会再次变慢。

2.为什么天真循环运行缓慢

当您将所有代码都放在main方法中时,就无法有效地对其进行JIT编译。这个方法只执行一次,它开始在解释器中运行,然后,当JVM检测到热循环时,它从解释模式切换到动态编译模式。这被称为堆栈替换(OSR)。

OSR编译通常不如常规编译方法优化。我之前已经详细解释过了,看看这个和这个答案。

如果将内部循环放在一个单独的方法中,JIT将生成更好的代码:

long solutions = 0;
for (Integer p : peter) {
solutions += countLargerThan(colin, p);
}
...
private static int countLargerThan(List<Integer> colin, int p) {
int count = 0;
for (Integer c : colin) {
if (p > c) {
count++;
}
}
return count;
}

在这种情况下,countLargerThan方法将被正常编译,并且性能将优于JDK8和JDK9上的流。

最新更新