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編 で使用した、quickperf と Javaでベンチマーク(性能測定) JUnit5編 で紹介した jmh を使いました
Stream API の parallel を推したいのですが、なかなか結果がついてこないので、今回は欲張って1万件、10万件も検証しました。
スコアは、parallelなしの Stream API に比べ、parallelありの場合が、1万件でやや劣勢、10万件で逆転しました
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));
}
}