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

问题描述 投票:3回答:3

在阅读顺序流的文章时,我想到的问题是,使用顺序流比传统的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的流处理开始(之前我认为当d2由foreach处理时,过滤器会在a2上进行操作,但根据本文不是这样的:https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/)使用经典java的情况也是如此,那么除了“表达”和“优雅”的编码风格之外,使用流的动机应该是什么?我知道编译器在处理流时会有性能开销,有没有人知道/体验过任何性能使用顺序流时的好处?

java java-8
3个回答
3
投票

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


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

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

另一方面是执行某些优化所需的信息。 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()的情况下,操作将最终在索引上进行简单的计数循环,执行普通的阵列访问。

您可以将此类设计与例如BufferedReadernull执行操作并在最后一个元素之后返回find(),或执行搜索的正则表达式MatcherArrays.asList(1,2,3) .map(x -> x + 1) .count(); ,更新匹配器的状态并返回成功状态。

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


1
投票

Streams可能(并且已经有一些技巧)在引擎盖下,传统的for循环没有。例如:

map

从java-9开始,someSource.stream() .sorted() .... 将被跳过,因为你并不关心它。

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

someSource

如果TreeSet已经排序(如sorted),在这种情况下,Arrays.stream将是一个无操作。这些优化中有许多是在内部完成的,并且有可能在未来完成更多的工作。


0
投票

如果你仍然使用流,你可以使用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?

在性能说明中,由于您愿意遍历整个阵列,因此在循环上使用流没有特定的好处。有关它的更多信息已经讨论过qazxswpoi和其他相关问题。

© www.soinside.com 2019 - 2024. All rights reserved.