#はじめに
Java 8のStream APIの性能について簡潔にまとめた資料がないので、自前で2種類のベンチマークを作成しました。
No. | ベンチマークの内容 | 処理の粒度 | Stream不使用 | Streamシリアル | Streamパラレル |
---|---|---|---|---|---|
1 | 1~nの総和($\sum_{i=1}^n i$)を求める | 小 | ○ | ○(△) | × |
2 | レーベンシュタイン距離を求める | 大 | × | × | ○ |
No.1は、IntStream.range(0, n).sum()などを使った小粒度の処理で、
No.2は、大量の単語についてレーベンシュタイン距離を計算する大粒度の処理です。
それぞれのベンチマークについて、Streamを使わない処理、Streamを利用したシリアル処理、Streamを利用したパラレル処理の3種類で速度性能を採取しました。
Collection.stream().中間操作()...終端操作();
Collection.parallelStream().中間操作()...終端操作();
結果、速度性能が良かったものの順に「○→△→×」を上表に記載しました。
速度性能結果の考察
##粒度が小さい処理の場合
中間操作~終端操作の一連の処理のコストが小さい場合は、JITによる最適化が効くまでの間は「Streamを使わない処理」が最も速度性能が優れています。
しかし、サーバなど長期運用ではJITによる最適化により「Streamを利用したのシリアル処理」の速度性能は「Streamを使わない処理」と同程度になります。
なお、「Streamを利用したパラレル処理」の場合、パラレル制御のオーバーヘッドが大半を占めるため、JITによって最適化されても速度性能が優れることは望めません。
##粒度が大きい処理の場合
中間操作~終端操作の一連の処理のコストが大きい場合は、「Streamを使用したパラレル処理」を積極的に検討してよい。
ただし、念のためにシリアル処理とパラレル処理の性能比較はしておいた方がよい。
##Streamを利用したパラレル処理を行わない方がよい処理
次のようなケースはパラレル化しない方がよい。
- ラムダ式から外部変数にアクセスするもの
- サイズが未定のもの(Stream#iterateで生成した場合など)
- 順番が必要なもの
##for文の単純なIntStream.forEach()への置換
for(int i=0; i<100; i++) {
小粒度の処理
}
IntStream
.range(0, 100)
.forEach(小粒度の処理)
後者のIntStream.forEach()にはメリットがなく、多用するとオブジェクトを多く生成するため、ガーベジコレクションが頻発しやすくなります。
また、可読性と性能の両方の観点からしても、前者のfor文の方がよいのではないでしょうか。