Java レイジー エバリュエーション

[Javaのカレンダー | Advent Calendar 2021](https://qiita.com/advent-calendar/2021/java)の16日目の記事です

_I will always choose a lazy person to do a difficult job. Because he will find an easy way to do it_ - Bill Gates

※ 私はいつも難しい仕事は怠け者にやってもらいます。なぜなら、怠け者は楽なやり方を見つけてくれるからです。 - ビル・ゲイツ

# 概要

Java の遅延評価(lazy evaluation)を Stream API 経由で実行した場合の性能を見てみます

[Java ストリーム](https://qiita.com/gosshys/items/8ff46625f46ed4214de1) で検証したように、1000件以上のオブジェクトを処理してみます

結果は、大雑把に表現するとヒープも処理速度も10倍以上、 Stream API を利用したほうが良いと思いました(感想)

:::note warn
Stream API の fliter (Predicate) は遅延評価ではないといわれてしまうと...
:::

# 比較したコード

コードは適当なオブジェクトが1000個（、1万個、10万個）入ったリストからある条件(今回は年齢が30以上)を抽出するものです

## Stream API の場合

```java
    void useStream() {
        l.stream().filter(p -> p.age >= 30).collect(Collectors.toList());
    }
```

## Stream API で parallel をつけた場合 

```java
    void useStreamWithParallel() {
        l.stream().filter(p -> p.age >= 30).parallel().collect(Collectors.toList());
    }
```

## List を使う場合

```java
    void useList() {
        List<Parson> res = new ArrayList<>();
        for (var p : l) {
            if (p.age < 30) {
                continue;
            }
            res.add(create());
        }
    }
```

# 測定結果

測定には、以前の記事　[Javaでヒープサイズ測定 JUnit5編](https://qiita.com/gosshys/items/e6aa2fe12762cf29e9c0)　で使用した、[quickperf](https://github.com/quick-perf/quickperf) と [Javaでベンチマーク(性能測定) JUnit5編](https://qiita.com/gosshys/items/72f56daf9ee01e10e987) で紹介した [jmh](https://github.com/openjdk/jmh) を使いました

Stream API の parallel を推したいのですが、なかなか結果がついてこないので、今回は欲張って1万件、10万件も検証しました。
スコアは、parallelなしの Stream API に比べ、parallelありの場合が、1万件でやや劣勢、10万件で逆転しました　:clap:

## JVM のヒープサイズ

### 1000 件

```
[QUICK PERF] Measured heap allocation (test method thread): 21.44 Kilo bytes (21 952 bytes) ← Stream API
[QUICK PERF] Measured heap allocation (test method thread): 50.95 Kilo bytes (52 176 bytes) ← Stream API (parallel)
[QUICK PERF] Measured heap allocation (test method thread): 264.87 Kilo bytes (271 224 bytes) ← List
```
### 10000 件

```
[QUICK PERF] Measured heap allocation (test method thread): 121.70 Kilo bytes (124 616 bytes) ← Stream API
[QUICK PERF] Measured heap allocation (test method thread): 160.49 Kilo bytes (164 344 bytes) ← Stream API (parallel)
[QUICK PERF] Measured heap allocation (test method thread): 2.45 Mega bytes (2 573 952 bytes) ← List (単位が違う)
```

### 10000 件

```
[QUICK PERF] Measured heap allocation (test method thread): 845.55 Kilo bytes (865 848 bytes) ← Stream API
[QUICK PERF] Measured heap allocation (test method thread): 823.67 Kilo bytes (843 440 bytes) ← Stream API (parallel)
[QUICK PERF] Measured heap allocation (test method thread): 24.67 Mega bytes (25 870 512 bytes) ← List (単位が違う)
```


## ベンチマーク

### 1000 件

```
Benchmark                                        Mode  Cnt       Score      Error  Units
StreamVsListLazyPerfTest.useList                thrpt    5    1171.093 ±   22.063  ops/s
StreamVsListLazyPerfTest.useStream              thrpt    5  218394.756 ± 4049.119  ops/s
StreamVsListLazyPerfTest.useStreamWithParallel  thrpt    5   68465.691 ± 1838.747  ops/s
```

### 10000 件

```
Benchmark                                        Mode  Cnt      Score      Error  Units
StreamVsListLazyPerfTest.useList                thrpt    5    118.407 ±    3.344  ops/s
StreamVsListLazyPerfTest.useStream              thrpt    5  21036.060 ±  379.085  ops/s
StreamVsListLazyPerfTest.useStreamWithParallel  thrpt    5  18515.519 ± 3244.870  ops/s
```

### 100000 件

```
Benchmark                                        Mode  Cnt     Score    Error  Units
StreamVsListLazyPerfTest.useList                thrpt    5    10.984 ±  0.102  ops/s
StreamVsListLazyPerfTest.useStream              thrpt    5   813.617 ± 86.894  ops/s
StreamVsListLazyPerfTest.useStreamWithParallel  thrpt    5  2140.738 ± 79.030  ops/s
```

# 付録

## ヒープ測定用のテストコード

```java
@QuickPerfTest
public class StreamVsListLazyTest {

    List<Parson> l = new ArrayList<>();

    @BeforeEach
    void beforeAll() {
        for (int i = 0; i < 10000; i++) {
            l.add(create());
        }
    }

    @MeasureHeapAllocation
    @Test
    void useStream() {
        l.stream().filter(p -> p.age >= 30).collect(Collectors.toList());
    }

    @MeasureHeapAllocation
    @Test
    void useStreamWithParallel() {
        l.stream().filter(p -> p.age >= 30).parallel().collect(Collectors.toList());
    }

    @MeasureHeapAllocation
    @Test
    void useList() {
        List<Parson> res = new ArrayList<>();
        for (var p : l) {
            if (p.age < 30) {
                continue;
            }
            res.add(create());
        }
    }

    @AllArgsConstructor
    @Data
    class Parson {
        private String name;
        private int age;
        private String addr;
        private String addr2;
    }

    Parson create() {
        return new Parson(RandomStringUtils.randomAlphabetic(10), new Random().nextInt(100), RandomStringUtils.randomAlphabetic(20), RandomStringUtils.randomAlphabetic(20));
    }

}
```

## ベンチマーク取得用のテストコード

```java
@State(value = Scope.Benchmark)
public class StreamVsListLazyPerfTest {

    List<Parson> l = new ArrayList<>();

    @Setup
    public void setup() {
        for (int i = 0; i < 10000; i++) {
            l.add(create());
        }
    }

    @Benchmark
    public void useStream() {
        l.stream().filter(p -> p.age >= 30).collect(Collectors.toList());
    }

    @Benchmark
    public void useStreamWithParallel() {
        l.stream().filter(p -> p.age >= 30).parallel().collect(Collectors.toList());
    }

    @Benchmark
    public void useList() {
        List<Parson> res = new ArrayList<>();
        for (var p : l) {
            if (p.age < 30) {
                continue;
            }
            res.add(create());
        }
    }

    // Junit のテストアノテーションで Runner を設定する
    @Test
    void benchMark() throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(StreamVsListLazyPerfTest.class.getSimpleName())
                .forks(1) // 1回実行
                .warmupIterations(1) // 1回繰り返し
                .build();
        new Runner(opt).run();
    }

    @AllArgsConstructor
    @Data
    class Parson {
        private String name;
        private int age;
        private String addr;
        private String addr2;
    }

    Parson create() {
        return new Parson(RandomStringUtils.randomAlphabetic(10), new Random().nextInt(100), RandomStringUtils.randomAlphabetic(20), RandomStringUtils.randomAlphabetic(20));
    }

}
```