Java 8:Streams vs Collections的性能

Mis*_*ith 126 java collections performance java-8 java-stream

我是Java 8的新手.我仍然不深入了解API,但我已经做了一个小的非正式基准测试来比较新Streams API与优秀旧Collections的性能.

测试包括过滤一个列表Integer,并为每个偶数计算平方根并将其存储在结果ListDouble.

这是代码:

    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));      
    }.
Run Code Online (Sandbox Code Playgroud)

以下是双核机器的结果:

    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)
Run Code Online (Sandbox Code Playgroud)

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

问题:

  • 这个测试公平吗?我犯了什么错吗?
  • 流比收集慢吗?有谁在这方面做了一个很好的正式基准?
  • 我应该采取哪种方法?

更新结果.

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

    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)
Run Code Online (Sandbox Code Playgroud)

在这种情况下,流更高效.我想知道在运行时只调用过滤函数一次或两次的应用程序会观察到什么.

lev*_*tov 175

  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)));
    }
}
Run Code Online (Sandbox Code Playgroud)

结果:

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
Run Code Online (Sandbox Code Playgroud)

正如我预期的那样,流实现速度相当慢.JIT能够内联所有lambda内容,但不会像vanilla版本那样生成完美简洁的代码.

通常,Java 8流不是魔术.他们无法加速已经很好实现的东西(可能是普通的迭代或Java 5的 - 每个语句都替换为Iterable.forEach()Collection.removeIf()调用).流更多的是编码方便性和安全性.方便 - 速度权衡在这里工作.

  • 关于表现的结论虽然有效,却被夸大了.在很多情况下,流代码比迭代代码快**,主要是因为流的每元素访问成本比使用普通迭代器更便宜.在许多情况下,流版本内联到与手写版本相同的内容.当然,魔鬼在于细节; 任何给定的代码位可能表现不同. (47认同)
  • @BrianGoetz,你能指出用例,当流更快? (21认同)
  • @BrianGoetz,当 Streams 更快时,您能指定用例吗? (7认同)
  • 感谢您抽出宝贵的时间来为此做准备.我不认为更改LinkedList for ArrayList会改变任何东西,因为两个测试都应该添加它,时间不应该受到影响.无论如何,你能解释一下结果吗?很难说出你在这里测量的是什么(单位说ns/op,但是什么被认为是op?). (2认同)
  • @HariBharathi 你知道“peek”的作用吗? (2认同)

Ser*_*rov 16

1)您使用基准测试时间不到1秒.这意味着副作用会对您的结果产生强烈影响.所以,我增加了你的任务10次

    int max = 10_000_000;
Run Code Online (Sandbox Code Playgroud)

并运行你的基准.我的结果:

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)
Run Code Online (Sandbox Code Playgroud)

没有edit(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)
Run Code Online (Sandbox Code Playgroud)

这就像你的结果:流比收集慢.结论:流初始化/值传输花费了大量时间.

2)增加任务流后变得更快(没关系),但并行流仍然太慢.怎么了?注意:你有collect(Collectors.toList())命令.收集单个集合实质上会在并发执行时引入性能瓶颈和开销.可以通过替换来估计开销的相对成本

collecting to collection -> counting the element count
Run Code Online (Sandbox Code Playgroud)

对于流,它可以通过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)
Run Code Online (Sandbox Code Playgroud)

那是一项艰巨的任务!(int max = 10000000)结论:收集物品需要花费大部分时间.最慢的部分是添加到列表中.BTW,简单ArrayList用于Collectors.toList().


pve*_*jer 5

对于你想要做的事情,我无论如何都不会使用常规的 java api。正在进行大量的装箱/拆箱,因此存在巨大的性能开销。

就我个人而言,我认为很多 API 设计都是垃圾,因为它们创建了很多对象垃圾。

尝试使用 double/int 的原始数组并尝试单线程执行,看看性能如何。

PS:您可能想看看 JMH 来负责进行基准测试。它解决了一些典型的陷阱,例如预热 JVM。


小智 5

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

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

static double doSomeCalculate(int input) {
    for(int i=0; i<100000; i++){
        Math.sqrt(i+input);
    }
    return Math.sqrt(input);
}
Run Code Online (Sandbox Code Playgroud)

我稍微更改了代码,在我的 8 核 mac book pro 上运行,我得到了一个合理的结果:

Collections: Elapsed time:      1522036826 ns   (1.522037 seconds)
Streams: Elapsed time:          4315833719 ns   (4.315834 seconds)
Parallel streams: Elapsed time:  261152901 ns   (0.261153 seconds)
Run Code Online (Sandbox Code Playgroud)