为什么在Java 9中使用流的这段代码比Java 8中运行得更快?

6
我在解决欧拉计划第205题时发现了这个问题。 问题如下:

Peter有9个四面体骰子,每个骰子的面数为1、2、3、4。 Colin有6个六面体骰子,每个骰子的面数为1、2、3、4、5、6。

Peter和Colin掷骰子并比较点数:最高点数获胜。 如果总分相等,则结果为平局。

Pyramidal Pete击败Cubic Colin的概率是多少? 将您的答案四舍五入为小数点后七位,格式为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)

3
如果你没有使用像JMH这样的微基准测试工具,那么你可能做错了,并且你对执行速度的结论可能是错误的。 - scottb
3
Java 9拥有许多JVM增强功能,例如改进的缓存和动态链接。 - Mick Mnemonic
2
“……但我不明白为什么在Java 9中,某些代码的运行速度比Java 8快一倍,这绝不仅仅是因为懒惰的基准测试。”- 仅仅因为你看不到原因,并不意味着它不可能发生。 - Stephen C
2
你是否反对更好的性能(无论是感知上还是实际上)? - Kevin Anderson
3
这里不适用于有关糟糕基准测试的争论,因为这段代码不是微基准测试——它是一个完整的独立应用程序,解决了特定的任务。因此,这里真正的问题是为什么在这个特定例子中JIT表现得如此奇怪。 - apangin
显示剩余2条评论
1个回答

20

1. 为什么JDK 9上流速度更快

Stream.count()在JDK 8中的实现相当愚蠢:它只是遍历整个流,对每个元素添加1L

这在JDK 9中被修复了。尽管错误报告指出了SIZED流,但新代码也改进了非SIZED流。

如果您用Java 8风格的实现.mapToLong(e -> 1L).sum()替换.count(),即使在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;
    }

在这种情况下,countLargerThan方法将正常编译,并且在JDK 8和JDK 9上的性能都比使用流更好。

SIZED流的count()方法有哪些改进? - Holger
4
有了一个专门的下沉操作(TerminalOp)的ReduceOps,现在流水线中没有中间映射操作,也没有额外的归约间接操作。 - apangin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接