Help us understand the problem. What is going on with this article?

Java8のStreamとParallelStreamの使い分けについて

More than 5 years have passed since last update.

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした