2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaAdvent Calendar 2021

Day 16

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

Posted at

Javaのカレンダー | Advent Calendar 2021の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 ストリーム で検証したように、1000件以上のオブジェクトを処理してみます

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

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

比較したコード

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

Stream API の場合

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

Stream API で parallel をつけた場合

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

List を使う場合

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

測定結果

測定には、以前の記事 Javaでヒープサイズ測定 JUnit5編 で使用した、quickperfJavaでベンチマーク(性能測定) JUnit5編 で紹介した 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

付録

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

@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));
    }

}

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

@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));
    }

}
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?