1. 程式人生 > 其它 >Java Stream 效能測試

Java Stream 效能測試

Java Stream 效能測試 本測試是測試 Java Stream 與傳統程式設計方式(即 foreach)之間的效能差距。

Java Stream 效能測試

本測試是測試 Java Stream 與傳統程式設計方式(即 foreach)之間的效能差距。

測試環境

JDK

JDK 1.8.0_191, Java HotSpot(TM) 64-Bit Server VM, 25.191-b12

測試框架

JMH v1.33

測試機器

  • CPU:AMD Ryzen 7 3700X 8 核 16 執行緒 4.4GHz
  • 記憶體:8GB * 2 1200MHz
  • 作業系統:Windows 10 專業版 64 位 19043.1348

測試方式

  • 用 maven 把專案打成 jar 包,在命令列介面執行這個 jar 包
  • 每個測試專案預熱 3 次,每次持續 5 秒
  • 每個測試專案執行 5 次,每次持續 5 秒
  • 每個測試場景跑 3 輪,測試結果裡的是兩種方式每毫秒的吞吐量

執行命令

java -server -Xms2048m -jar stream-demo-1.0-SNAPSHOT-jar-with-dependencies.jar

測試場景

測試資料類:

public class User {
    private Integer id;
    private String username;
    private Boolean sex;
    private Integer type;
    ...
}

場景一

將使用者列表轉成 id -> username 的對映

測試程式碼

@BenchmarkMode(Mode.Throughput)
@State(Scope.Thread)
@Fork(1)
@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 5, time = 5)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class PerformanceTest {

    private final List<User> users = initUsers();

    /**
     * 傳統方式
     */
    @Benchmark
    public Map<Integer, String> tradition() {
        Map<Integer, String> map = new HashMap<>((int) (users.size() / 0.75));
        for (User user : users) {
            map.put(user.getId(), user.getUsername());
        }
        return map;
    }

    /**
     * Stream方式
     */
    @Benchmark
    public Map<Integer, String> stream() {
        Map<Integer, String> map = users.stream().collect(Collectors.toMap(
            User::getId, User::getUsername));
        return map;
    }
    
    private List<User> initUsers() {
        int size = 1000;
        List<User> users = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            users.add(new User(i, "user" + i, true, 0));
        }
        return users;
    }

    public static void main(String[] args) throws Exception {
        Options opt = new OptionsBuilder()
            .include(PerformanceTest.class.getSimpleName())
            .build();
        new Runner(opt).run();
    }
}

測試結果

第一輪
Benchmark                   Mode  Cnt   Score   Error   Units
PerformanceTest.tradition  thrpt    5  87.527 ± 6.281  ops/ms
PerformanceTest.stream     thrpt    5  62.563 ± 0.199  ops/ms
第二輪
Benchmark                   Mode  Cnt   Score   Error   Units
PerformanceTest.tradition  thrpt    5  85.194 ± 2.157  ops/ms
PerformanceTest.stream     thrpt    5  62.310 ± 0.203  ops/ms
第三輪
Benchmark                   Mode  Cnt   Score   Error   Units
PerformanceTest.tradition  thrpt    5  87.472 ± 4.789  ops/ms
PerformanceTest.stream     thrpt    5  62.572 ± 2.242  ops/ms

傳統方式跟 Stream 方式的比值大約是 1.39

場景二

將使用者列表轉成 VO 列表

測試程式碼

