我是Java 8的新手。我仍然不了解API的详细信息,但是我做了一个小型的非正式基准测试,以比较新Streams API和旧的Collections的性能。

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

这是代码:

    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)
    

    在这种情况下,流的性能更高。我想知道在运行时仅过滤一次或两次的应用程序中会观察到什么。

    最佳答案

  • 停止使用LinkedList进行任何操作,但要使用迭代器从列表中间大量删除。
  • 停止手动编写基准测试代码,请使用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()Collection.removeIf()调用)。流更多地涉及编码便利性和安全性。便利-速度折衷在这里起作用。

    09-30 15:10
    查看更多