乐趣区

关于java:为什么-listsort-比-streamsorted-要更快测试结果把我惊呆了

作者:是奉壹呀 \
起源:juejin.cn/post/7262274383287500860

看到一个评论,外面提到了 list.sort() 和 list.strem().sorted() 排序的差别。

说到 list sort() 排序比 stream().sorted() 排序性能更好,但没说到为什么。

有敌人也提到了这一点。本文从新开始,先问是不是,再问为什么。

举荐一个开源收费的 Spring Boot 实战我的项目:

https://github.com/javastacks/spring-boot-best-practice

真的更好吗?

先简略写个 demo

List<Integer> userList = new ArrayList<>();
        Random rand = new Random();
        for (int i = 0; i < 10000 ; i++) {userList.add(rand.nextInt(1000));
        }
        List<Integer> 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<Integer> userList = new ArrayList<>();
        Random rand = new Random();
        for (int i = 0; i < 10000 ; i++) {userList.add(rand.nextInt(1000));
        }
        List<Integer> 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<Integer> 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<Integer> 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、 绝大多数场景下,几百几千几万的数据,开心就好,怎么不便怎么用,没有必要去计较这点性能差别;

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿 (2022 最新版)

2. 劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4. 别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

退出移动版