在浏览顺序流的文章时,我想到的问题是,与传统的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相同,因此使用超出“表现力”和“优雅”编码风格的流应该是什么动机?我知道编译器在处理流时会有性能开销,有人吗?知道/曾经体验过使用顺序流带来的任何性能优势?
答案 0 :(得分:3)
首先,让一些特殊情况,例如省略多余的sorted
操作或返回count()
上的已知大小,操作的时间复杂度通常不会改变,因此所有差异在执行时间上通常大约是一个恒定的偏移量或一个(相当小的)因素,而不是基本的变化。
您总是可以编写一个与Stream内部实现基本相同的手动循环。因此,this answer提到的内部优化总是会被“但我可以在循环中做同样的事情”抛弃。
但是……当我们将“流”与“循环”进行比较时,假设针对特定用例以最有效的方式编写所有手动循环是否真的合理?无论调用代码作者的经验水平如何,特定的Stream实现都会将其优化应用于所有适用的情况。我已经看到循环丢失了短路或执行特定用例不需要的冗余操作的机会。
另一方面是执行某些优化所需的信息。流API围绕Spliterator
接口构建,该接口可以提供源数据的特征,例如它允许找出数据是否具有有意义的顺序(某些操作需要保留),或者是否已经按照自然顺序或使用特定的比较器进行了预排序。当可以预测时,它还可以提供预期的元素数量,作为估计值或精确值。
一种接收任意Collection
的方法以实现具有普通循环的算法,将很难找出是否存在这样的特征。 List
表示有意义的顺序,而Set
通常没有意义,除非它是SortedSet
或LinkedHashSet
,而后者是特定的实现类,而不是接口。因此,针对所有已知的星座进行测试可能仍会错过第三方协议的实现,而第三方协议无法通过预定义的界面来表达。
当然,从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());
}