Java 惰性评估

这是2021年Java日历 | Advent Calendar第16天的文章。

我会永远选择一个懒人来做一个困难的工作,因为他会找到一个简单的方法来完成它 – 比尔·盖茨

※ 因为懒惰的人能找到更简便的方式,所以我总是把困难的工作交给他们。- 比尔·盖茨

总结

我们来看一下通过Stream API执行Java的延迟评估(lazy evaluation)的性能。

如同使用Java Stream进行验证一样,我们将尝试处理1000个以上的对象。

我的观感是,总的来说,结果表明使用 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());
    }

使用列表的情况下

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

测量结果

在此次测试中,我们使用了之前在“Javaでヒープサイズ測定 JUnit5編”中介绍的quickperf和在“Javaでベンチマーク(性能測定) JUnit5編”中介绍的jmh。

我想推荐使用Stream API的parallel功能,但由于结果迟迟不出来,所以这次我贪心地进行了1万条和10万条的验证。
相比没有parallel的Stream API,得分在1万条时稍逊一筹,但在10万条时却反超了。

JVM 的堆大小

一千件

[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

一萬件 (yī

[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 (単位が違う)

一万件

[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 (単位が違う)

基准测试

一千件

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

一万件

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

10万件

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

}
广告
将在 10 秒后关闭
bannerAds