【问题标题】:Why does this code using streams run so much faster in Java 9 than Java 8?为什么使用流的这段代码在 Java 9 中的运行速度比 Java 8 快得多?
【发布时间】:2018-03-18 20:02:56
【问题描述】:

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

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

Peter 和 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 在我的机器上以 37465 毫秒计算解决方案。 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;
}

它在大约 35000 毫秒内计算解决方案,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)

【问题讨论】:

  • 如果您没有使用 JMH 等微基准测试工具,那么您做错了,您对执行速度的结论可能是错误的。
  • Java 9 有一堆JVM enhancements,比如改进的缓存和动态链接。
  • "... 但我看不出某些代码在 Java 9 中的运行速度是 Java 8 的两倍,这完全是由于延迟基准测试造成的。” -仅仅因为你看不到如何并不意味着它不会发生。
  • 是的。听起来像是基准测试问题,而不是实际的性能差异。
  • 关于糟糕的基准测试的论点在这里不适用,因为代码不是微基准测试——它是一个完整的独立应用程序,可以解决特定任务。所以,这里真正的问题是为什么 JIT 在这个特定的例子中表现得如此奇怪。

标签: java performance java-8 java-stream java-9


【解决方案1】:

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

Stream.count() 在 JDK 8 中的实现是 rather dumb:它只是遍历整个流,为每个元素添加 1L。

这是 JDK 9 中的 fixed。尽管错误报告中提到了 SIZED 流,但新代码也改进了非大小流。

如果将.count() 替换为Java 8 风格的实现.mapToLong(e -&gt; 1L).sum(),即使在JDK 9 上也会再次变慢。

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

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

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

如果将内部循环放在单独的方法中,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上的流。

【讨论】:

  • count() 在非 SIZED 流上做了哪些改进?
  • @Holger New TerminalOp 带有专门的水槽。现在管道中没有中间映射操作,reduce中也没有额外的间接性。
猜你喜欢
  • 2022-01-19
  • 2017-02-12
  • 1970-01-01
  • 1970-01-01
  • 2017-11-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多