Java 8:流与集合的性能

我是 Java 8 的新手。我仍然不深入了解API,但我做了一个小的非正式基准测试,以比较新的Streams API与旧的集合的性能。

该测试包括过滤 列表,并针对每个偶数计算平方根并将其存储在 的结果中。IntegerListDouble

代码如下:

    public static void main(String[] args) {
        //Calculating square root of even numbers from 1 to N       
        int min = 1;
        int max = 1000000;

        List<Integer> sourceList = new ArrayList<>();
        for (int i = min; i < max; i++) {
            sourceList.add(i);
        }

        List<Double> result = new LinkedList<>();


        //Collections approach
        long t0 = System.nanoTime();
        long elapsed = 0;
        for (Integer i : sourceList) {
            if(i % 2 == 0){
                result.add(Math.sqrt(i));
            }
        }
        elapsed = System.nanoTime() - t0;       
        System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));


        //Stream approach
        Stream<Integer> stream = sourceList.stream();       
        t0 = System.nanoTime();
        result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
        elapsed = System.nanoTime() - t0;       
        System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));


        //Parallel stream approach
        stream = sourceList.stream().parallel();        
        t0 = System.nanoTime();
        result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
        elapsed = System.nanoTime() - t0;       
        System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));      
    }.

以下是双核计算机的结果:

    Collections: Elapsed time:        94338247 ns   (0,094338 seconds)
    Streams: Elapsed time:           201112924 ns   (0,201113 seconds)
    Parallel streams: Elapsed time:  357243629 ns   (0,357244 seconds)

对于这个特定的测试,流的速度大约是集合的两倍,并行性没有帮助(或者我以错误的方式使用它?)。

问题:

  • 这个测试公平吗?我犯了什么错误吗?
  • 流是否比集合慢?有没有人在这方面做了一个很好的正式基准?
  • 我应该努力采用哪种方法?

更新的结果。

我在JVM预热(1k迭代)后运行了1000次测试,@pveentjer建议:

    Collections: Average time:      206884437,000000 ns     (0,206884 seconds)
    Streams: Average time:           98366725,000000 ns     (0,098367 seconds)
    Parallel streams: Average time: 167703705,000000 ns     (0,167704 seconds)

在这种情况的性能更高。我想知道在运行时仅调用一次或两次筛选函数的应用程序中会观察到什么。


答案 1
  1. 停止使用除使用迭代器从列表中间删除之外的任何内容。LinkedList

  2. 停止手动编写基准测试代码,使用 JMH

适当的基准测试:

@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(StreamVsVanilla.N)
public class StreamVsVanilla {
    public static final int N = 10000;

    static List<Integer> sourceList = new ArrayList<>();
    static {
        for (int i = 0; i < N; i++) {
            sourceList.add(i);
        }
    }

    @Benchmark
    public List<Double> vanilla() {
        List<Double> result = new ArrayList<>(sourceList.size() / 2 + 1);
        for (Integer i : sourceList) {
            if (i % 2 == 0){
                result.add(Math.sqrt(i));
            }
        }
        return result;
    }

    @Benchmark
    public List<Double> stream() {
        return sourceList.stream()
                .filter(i -> i % 2 == 0)
                .map(Math::sqrt)
                .collect(Collectors.toCollection(
                    () -> new ArrayList<>(sourceList.size() / 2 + 1)));
    }
}

结果:

Benchmark                   Mode   Samples         Mean   Mean error    Units
StreamVsVanilla.stream      avgt        10       17.588        0.230    ns/op
StreamVsVanilla.vanilla     avgt        10       10.796        0.063    ns/op

正如我所期望的那样,流实现相当慢。JIT能够内联所有lambda的东西,但不能像vanilla版本那样生成完全简洁的代码。

一般来说,Java 8流不是魔术。他们无法加速已经很好地实现的东西(可能是普通迭代或Java 5的for-each语句替换为和调用)。流更多的是关于编码的便利性和安全性。便利性 - 速度权衡在这里起作用。Iterable.forEach()Collection.removeIf()


答案 2

1)使用基准测试,您会看到时间小于1秒。这意味着副作用可能会对您的结果产生很大的影响。所以,我增加了你的任务10倍

    int max = 10_000_000;

并运行您的基准测试。我的结果:

Collections: Elapsed time:   8592999350 ns  (8.592999 seconds)
Streams: Elapsed time:       2068208058 ns  (2.068208 seconds)
Parallel streams: Elapsed time:  7186967071 ns  (7.186967 seconds)

没有编辑 () 结果是int max = 1_000_000

Collections: Elapsed time:   113373057 ns   (0.113373 seconds)
Streams: Elapsed time:       135570440 ns   (0.135570 seconds)
Parallel streams: Elapsed time:  104091980 ns   (0.104092 seconds)

这就像你的结果:流比收集慢。结论:花了很多时间进行流初始化/值传输。

2)增加任务流后变得更快(没关系),但并行流仍然太慢。怎么了?注意:你有命令。在并发执行的情况下,收集到单个集合实质上会带来性能瓶颈和开销。可以通过以下方式估算间接费用的相对成本:collect(Collectors.toList())

collecting to collection -> counting the element count

对于流,可以由 来完成。我得到了结果:collect(Collectors.counting())

Collections: Elapsed time:   41856183 ns    (0.041856 seconds)
Streams: Elapsed time:       546590322 ns   (0.546590 seconds)
Parallel streams: Elapsed time:  1540051478 ns  (1.540051 seconds)

这是一项艰巨的任务!() 结论:收集物品收集花费了大部分时间。最慢的部分是添加到列表中。顺便说一句,简单用于.int max = 10000000ArrayListCollectors.toList()


推荐