@BenchmarkMode(Mode.Throughput)
@State(Scope.Thread)
@Fork(1)
@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 5, time = 5)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class PerformanceTest {

    private final List<User> users = initUsers();

    /**
     * 傳統方式
     */
    @Benchmark
    public List<UserVO> tradition() {
        List<UserVO> userVOList = new ArrayList<>(users.size());
        for (User user : users) {
            userVOList.add(toUserVo(user));
        }
        return userVOList;
    }

    /**
     * Stream方式
     */
    @Benchmark
    public List<UserVO> stream() {
        List<UserVO> userVoList = users.stream()
            .map(this::toUserVo)
            .collect(Collectors.toList());
        return userVoList;
    }

    private UserVO toUserVo(User user) {
        UserVO userVO = new UserVO();
        userVO.setId(user.getId());
        userVO.setUsername(user.getUsername());
        userVO.setSex(getSexStr(user.getSex()));
        userVO.setType(getTypeStr(user.getType()));
        return userVO;
    }

    private String getSexStr(Boolean sex) {
        if (sex == null) return "";
        return sex ? "男" : "女";
    }

    private String getTypeStr(Integer type) {
        if (type == null) return "";
        switch (type) {
            case 0: return "青銅使用者";
            case 1: return "白銀使用者";
            case 2: return "黃金使用者";
            default: return "";
        }
    }

    private List<User> initUsers() {
        int size = 1000;
        List<User> users = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            users.add(new User(i, "user" + i, true, 0));
        }
        return users;
    }

    public static void main(String[] args) throws Exception {
        Options opt = new OptionsBuilder()
            .include(PerformanceTest.class.getSimpleName())
            .build();
        new Runner(opt).run();
    }
}

測試結果

第一輪
Benchmark                   Mode  Cnt    Score   Error   Units
PerformanceTest.tradition  thrpt    5  126.362 ± 1.992  ops/ms
PerformanceTest.stream     thrpt    5   86.086 ± 2.573  ops/ms
第二輪
Benchmark                   Mode  Cnt    Score   Error   Units
PerformanceTest.tradition  thrpt    5  124.212 ± 2.732  ops/ms
PerformanceTest.stream     thrpt    5   81.427 ± 1.011  ops/ms
第三輪
Benchmark                   Mode  Cnt    Score   Error   Units
PerformanceTest.tradition  thrpt    5  124.594 ± 1.108  ops/ms
PerformanceTest.stream     thrpt    5   85.825 ± 1.004  ops/ms

傳統方式跟 Stream 方式的比值大約是 1.48

場景三

將使用者列表的 username 按照在列表中的順序用逗號拼接起來,並去重

測試程式碼

@BenchmarkMode(Mode.Throughput)
@State(Scope.Thread)
@Fork(1)
@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 5, time = 5)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class PerformanceTest {

    private final List<User> users = initUsers();

    /**
     * 傳統方式
     */
    @Benchmark
    public String tradition() {
        StringBuilder usernames = new StringBuilder();
        Set<String> set = new HashSet<>();
        for (User user : users) {
            if (set.add(user.getUsername())) {
                usernames.append(user.getUsername()).append(',');
            }
        }
        if (usernames.length() != 0) {
            usernames.deleteCharAt(usernames.length() - 1);
        }
        return usernames.toString();
    }

    /**
     * Stream方式
     */
    @Benchmark
    public String stream() {
        String usernames = users.stream()
            .map(User::getUsername)
            .distinct()
            .collect(Collectors.joining(","));
        return usernames;
    }

    private List<User> initUsers() {
        int size = 1000;
        List<User> users = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            users.add(new User(i, "user" + i, true, 0));
        }
        return users;
    }

    public static void main(String[] args) throws Exception {
        Options opt = new OptionsBuilder()
            .include(PerformanceTest.class.getSimpleName())
            .build();
        new Runner(opt).run();
    }
}

測試結果

第一輪
Benchmark                   Mode  Cnt   Score   Error   Units
PerformanceTest.tradition  thrpt    5  35.034 ± 0.502  ops/ms
PerformanceTest.stream     thrpt    5  26.225 ± 0.542  ops/ms
第二輪
Benchmark                   Mode  Cnt   Score   Error   Units
PerformanceTest.tradition  thrpt    5  35.071 ± 0.274  ops/ms
PerformanceTest.stream     thrpt    5  26.634 ± 0.304  ops/ms
第三輪
Benchmark                   Mode  Cnt   Score   Error   Units
PerformanceTest.tradition  thrpt    5  34.946 ± 0.318  ops/ms
PerformanceTest.stream     thrpt    5  26.140 ± 0.167  ops/ms

傳統方式跟 Stream 方式的比值大約是 1.33

場景四

統計整型陣列中的偶數個數

測試程式碼

