【问题标题】:Why is the Java Stream.forEach method faster than other loops in certain situations?为什么在某些情况下 Java Stream.forEach 方法比其他循环更快?
【发布时间】:2019-12-16 20:01:52
【问题描述】:

我目前正在进行一个项目,我正在使用 Java Microbenchmark Harness (JMH) 框架测量 Java 中不同类型循环的速度。我得到了一些关于流的有趣结果,但我无法解释,我想知道是否有更好地理解流和数组列表的人可以帮助我解释我的结果。

基本上,当遍历大小约为 100 的数组列表时,stream.forEach 方法比任何其他类型的循环都要快得多:

我的结果图表如下所示: https://i.imgur.com/ypXoWWq.png

我尝试过使用对象和字符串的数组列表,都产生了相似的结果。随着列表的大小变大,流方法仍然更快,但其他列表之间的性能差距会变小。我不知道是什么导致了这些结果。

@Fork(5)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@State(Scope.Thread)
public class StackOverflowQ {

    List<Integer> intList;

    int size = 100;

    @Setup
    public void setup() {
        intList = new ArrayList<>(size);
        for(int i = 0; i<size;i++){
            intList.add(i);
        }
    }

    /**
     Work done to each item in the loop.
     */
    public double doWork(int item) {
        return item + 47;
    }

    @Benchmark
    public void standardFor(Blackhole bh){
        for (int i = 0; i<intList.size(); i++){
            bh.consume(doWork(intList.get(i)));
        }
    }

    @Benchmark
    public void streamForEach(Blackhole bh){
        intList.stream().forEach(i -> bh.consume(doWork(i)));
    }

}

【问题讨论】:

  • 您是否尝试过从循环中提取size() 调用(使用size 而不是intList.size()) - 检查stackoverflow.com/q/57414353/85421
  • 查看这个关于流的优点和缺点的问题,这可能会有所帮助 -stackoverflow.com/questions/44180101
  • 你能确认你的流是sequential()吗?为了正确起见,我会将这个显式调用包含在您的基准测试中。
  • 您还应该与for(Integer i: intList) bh.consume(doWork(i)); 变体进行比较。此外,您可以直接在ArrayList 上使用forEach,无需流。
  • 两种方法怎么能给出四种结果?

标签: java loops for-loop java-stream jmh


【解决方案1】:

这个答案谈到java.util.ArrayList。其他 Collection 实现可能(并且确实!)显示完全不同的结果。

简而言之:因为流使用Spliterator 而不是Iterator

说明:

基于Iterator 的迭代基于hasNextnext 方法调用,其中第二个方法会改变Iterator 实例。这会带来一些性能成本。 Spliterator 支持批量迭代。 Read more about this here. 增强的 for 循环 (for (T e : iterable) { ... }) 似乎使用了 Iterator

性能通常应该是次要的问题;您应该使用最能描述您的意图的结构。虽然由于向后兼容的原因,它会变得很困难,但基于拆分器的 forEach 和增强的 for 循环(在 ArrayList 上)之间的性能差异可能会在未来消失。

索引 for 循环怎么样? (List#get(int))
它们表现出比分离器更差的性能,部分原因是它们需要验证索引。其他原因可能包括方法调用,例如。获取索引处的数据,而Spliterator 直接访问数组。但这纯属猜测。

微型 JMH 基准测试

您可以在下面看到一个小型基准,它证实了所述理论。请注意,基准测试应该运行更长时间。

@State(Scope.Benchmark)
@Fork(value = 2)
@Warmup(iterations = 2, time = 3)
@Measurement(iterations = 2, time = 3)
public class A {
    
    public List<Object> list;
    
    @Setup
    public void setup() {
        list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) list.add(i);
    }
    
    @Benchmark
    public void collectionForEach(Blackhole hole) {
        list.forEach(hole::consume);
    }
    
    @Benchmark
    public void iteratorFor(Blackhole hole) {
        for (Iterator<Object> iterator = list.iterator(); iterator.hasNext(); ) {
            hole.consume(iterator.next());
        }
    }
    
    @Benchmark
    public void enhancedFor(Blackhole hole) {
        for (Object e : list) {
            hole.consume(e);
        }
    }
    
    @Benchmark
    public void iteratorForEach(Blackhole hole) {
        list.iterator().forEachRemaining(hole::consume);
    }
    
    @Benchmark
    public void indexedFor(Blackhole hole) {
        for (int i = 0, size = list.size(); i < size; i++) {
            hole.consume(list.get(i));
        }
    }
    
    @Benchmark
    public void streamForEach(Blackhole hole) {
        list.stream().forEach(hole::consume);
    }
    
    @Benchmark
    public void spliteratorForEach(Blackhole hole) {
        list.spliterator().forEachRemaining(hole::consume);
    }
}

还有结果。 “ops/s”表示操作/秒,越高越好。

Benchmark              Mode  Cnt       Score      Error  Units
A.collectionForEach   thrpt    4  158047.064 ±  959.534  ops/s
A.iteratorFor         thrpt    4  177338.462 ± 3245.129  ops/s
A.enhancedFor         thrpt    4  177508.037 ± 1629.494  ops/s
A.iteratorForEach     thrpt    4  184919.605 ± 1922.114  ops/s
A.indexedFor          thrpt    4  193318.529 ± 2715.611  ops/s
A.streamForEach       thrpt    4  280963.272 ± 2253.621  ops/s
A.spliteratorForEach  thrpt    4  283264.539 ± 3055.967  ops/s

【讨论】:

    猜你喜欢
    • 2019-12-27
    • 1970-01-01
    • 2017-10-12
    • 2016-06-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-26
    • 1970-01-01
    相关资源
    最近更新 更多