Java 8:Streams与Collections的性能


140

我是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));      
    }.

这是双核计算机的结果:

    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)

对于此特定测试,流的速度大约是集合速度的两倍,并且并行性无济于事(或者我是否使用了错误的方式?)。

问题:

  • 这个测试公平吗?我有没有犯错?
  • 流比集合慢吗?有人对此做出过良好的正式基准吗?
  • 我应该争取哪种方法?

更新结果。

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

    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
你有没有尝试过呢IntStream
Mark Rotteveel 2014年

2
您能正确测量吗?如果您所做的只是一次运行,那么您的基准当然会关闭。
skiwi 2014年

2
@MisterSmith我们是否可以通过1K测试来对如何预热JVM有所了解?
skiwi 2014年

1
而对于那些有兴趣在书面方式正确的微基准,继承人的问题:stackoverflow.com/questions/504103/...
Smith先生

2
@assylias Using toList应该并行运行,即使它收集到非线程安全列表中也是如此,因为不同的线程在合并之前会收集到线程限制的中间列表中。
斯图尔特(Stuart)标记2014年

Answers:


192
  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内容,但不会产生像原始版本一样完美的代码。

通常,Java 8流不是魔术。他们无法加快已经实现的事情的速度(可能是用普通迭代或Java 5的for-each语句替换为Iterable.forEach()and Collection.removeIf()调用)。流更多地涉及编码便利性和安全性。便利-速度折衷在这里起作用。


2
感谢您抽出宝贵的时间对此进行测试。我不认为将ArrayList的LinkedList更改会改变任何东西,因为两个测试都应该添加到它,时间不应该受到影响。无论如何,能否请您解释一下结果?在这里很难说出您要测量的内容(单位为ns / op,但是什么被认为是op?)。
史密斯先生2014年

52
您对性能的结论虽然有效,但被夸大了。在很多情况下,流代码比迭代代码,这在很大程度上是因为流的每个元素的访问成本比普通迭代器便宜。而且在许多情况下,流版本会内联到等效于手写版本的内容。当然,细节决定成败。任何给定的代码位都可能表现不同。
Brian Goetz 2014年

26
@BrianGoetz,能否请您指定用例,以提高流的速度?
亚历山德拉(Alexandre)

1
在FMH的最新版本中:使用@Benchmark代替@GenerateMicroBenchmark
pdem,2016年

3
@BrianGoetz,您可以指定流更快时的用例吗?
kiltek

17

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

    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 = 10000000结论:收集物品要花费大部分时间。最慢的部分是添加到列表中。顺便说一句,简单ArrayList用于Collectors.toList()


您需要对该测试进行微基准测试,这意味着应该先对其进行很多次预热,然后再执行很多tme并取平均值。
skiwi 2014年

@skiwi当然,您是对的,特别是因为测量值存在较大差异。我只做基本调查,并不假装结果准确。
谢尔盖·费多罗夫

服务器模式下的JIT在执行10k后启动。然后需要一些时间来编译代码并交换代码。
pveentjer 2014年

关于这句话:“ 您已经collect(Collectors.toList())在命令中,即可能需要通过多个线程处理单个Collection。 ”我几乎可以肯定,它会并行地toList收集到多个不同的列表实例。仅作为集合的最后一步,元素才被转移到一个列表中,然后返回。因此,不应有同步开销。这就是收集器同时具有供应商,累加器和合并器功能的原因。(当然,由于其他原因,它可能会变慢。)
Lii

@Lii我认为collect这里的实现方式相同。但是最后应该将几个列表合并为一个列表,在给定的示例中,合并似乎是最繁重的操作。
谢尔盖·费多罗夫

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

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

采集时间:经过时间:1522036826 ns(1.522037秒)

流:经过时间:4315833719 ns(4.315834秒)

并行流:经过时间:261152901 ns(0.261153秒)


我认为您的测试是公平的,您只需要一台具有更多cpu内核的机器即可。
梅隆2015年

3

对于您要尝试执行的操作,无论如何我都不会使用常规的Java api。有大量的装箱/拆箱操作,因此会产生巨大的性能开销。

我个人认为设计的许多API都是废话,因为它们会创建大量的对象垃圾。

尝试使用double / int的原始数组,并尝试将其单线程处理,看看性能如何。

PS:您可能想看看JMH来完成基准测试。它可以解决一些典型的陷阱,例如预热JVM。


LinkedList比ArrayList更糟糕,因为您需要创建所有节点对象。mod运算符也很慢。我相信类似10/15个周期的内容会耗尽指令流水线。如果要快速除以2,只需将数字右移1位即可。这些是基本技巧,但是我敢肯定,有一些模式高级技巧可以加快速度,但是这些可能是针对特定问题的。
pveentjer 2014年

我知道拳击。这只是一个非正式的基准。想法是在集合和流测试中具有相同数量的装箱/拆箱。
史密斯先生

首先,我要确保它没有测量错误。在执行真正的基准测试之前,请尝试运行几次基准测试。然后,至少您可以避免JVM预热,并且代码已正确设置。没有这个,您可能会得出错误的结论。
pveentjer 2014年

好的,我会按照您的建议发布新结果。我看过JMH,但是它需要Maven,并且需要一些时间来配置。不管怎么说,还是要谢谢你。
史密斯先生2014年

我认为最好避免以“针对您要执行的操作”来考虑基准测试。也就是说,通常将这些类型的练习简化得足以证明,但又足够复杂,以至于看起来它们可以/应该简化。
ryvantage 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.