@BenchmarkMode(Mode.Throughput)
@State(Scope.Thread)
@Fork(1)
@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 5, time = 5)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class PerformanceTest {

    private final int[] array = initArray();

    /**
     * 傳統方式
     */
    @Benchmark
    public int tradition() {
        int count = 0;
        for (int i : array) {
            if (i % 2 == 0) {
                count++;
            }
        }
        return count;
    }

    /**
     * Stream方式
     */
    @Benchmark
    public int stream() {
        long count = Arrays.stream(array)
            .filter(i -> i % 2 == 0)
            .count();
        return (int) count;
    }

    private int[] initArray() {
        int size = 1000;
        int[] array = new int[size];
        for (int i = 0; i < size; i++) {
            array[i] = i;
        }
        return array;
    }

    public static void main(String[] args) throws Exception {
        Options opt = new OptionsBuilder()
            .include(PerformanceTest.class.getSimpleName())
            .build();
        new Runner(opt).run();
    }
}

測試結果

第一輪
Benchmark                   Mode  Cnt     Score    Error   Units
PerformanceTest.tradition  thrpt    5  2099.627 ± 24.871  ops/ms
PerformanceTest.stream     thrpt    5  1203.562 ± 12.984  ops/ms
第二輪
Benchmark                   Mode  Cnt     Score   Error   Units
PerformanceTest.tradition  thrpt    5  2083.610 ± 6.045  ops/ms
PerformanceTest.stream     thrpt    5  1220.351 ± 7.516  ops/ms
第三輪
Benchmark                   Mode  Cnt     Score    Error   Units
PerformanceTest.tradition  thrpt    5  2096.514 ± 58.327  ops/ms
PerformanceTest.stream     thrpt    5  1234.086 ± 14.287  ops/ms

傳統方式跟 Stream 方式的比值大約是 1.72

場景五

跟場景一一樣,不過使用的是並行流

測試程式碼

@BenchmarkMode(Mode.Throughput)
@State(Scope.Thread)
@Fork(1)
@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 5, time = 5)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class PerformanceTest {

    private final List<User> users = initUsers();

    /**
     * 傳統方式
     */
    @Benchmark
    public Map<Integer, String> tradition() {
        Map<Integer, String> map = new HashMap<>((int) (users.size() / 0.75));
        for (User user : users) {
            map.put(user.getId(), user.getUsername());
        }
        return map;
    }

    /**
     * Stream方式
     */
    @Benchmark
    public Map<Integer, String> stream() {
        Map<Integer, String> map = users.parallelStream()
            .collect(Collectors.toMap(
                User::getId, User::getUsername));
        return map;
    }

    private List<User> initUsers() {
        int size = 1000;
        List<User> users = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            users.add(new User(i, "user" + i, true, 0));
        }
        return users;
    }

    public static void main(String[] args) throws Exception {
        Options opt = new OptionsBuilder()
            .include(PerformanceTest.class.getSimpleName())
            .build();
        new Runner(opt).run();
    }
}

測試結果

第一輪
Benchmark                   Mode  Cnt   Score   Error   Units
PerformanceTest.tradition  thrpt    5  81.262 ± 2.309  ops/ms
PerformanceTest.stream     thrpt    5  17.596 ± 0.013  ops/ms
第二輪
Benchmark                   Mode  Cnt   Score   Error   Units
PerformanceTest.tradition  thrpt    5  85.698 ± 3.744  ops/ms
PerformanceTest.stream     thrpt    5  17.588 ± 0.092  ops/ms
第三輪
Benchmark                   Mode  Cnt   Score   Error   Units
PerformanceTest.tradition  thrpt    5  86.445 ± 6.377  ops/ms
PerformanceTest.stream     thrpt    5  17.668 ± 0.021  ops/ms

傳統方式跟 Stream 方式的比值大約是 4.79

總結

  • 對於一般場景下 1000 左右的資料量的處理,Stream 方式的耗時大概是傳統方式的 1.5 倍左右
  • 對於基本資料型別 1000 左右資料量的處理,Stream 方式的耗時大概是傳統方式在 1.7 倍左右
  • 對於計算量較少的情況,不建議使用並行流

使用 Stream 方式跟傳統程式設計方式相比執行效率之間的差距並不大,除非是對效能要求很高的系統,否則對於一般的業務系統來說不會造成 CPU 的瓶頸,IO 才是最大的瓶頸。相比之下,使用 Stream 可以讓程式碼變得更加乾淨整潔,可讀性和可維護性更好,所以一般情況下推薦使用 Stream 來程式設計。