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);
}
}

点 这是在 a2 的流处理只有在 d2 上的所有操作完成后才开始(早些时候我认为当 d2 由 foreach 处理时,过滤器会在 a2 上分层操作,但根据本文并非如此:https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/),经典 java 也是如此,那么使用流超越"表现力"和"优雅"编码风格的动机应该是什么?我知道编译器在处理流时存在性能开销,有谁知道/经历过使用顺序流时的任何性能优势?

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


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

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

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

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

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


基于Spliterator的 Stream 解决方案和传统循环之间还有另一个有趣的区别,即在迭代数组以外的内容时使用Iterator。模式是在迭代器上调用hasNext,后跟next,除非hasNext返回false。但Iterator合同并没有强制要求这种模式。调用者可以在不hasNext的情况下调用next,甚至在已知成功时多次调用(例如,您已经知道集合的大小)。此外,如果调用方不记得上一次调用的结果,调用方可能会多次调用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,或执行搜索的正则表达式Matcherfind(),更新匹配器的状态并返回成功状态。

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

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

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

从java-9开始,map将被跳过,因为你并不真正关心它。

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

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

如果someSource已经排序(如TreeSet),在这种情况下sorted将是无操作的。其中许多优化都是在内部完成的,并且将来可能会进行更多优化。

如果您仍然使用流,则可以使用Arrays.stream从数组中创建一个流,并使用forEach如下:

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

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

在这里输入图像描述如果使用 stream,我们可以与 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());
}

最新更新