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

bcs*_*001 3 java performance java-8 java-stream java-9

我在解决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");
    }
}
Run Code Online (Sandbox Code Playgroud)

我突出显示的代码使用了一个简单的代码filter(),count()并且sum()在Java 9中的运行速度似乎比Java 8快得多.具体来说,Java 8在我的机器上计算37465ms的解决方案.Java 9大约需要16000毫秒,无论是运行使用Java 8编译的文件还是使用Java 9编译的文件都是如此.

如果我用似乎是精确的前流等效替换流代码:

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

它在大约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)
Run Code Online (Sandbox Code Playgroud)

我的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)
Run Code Online (Sandbox Code Playgroud)

apa*_*gin 16

1.为什么流在JDK 9上运行得更快

Stream.count()JDK 8中的实现相当愚蠢:它只是1L为每个元素迭代整个流.

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

如果替换.count()为Java 8样式的实现.mapToLong(e -> 1L).sum(),即使在JDK 9上它也会再次变慢.

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;
    }
Run Code Online (Sandbox Code Playgroud)

在这种情况下,countLargerThan方法将被正常编译,并且性能将优于JDK 8和JDK 9上的流.

  • @Holger New [`TerminalOp`](http://hg.openjdk.java.net/jdk9/jdk9/jdk/file/65464a307408/src/java.base/share/classes/java/util/stream/ReduceOps.java #l236)带有专门的水槽.现在管道中没有中间映射操作,并且在reduce中没有额外的间接. (3认同)