首页 > 解决方案 > 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 进行流处理(之前我认为当 d2 由 foreach 处理时,过滤器会在 a2 上进行分层操作,但根据本文并非如此:https: //winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/),经典java也是如此,那么除了“富有表现力”和“优雅”编码之外,使用流的动机应该是什么风格?我知道在处理流时编译器会产生性能开销,有人知道/经历过使用顺序流时的任何性能优势吗?

标签: javajava-8

解决方案


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


您总是可以编写一个手动循环,其操作与内部的 Stream 实现基本相同。因此,正如这个答案所提到的,内部优化总是会被“但我可以在我的循环中做同样的事情”而忽略。

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

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

接收任意 的方法Collection,用普通循环实现算法,将很难找出是否存在这样的特征。AList意味着一个有意义的顺序,而 aSet通常没有,除非它是 aSortedSet或 a LinkedHashSet,而后者是一个特定的实现类,而不是一个接口。因此,针对所有已知星座的测试可能仍然会错过具有预定义接口无法表达的特殊合同的第 3 方实现。

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


Spliterator基于 Stream 解决方案和传统循环之间还有另一个有趣的区别,Iterator在迭代数组以外的东西时使用。该模式是在迭代器上调用hasNext,后跟next,除非hasNext返回false。但是 的合同Iterator并没有强制使用这种模式。当已知成功时(例如,您确实知道集合的大小),调用者可以next不调用hasNext,甚至多次调用。此外,调用者可能会调用hasNext多次next,以防调用者不记得前一次调用的结果。

因此,Iterator实现必须执行冗余操作,例如循环条件被有效检查两次,一次在hasNext中返回 a boolean,一次在 中,在未满足时next抛出 a 。NoSuchElementException通常,hasNext必须执行实际的遍历操作并将结果存储到Iterator实例中,以确保结果在后续next调用之前保持有效。反过来next,操作必须检查这样的遍历是否已经发生,或者它是否必须自己执行操作。在实践中,热点优化器可能会或可能不会消除Iterator设计带来的开销。

相比之下,Spliterator有一个遍历方法 ,boolean tryAdvance(Consumer<? super T> action)它执行实际操作返回是否存在元素。这显着简化了循环逻辑。甚至还有void forEachRemaining(Consumer<? super T> action)for 非短路操作,它允许实际实现提供整个循环逻辑。例如,如果ArrayList操作将在索引上的简单计数循环中结束,则执行普通数组访问。

您可以将此类设计与readLine()执行BufferedReader操作并null在最后一个元素之后返回的 of 或执行搜索、更新匹配器状态并返回成功状态find()的正则表达式进行比较。Matcher

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


推荐阅读