03/06 追記
この記事についてTwitterで言及があったので、まとめのURL貼っておきますね。
streamとparallelStreamについての考察 - Togetterまとめ
なんかいろいろ間違ってるというかStreamへの理解が全然足りないっぽいです!ごめんなさい!
Java8! Java8!
もういくつねると
楽しい方のJavaがリリース間近ですね。その中でも特に便利になりそうな感じのStreamAPI。Lambdaと合わせてCollectionの処理がだいぶやりやすくなったり、 1つメソッドを変えるだけで並列処理ができる とか!すごい!
今までJavaで並列処理するときは色々考えることが多くて大変だったけど今後はその辺もろもろ考慮した上で並列化してくれるからこっちは何も考えずただひたすら parallelStream()
呼んでおけばOK!!!
……とはさすがにならないので、 stream()
と parallelStream()
の使い分けどころみたいなのを、マイクロベンチマークで探ってみました。
マイクロベンチマークはOpenJDKのJMHでやりました。JMHの導入については、Javaのマイクロベンチマークツール「JMH」を見るのがいいと思います。
環境
OS : Windows8 Pro 64bit
CPU : Intel Corei5-4670 3.46GHz 4コア/4スレッド
RAM : 8GB
0~Nまでの整数の平均を計算
そんなにコスト高くない処理をStreamでやるパターン。
コードのイメージとしてはこんな感じ。
Stream<Integer> s = ...;
// sequentialStream
s.collect(Collectors.averagingInt(i -> i))
// parallelStream
s.parallel().collect(Collectors.averagingInt(i -> i))
まずはCollectionから
Collectionを、実装クラスと格納した要素数ごとに複数用意して、そこから stream()
を使った場合の処理速度と、 parallelStream()
を使った場合の処理速度を比較してみました。
Stream source | Stream type | 要素数1000 | 要素数10000 | 要素数100000 |
---|---|---|---|---|
ArrayList | sequential | 4414 | 44254 | 407152 |
ArrayList | parallel | 4179 | 9636 | 157191 |
LinkedList | sequential | 3957 | 43425 | 474494 |
LinkedList | parallel | 7276 | 53725 | 416986 |
CopyOnWriteArrayList | sequential | 4339 | 37482 | 392907 |
CopyOnWriteArrayList | parallel | 4006 | 17295 | 134230 |
HashSet | sequential | 7886 | 72275 | 985102 |
HashSet | parallel | 7519 | 31377 | 441599 |
LinkedHashSet | sequential | 6601 | 65976 | 490590 |
LinkedHashSet | parallel | 8708 | 58715 | 667024 |
TreeSet | sequential | 6237 | 61928 | 708406 |
TreeSet | parallel | 6947 | 38485 | 336940 |
ConcurrentSkipListSet | sequential | 4342 | 38216 | 410524 |
ConcurrentSkipListSet | parallel | 3820 | 17729 | 135994 |
CopyOnWriteArraySet | sequential | 4368 | 43979 | 532631 |
CopyOnWriteArraySet | parallel | 11696 | 26282 | 188514 |
(単位はすべてnsec.) |
全体的に要素数が少ないほどparallelの効果も薄いし、実装によってはsequentialのほうが遅い場合もありますね。
要素数が多くなっても、parallelの効果がそれほど出ないものもあります。
あと何故かLinkedHashSetだけ10000になるとparallelのほうが速いのに、100000になるとまた逆転する。
理由としては、
- スレッド生成コストと、タスクスイッチコストが増える。
- Collectionの内部実装的に並列的に処理しづらい(LinkedListとか)
のあたりみたいなところが考えられるかと思います。
Collection以外のパターン
次はCollection以外から生成したStreamでそのまま処理した場合と、 parallel()
を呼んでから処理した場合で、処理時間を比較してみます。
Collection以外というとだいたいこんなパターンって思いつくものを挙げてみましたが、無理やりIntegerにするために不自然なコードになってしまった感がちょっとありますね。
// rangeでIntStream生成 -> Stream<Integer>へ変換
range = IntStream.range(0, i).boxed();
// int配列からIntStream生成 -> Stream<Integer>へ変換
boxed = Arrays.stream(IntStream.range(0, i).toArray()).boxed();
// boxed = IntStream.of(IntStream.range(0, i).toArray()).boxed();
// Integerの配列からStream<Integer>を生成
array = Arrays.stream(IntStream.range(0, i).boxed().toArray(Integer[]::new));
// array = Stream.of(IntStream.range(0, i).boxed().toArray(Integer[]::new));
// 無限リスト
iterate = Stream.iterate(0, e -> ++e).limit(i);
上記4種類のストリームに対して、さっきと同じ処理をした結果。
Stream source | Stream type | 要素数1000 | 要素数10000 | 要素数100000 |
---|---|---|---|---|
range | sequential | 7411 | 76572 | 573449 |
range | parallel | 9315 | 30791 | 274682 |
boxed | sequential | 4027 | 39228 | 481309 |
boxed | parallel | 8006 | 24815 | 180485 |
array | sequential | 1291 | 11936 | 129851 |
array | parallel | 6120 | 14975 | 73327 |
iterate | sequential | 11732 | 96977 | 970426 |
iterate | parallel | 23082 | 119943 | 624727 |
やっぱり全体的に要素数が少ないとparallelのほうが遅い。
しかしStream.ofが速いですね。引数に渡した配列をそのまま処理しているからだと思うけど。
IntStreamで
ちなみにIntStreamでやるとこんな感じです。
range = IntStream.range(0, i);
array = Arrays.stream(IntStream.range(0, i).toArray());
// array = IntStream.of(IntStream.range(0, i).toArray());
iterate = IntStream.iterate(0, e -> e++).limit(i);
// 平均値計算
IntStream s = ...;
s.average().orElse(0);
Stream source | Stream type | 要素数1000 | 要素数10000 | 要素数100000 |
---|---|---|---|---|
range | sequential | 3226 | 9550 | 241349 |
range | parallel | 4949 | 16534 | 108291 |
array | sequential | 965 | 8305 | 83726 |
array | parallel | 6073 | 10278 | 39848 |
iterate | sequential | 6478 | 64082 | 239722 |
iterate | parallel | 22271 | 101978 | 825713 |
0~Nまでの整数を文字列化して結合
次はちょっとコスト高めの処理をStreamでやります。
コードのイメージとしてはこんな感じ。
Stream<Integer> s = ...;
// sequentialStream
s.map(Object::toString).collect(Collectors.joining(","));
// parallelStream
s.parallel().map(Object::toString).collect(Collectors.joining(","));
Collection
先ほどと同じく、実装クラスと要素数ごとに複数Collectionを用意して計測していきます。
Stream source | Stream type | 要素数1000 | 要素数10000 | 要素数100000 |
---|---|---|---|---|
ArrayList | sequential | 38272 | 400976 | 5287859 |
ArrayList | parallel | 27007 | 186418 | 2431721 |
LinkedList | sequential | 37602 | 432568 | 5076029 |
LinkedList | parallel | 24805 | 232773 | 4715496 |
CopyOnWriteArrayList | sequential | 37564 | 442875 | 4589813 |
CopyOnWriteArrayList | parallel | 23618 | 197381 | 2404578 |
HashSet | sequential | 41093 | 432061 | 5747497 |
HashSet | parallel | 23101 | 207755 | 2542056 |
LinkedHashSet | sequential | 42117 | 407693 | 5153880 |
LinkedHashSet | parallel | 27979 | 233375 | 4868135 |
TreeSet | sequential | 39580 | 464776 | 4813765 |
TreeSet | parallel | 28905 | 253965 | 2968495 |
ConcurrentSkipListSet | sequential | 38167 | 399155 | 4533977 |
ConcurrentSkipListSet | parallel | 24619 | 200574 | 2417575 |
CopyOnWriteArraySet | sequential | 37855 | 410096 | 5135488 |
CopyOnWriteArraySet | parallel | 34547 | 278889 | 3289177 |
(単位はすべてnsec.) |
今回は全てのパターンで、parallelのほうが速くなりました。
Stringを生成するコストのほうが、スレッド生成などにかかるコストを上回っていることが考えられるかと思います。
Collection以外
Stream source | Stream type | 要素数1000 | 要素数10000 | 要素数100000 |
---|---|---|---|---|
range | 1000 | sequential | 39801 | 418492 |
range | 1000 | parallel | 28578 | 220162 |
boxed | 1000 | sequential | 40142 | 427359 |
boxed | 1000 | parallel | 27697 | 224763 |
array | 1000 | sequential | 37601 | 399364 |
array | 1000 | parallel | 27750 | 209609 |
iterate | 1000 | sequential | 43822 | 481209 |
iterate | 1000 | parallel | 45792 | 319233 |
こっちも要素数が少なくても、だいたいparallelのほうが速いです。ただ、 Stream.iterate
を使用したものは案の定というか遅い場合もありますね。
まとめ
というわけで stream()
と parallelStream()
の使い分けについては、
- コストの高い処理をStreamで行う場合
- 処理コストは低いが、要素数が多い場合
- Streamの生成元ソースが、並列実行しやすいものである場合
というところを判断しながらやっていけばいいのではないでしょうか。
また、JMHでテストしている時に当初 @Threads
の設定をミスって4に指定して4スレッドすべてを使い切ってしまい、それによりparallel処理が遅くなってしまっていました。
なので、コア数が多い場合は効果は高いですが、そもそもコア数が少ない場合や、負荷のかかっているコア数が多い場合、そこまでの効果は期待できなさそうですね。
今回マイクロベンチマークに使ったソースはGitHubにおいてあります。コア/スレッド数の違うPCで動かしてみるとまた違った結果が出て面白いかもしれないですね。
https://github.com/clomie/ParallelStreamBenchmark