爲什麼list.sort()比Stream().sorted()更快?

昨天寫了一篇文章《小細節,大問題。分享一次代碼優化的過程》,裏面提到了list.sort()和list.strem().sorted()排序的差異。
說到list sort()排序比stream().sorted()排序性能更好。
但沒說到爲什麼。
有朋友也提到了這一點。

本文重新開始,先問是不是,再問爲什麼。


真的更好嗎?


先簡單寫個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. 絕大多數場景下,幾百幾千幾萬的數據,開心就好,怎麼方便怎麼用,沒有必要去計較這點性能差異。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章