• 为什么list.sort()比Stream().sorted()更快?


    昨天写了一篇文章《小细节,大问题。分享一次代码优化的过程》,里面提到了list.sort()和list.strem().sorted()排序的差异。
    说到list sort()排序比stream().sorted()排序性能更好。
    但没说到为什么。
    有朋友也提到了这一点。

    本文重新开始,先问是不是,再问为什么。


    真的更好吗?


    先简单写个demo

    List userList = new ArrayList<>();
            Random rand = new Random();
            for (int i = 0; i < 10000 ; i++) {
                userList.add(rand.nextInt(1000));
            }
            List userList2 = new ArrayList<>();
            userList2.addAll(userList);
    
            Long startTime1 = System.currentTimeMillis();
            userList2.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
            System.out.println("stream.sort耗时:"+(System.currentTimeMillis() - startTime1)+"ms");
    
            Long startTime = System.currentTimeMillis();
            userList.sort(Comparator.comparing(Integer::intValue));
            System.out.println("List.sort()耗时:"+(System.currentTimeMillis()-startTime)+"ms");
    

    输出

    stream.sort耗时:62ms
    List.sort()耗时:7ms
    
    

    由此可见list原生排序性能更好。
    能证明吗?
    证据错了。


    再把demo变换一下,先输出stream.sort

    List userList = new ArrayList<>();
            Random rand = new Random();
            for (int i = 0; i < 10000 ; i++) {
                userList.add(rand.nextInt(1000));
            }
            List userList2 = new ArrayList<>();
            userList2.addAll(userList);
    
            Long startTime = System.currentTimeMillis();
            userList.sort(Comparator.comparing(Integer::intValue));
            System.out.println("List.sort()耗时:"+(System.currentTimeMillis()-startTime)+"ms");
    
            Long startTime1 = System.currentTimeMillis();
            userList2.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
            System.out.println("stream.sort耗时:"+(System.currentTimeMillis() - startTime1)+"ms");
    

    此时输出变成了

    List.sort()耗时:68ms
    stream.sort耗时:13ms
    

    这能证明上面的结论错误了吗?
    都不能。
    两种方式都不能证明什么。

    使用这种方式在很多场景下是不够的,某些场景下,JVM会对代码进行JIT编译和内联优化。

    Long startTime = System.currentTimeMillis();
    ...
    System.currentTimeMillis() - startTime
    
    

    此时,代码优化前后执行的结果就会非常大。

    基准测试是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。
    

    基准测试使得被测试代码获得足够预热,让被测试代码得到充分的JIT编译和优化。


    下面是通过JMH做一下基准测试,分别测试集合大小在100,10000,100000时两种排序方式的性能差异。

    import org.openjdk.jmh.annotations.*;
    import org.openjdk.jmh.infra.Blackhole;
    import org.openjdk.jmh.results.format.ResultFormatType;
    import org.openjdk.jmh.runner.Runner;
    import org.openjdk.jmh.runner.RunnerException;
    import org.openjdk.jmh.runner.options.Options;
    import org.openjdk.jmh.runner.options.OptionsBuilder;
    
    import java.util.*;
    import java.util.concurrent.ThreadLocalRandom;
    import java.util.concurrent.TimeUnit;
    import java.util.stream.Collectors;
    
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    @Warmup(iterations = 2, time = 1)
    @Measurement(iterations = 5, time = 5)
    @Fork(1)
    @State(Scope.Thread)
    public class SortBenchmark {
    
        @Param(value = {"100", "10000", "100000"})
        private int operationSize; 
    
    
        private static List arrayList;
    
        public static void main(String[] args) throws RunnerException {
            // 启动基准测试
            Options opt = new OptionsBuilder()
                    .include(SortBenchmark.class.getSimpleName()) 
                    .result("SortBenchmark.json")
                    .mode(Mode.All)
                    .resultFormat(ResultFormatType.JSON)
                    .build();
            new Runner(opt).run(); 
        }
    
        @Setup
        public void init() {
            arrayList = new ArrayList<>();
            Random random = new Random();
            for (int i = 0; i < operationSize; i++) {
                arrayList.add(random.nextInt(10000));
            }
        }
    
    
        @Benchmark
        public void sort(Blackhole blackhole) {
            arrayList.sort(Comparator.comparing(e -> e));
            blackhole.consume(arrayList);
        }
    
        @Benchmark
        public void streamSorted(Blackhole blackhole) {
            arrayList = arrayList.stream().sorted(Comparator.comparing(e -> e)).collect(Collectors.toList());
            blackhole.consume(arrayList);
        }
    
    }
    
    

    性能测试结果:

    可以看到,list sort()效率确实比stream().sorted()要好。


    为什么更好?


    流本身的损耗


    java的stream让我们可以在应用层就可以高效地实现类似数据库SQL的聚合操作了,它可以让代码更加简洁优雅。

    但是,假设我们要对一个list排序,得先把list转成stream流,排序完成后需要将数据收集起来重新形成list,这部份额外的开销有多大呢?

    我们可以通过以下代码来进行基准测试

    import org.openjdk.jmh.annotations.*;
    import org.openjdk.jmh.infra.Blackhole;
    import org.openjdk.jmh.results.format.ResultFormatType;
    import org.openjdk.jmh.runner.Runner;
    import org.openjdk.jmh.runner.RunnerException;
    import org.openjdk.jmh.runner.options.Options;
    import org.openjdk.jmh.runner.options.OptionsBuilder;
    
    import java.util.ArrayList;
    import java.util.Comparator;
    import java.util.List;
    import java.util.Random;
    import java.util.concurrent.TimeUnit;
    import java.util.stream.Collectors;
    
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    @Warmup(iterations = 2, time = 1)
    @Measurement(iterations = 5, time = 5)
    @Fork(1)
    @State(Scope.Thread)
    public class SortBenchmark3 {
    
        @Param(value = {"100", "10000"})
        private int operationSize; // 操作次数
    
    
        private static List arrayList;
    
        public static void main(String[] args) throws RunnerException {
            // 启动基准测试
            Options opt = new OptionsBuilder()
                    .include(SortBenchmark3.class.getSimpleName()) // 要导入的测试类
                    .result("SortBenchmark3.json")
                    .mode(Mode.All)
                    .resultFormat(ResultFormatType.JSON)
                    .build();
            new Runner(opt).run(); // 执行测试
        }
    
        @Setup
        public void init() {
            // 启动执行事件
            arrayList = new ArrayList<>();
            Random random = new Random();
            for (int i = 0; i < operationSize; i++) {
                arrayList.add(random.nextInt(10000));
            }
        }
    
        @Benchmark
        public void stream(Blackhole blackhole) {
            arrayList.stream().collect(Collectors.toList());
            blackhole.consume(arrayList);
        }
    
        @Benchmark
        public void sort(Blackhole blackhole) {
            arrayList.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
            blackhole.consume(arrayList);
        }
     
    }
    

    方法stream测试将一个集合转为流再收集回来的耗时。

    方法sort测试将一个集合转为流再排序再收集回来的全过程耗时。


    测试结果如下:

    可以发现,集合转为流再收集回来的过程,肯定会耗时,但是它占全过程的比率并不算高。

    因此,这部只能说是小部份的原因。


    排序过程


    我们可以通过以下源码很直观的看到。

    • 1 begin方法初始化一个数组。
    • 2 accept 接收上游数据。
    • 3 end 方法开始进行排序。
      这里第3步直接调用了原生的排序方法,完成排序后,第4步,遍历向下游发送数据。

    所以通过源码,我们也能很明显地看到,stream()排序所需时间肯定是 > 原生排序时间。

    只不过,这里要量化地搞明白,到底多出了多少,这里得去编译jdk源码,在第3步前后将时间打印出来。

    这一步我就不做了。
    感兴趣的朋友可以去测一下。

    不过我觉得这两点也能很好地回答,为什么list.sort()比Stream().sorted()更快。

    补充说明:

    1. 本文说的stream()流指的是串行流,而不是并行流。
    2. 绝大多数场景下,几百几千几万的数据,开心就好,怎么方便怎么用,没有必要去计较这点性能差异。
  • 相关阅读:
    2022腾讯数字生态大会:腾讯云HiFlow,零代码自动化工作流助手
    js 时间加年月日
    Highlight_Matching_Pair问题解决
    《持续交付:发布可靠软件的系统方法》- 读书笔记(十五)
    SpringMVC获得请求数据类型
    sqlmap语法介绍
    Android Gradle 命令打包AAR
    Go语言入门心法(十四): Go操作Redis实战
    关于排序算法
    项目延期常见的三个原因及对应的解决方法
  • 原文地址:https://www.cnblogs.com/eryuan/p/17598880.html