Java 8顺序流有直接或间接的性能优势吗?

时间:2019-01-17 14:56:06

标签: java java-8

在浏览顺序流的文章时,我想到的问题是,与传统的for循环相比,使用顺序流有什么性能上的好处,或者流只是顺序语法糖,并且具有额外的性能开销?

考虑以下示例,在该示例中我看不到使用顺序流的任何性能优势:

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("a");
})
    .forEach(s -> System.out.println("forEach: " + s));

使用经典Java:

String[] strings = {"d2", "a2", "b1", "b3", "c"};
        for (String s : strings)
        {
            System.out.println("Before filtering: " + s);
            if (s.startsWith("a"))
            {
                System.out.println("After Filtering: " + s);
            }
        }

要点这是在流中,仅在完成对d2的所有操作后才开始对a2进行处理(我早先以为,在由foreach处理d2时,filter会开始对a2进行操作,但这不是本文所述的情况:https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/),与经典Java相同,因此使用超出“表现力”和“优雅”编码风格的流应该是什么动机?我知道编译器在处理流时会有性能开销,有人吗?知道/曾经体验过使用顺序流带来的任何性能优势?

4 个答案:

答案 0 :(得分:3)

首先,让一些特殊情况,例如省略多余的sorted操作或返回count()上的已知大小,操作的时间复杂度通常不会改变,因此所有差异在执行时间上通常大约是一个恒定的偏移量或一个(相当小的)因素,而不是基本的变化。


您总是可以编写一个与Stream内部实现基本相同的手动循环。因此,this answer提到的内部优化总是会被“但我可以在循环中做同样的事情”抛弃。

但是……当我们将“流”与“循环”进行比较时,假设针对特定用例以最有效的方式编写所有手动循环是否真的合理?无论调用代码作者的经验水平如何,特定的Stream实现都会将其优化应用于所有适用的情况。我已经看到循环丢失了短路或执行特定用例不需要的冗余操作的机会。

另一方面是执行某些优化所需的信息。流API围绕Spliterator接口构建,该接口可以提供源数据的特征,例如它允许找出数据是否具有有意义的顺序(某些操作需要保留),或者是否已经按照自然顺序或使用特定的比较器进行了预排序。当可以预测时,它还可以提供预期的元素数量,作为估计值或精确值。

一种接收任意Collection的方法以实现具有普通循环的算法,将很难找出是否存在这样的特征。 List表示有意义的顺序,而Set通常没有意义,除非它是SortedSetLinkedHashSet,而后者是特定的实现类,而不是接口。因此,针对所有已知的星座进行测试可能仍会错过第三方协议的实现,而第三方协议无法通过预定义的界面来表达。

当然,从Java 8开始,您可以自己获取Spliterator来检查这些特征,但这将使您的循环解决方案变成一件不平凡的事情,并且还意味着重复使用Stream已经完成的工作API。


基于Spliterator的Stream解决方案与常规循环之间还有另一个有趣的区别,当迭代数组以外的其他内容时使用Iterator。模式是在迭代器上调用hasNext,然后调用next,除非hasNext返回了false。但是Iterator的合同没有强制采用这种模式。在已知成功的情况下(例如,您已经知道集合的大小),调用者可以调用next而不使用hasNext,甚至多次。另外,如果呼叫者不记得上一次呼叫的结果,则呼叫者可以多次调用hasNext而不使用next

结果,Iterator实现必须执行冗余操作,例如有效地检查了两次循环条件,一次在hasNext中,返回一个boolean,一次在next中,当不满足时,抛出一个NoSuchElementException。通常,hasNext必须执行实际的遍历操作并将结果存储到Iterator实例中,以确保结果保持有效,直到后续的next调用为止。 next操作又必须检查是否已经发生了这种遍历或是否必须自己执行该操作。实际上,热点优化程序可能消除也可能不会消除Iterator设计带来的开销。

相反,Spliterator有一个遍历方法boolean tryAdvance(Consumer<? super T> action),它执行实际的操作返回是否有元素。这大大简化了循环逻辑。甚至还有void forEachRemaining(Consumer<? super T> action)用于非短路操作,这允许实际的实现提供整个循环逻辑。例如,在ArrayList的情况下,该操作将结束于对索引的简单计数循环,从而执行纯数组访问。

您可以将此类设计与例如readLine()中的BufferedReader执行操作并在最后一个元素之后返回null,或者执行搜索的正则表达式find()的{​​{1}}会更新匹配器的状态并返回成功状态。

但是,在专门设计用于识别和消除冗余操作的优化器的环境中,很难预测这种设计差异的影响。得出的结论是,基于流的解决方案有可能变得更快,尽管它取决于很多因素,是否会在特定情况下实现。如开始时所说,通常不会更改整体时间复杂度,这会变得更加重要。

答案 1 :(得分:1)

可能(并且已经有一些技巧)在后台,而传统的for循环则没有。例如:

Arrays.asList(1,2,3)
      .map(x -> x + 1)
      .count();

自从Java-9开始,map将被跳过,因为您并不关心它。

或者内部实现可能会检查某种数据结构是否已经排序,例如:

someSource.stream()
          .sorted()
          ....

如果someSource已经排序(如TreeSet),则在这种情况下sorted将是空操作。这些优化有很多是在内部完成的,并且有可能在将来进行更多的优化。

答案 2 :(得分:0)

如果您仍要使用流,则可以使用Arrays.stream在数组之外创建流,并将forEach用作:

Arrays.stream(strings).forEach(s -> {
    System.out.println("Before filtering: " + s);
    if (s.startsWith("a")) {
        System.out.println("After Filtering: " + s);
    }
});

在性能方面,由于您愿意遍历整个数组,因此使用流而不是循环并没有特别的好处。 In Java, what are the advantages of streams over loops?和其他相关问题已对此进行了讨论。

答案 3 :(得分:0)

enter image description here如果使用流,我们可以将其与parallel()一起使用,如下所示:

Stream<String> stringStream = Stream.of("d2", "a2", "b1", "b3", "c")
            .parallel()
            .filter(s -> s.startsWith("d"));

速度更快,因为您的计算机通常可以同时运行多个线程。

测试:

@Test
public void forEachVsStreamVsParallelStream_Test() {
    IntStream range = IntStream.range(Integer.MIN_VALUE, Integer.MAX_VALUE);
    StopWatch stopWatch = new StopWatch();

    stopWatch.start("for each");
    int forEachResult = 0;
    for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
        if (i % 15 == 0)
            forEachResult++;
    }
    stopWatch.stop();


    stopWatch.start("stream");
    long streamResult = range
            .filter(v -> (v % 15 == 0))
            .count();
    stopWatch.stop();


    range = IntStream.range(Integer.MIN_VALUE, Integer.MAX_VALUE);
    stopWatch.start("parallel stream");
    long parallelStreamResult = range
            .parallel()
            .filter(v -> (v % 15 == 0))
            .count();
    stopWatch.stop();

    System.out.println(String.format("forEachResult: %s%s" +
                    "parallelStreamResult: %s%s" +
                    "streamResult: %s%s",
            forEachResult, System.lineSeparator(),
            parallelStreamResult, System.lineSeparator(),
            streamResult, System.lineSeparator()));

    System.out.println("prettyPrint: " + stopWatch.prettyPrint());
    System.out.println("Time Elapsed: " + stopWatch.getTotalTimeSeconds());
}