可能な場合は常に並列ストリームを使用する必要がありますか?


514

Java 8とラムダを使用すると、コレクションをストリームとして反復するのが簡単で、並列ストリームを使用するのと同じくらい簡単です。docsの 2つの例、2番目の例はparallelStreamを使用しています。

myShapesCollection.stream()
    .filter(e -> e.getColor() == Color.RED)
    .forEach(e -> System.out.println(e.getName()));

myShapesCollection.parallelStream() // <-- This one uses parallel
    .filter(e -> e.getColor() == Color.RED)
    .forEach(e -> System.out.println(e.getName()));

順序を気にしない限り、パラレルを使用することは常に有益ですか?より多くのコアで作業を分割する方が速いと思う人もいるでしょう。

他の考慮事項はありますか?並列ストリームはいつ使用し、非並列ストリームはいつ使用する必要がありますか?

(この質問は、並列ストリームをいつどのように使用するかについてのディスカッションをトリガーするためのものです。常にそれらを使用することは良い考えだと思うからではありません。)

回答:


735

並列ストリームは、順次ストリームに比べてオーバーヘッドがはるかに高くなります。スレッドの調整にはかなりの時間がかかります。デフォルトでは順次ストリームを使用し、並列ストリームのみを考慮します

  • 処理するアイテムが大量にある(または各アイテムの処理に時間がかかり、並列化できる)

  • そもそもパフォーマンスに問題がある

  • マルチスレッド環境でプロセスをまだ実行していません(例:Webコンテナーで、並列処理する要求が既に多くある場合、各要求内に並列処理のレイヤーを追加すると、プラスの効果よりもマイナスの影響が出る可能性があります。 )

あなたの例では、パフォーマンスはとにかく同期されたアクセスによって駆動されます System.out.println()このプロセスを並列化しても効果はありません。

さらに、並列ストリームがすべての同期問題を魔法のように解決するわけではないことを覚えておいてください。共有リソースがプロセスで使用される述語と関数によって使用される場合、すべてがスレッドセーフであることを確認する必要があります。特に、副作用は、並行して実行する場合に本当に心配する必要があるものです。

いずれにせよ、測定して、推測しないでください!並列処理がそれだけの価値があるかどうかは、測定のみでわかります。


18
いい答えです。処理するアイテムが大量にある場合、スレッドの調整の問題が増えるだけです。各項目の処理に時間がかかり、並列化が可能な場合にのみ、並列化が役立ちます。
Warren Dew

16
@WarrenDew同意しない。Fork / Joinシステムは、Nアイテムをたとえば4つの部分に単純に分割し、これらの4つの部分を順次処理します。その後、4つの結果が削減されます。大量のものが本当に巨大な場合、高速のユニット処理であっても、並列化は効果的です。しかし、いつものように、測定する必要があります。
JB Nizet 2014

私はそれらをとして使用するためRunnableに呼び出す実装するオブジェクトのコレクションを持っています、それを並列化されたjava 8ストリームを使用するように変更しても大丈夫ですか?その後、スレッドコードをクラスから取り除くことができます。しかし、欠点はありますか?start()Threads.forEach()
ycomp

1
@JBNizet 4つのパーツが連続して処理される場合、プロセスの並列化または順次の認識の違いはありませんか?Plsの明確化
Harshana

3
@Harshana彼は明らかに、4つの部分のそれぞれの要素が順次処理されることを意味します。ただし、パーツ自体は同時に処理できます。言い換えると、複数のCPUコアを使用できる場合、各部分は他の部分とは無関係に独自のコア上で実行でき、同時に独自の要素を順次処理します。(注:わからない、これが並列Javaストリームの動作方法である場合、私はJBNizetの意味を明確にしようとしているだけです。)
明日

258

Stream APIは、実行方法から抽象化された方法で計算を簡単に記述できるように設計されており、シーケンシャルとパラレルの切り替えを簡単にします。

ただし、簡単だからといって、常に良いアイデアであるとは限りません。実際、できることだけで場所全体にドロップするのは悪い考え.parallel()です。

まず、並列処理は、より多くのコアが利用可能な場合に実行が高速になる可能性以外に利点がないことに注意してください。並列実行では、問題を解決するだけでなく、サブタスクのディスパッチと調整も実行する必要があるため、常に逐次実行よりも多くの作業が必要になります。複数のプロセッサー間で作業を分割することで、より早く答えに到達できることが期待されます。これが実際に発生するかどうかは、データセットのサイズ、各要素で実行している計算量、計算の性質など、多くのことに依存します(特に、ある要素の処理は他の要素の処理と相互作用しますか?) 、利用可能なプロセッサの数、およびそれらのプロセッサをめぐって競合する他のタスクの数。

さらに、並列処理は、多くの場合、シーケンシャル実装によって隠されている計算の非決定性も公開することに注意してください。時々これは問題にならないか、含まれる操作を制約することで軽減できます(つまり、リダクション演算子はステートレスで結合的でなければなりません)。

実際には、並列処理によって計算が高速化する場合もあれば、そうでない場合もあります。最初に順次実行を使用して開発し、次に並列処理を適用するのが最善です。

(A)パフォーマンスの向上には実際にメリットがあることと、

(B)実際にパフォーマンスが向上すること。

(A)はビジネス上の問題であり、技術的な問題ではありません。パフォーマンスの専門家であれば、通常はコードを見て(B)を判別できますが、測定はスマートパスです。(そして、(A)を確信するまで気にしないでください。コードが十分に高速である場合は、脳のサイクルを他の場所に適用することをお勧めします。)

並列処理の最も単純なパフォーマンスモデルは "NQ"モデルです。Nは要素の数、Qは要素ごとの計算です。一般に、パフォーマンス上の利点を得る前に、製品NQがあるしきい値を超える必要があります。「1からNまでの数値を合計する」のような低Qの問題の場合、通常、N = 1000とN = 10000の間で損益分岐点が見られます。Qの高い問題では、低いしきい値で損益分岐点が見られます。

しかし、現実はかなり複雑です。したがって、専門知識を得るまで、最初に順次処理が実際にコストをかけている時期を特定し、次に並列処理が役立つかどうかを測定します。


18
この投稿では、NQモデルの詳細について説明します:gee.cs.oswego.edu/dl/html/StreamParallelGuidance.html
Pino

4
@specializt:ストリームをシーケンシャルからパラレルに切り替えると、アルゴリズム変更されます(ほとんどの場合)。ここで言及されている決定論は、(任意の)演算子依存する可能性のあるプロパティに関するものです(Stream実装はそれを知ることができません)が、もちろん依存すべきではありません。それが、この回答のそのセクションが述べようとしたことです。あなたがルールを気にした場合、あなたは(それ以外の並列ストリームはかなり役に立たなかった)、と言うが、意図的に許容される非決定論の可能性が使用しているときのように、また、そこにちょうど同じように、決定論的結果を持つことができるfindAny代わりにfindFirst...
ホルガー

4
「まず、並列処理は、より多くのコアが利用可能な場合に実行が高速になる可能性以外に利点がないことに注意してください」、またはIOを含むアクション(例:)を適用する場合 myListOfURLs.stream().map((url) -> downloadPage(url))...
Jules

6
@Pacerierこれは素晴らしい理論ですが、悲しいことに素朴です(まず、自動並列化コンパイラーを構築する試みの30年の歴史を参照してください)。私たちが間違いを犯したときにユーザーに迷惑をかけないように十分な時間を推測することは現実的ではないので、責任のあることは、ユーザーに自分のやりたいことを言わせるだけでした。ほとんどの状況では、デフォルト(順次)が適切であり、より予測可能です。
Brian Goetz 2017年

2
@Jules:IOに並列ストリームを使用しないでください。これらは、CPUを集中的に使用する操作専用です。並列ストリームを使用ForkJoinPool.commonPool()していて、ブロックタスクがそこに行きたくない場合。
R2C2

68

Brian Goetz (Java言語アーキテクト&ラムダ式の仕様リード)プレゼンテーションの1つを見ました。並列化に進む前に考慮すべき次の4つのポイントについて詳しく説明しています。

分割/分解コスト
–分割は、単に作業を行うよりも高価な場合があります。
タスクのディスパッチ/管理コスト
–作業を別のスレッドに渡すのにかかる時間で多くの作業を行うことができます。
結果の組み合わせコスト
–組み合わせには、大量のデータのコピーが含まれる場合があります。たとえば、数値の追加は安価ですが、セットのマージには費用がかかります。
場所
-部屋の象。これは誰もが見逃すかもしれない重要なポイントです。キャッシュミスを考慮する必要があります。CPUがキャッシュミスのためにデータを待機している場合、並列化によって何も得られません。そのため、次のインデックス(現在のインデックスの近く)がキャッシュされ、CPUがキャッシュミスを経験する可能性が少なくなるため、配列ベースのソースが最適に並列化されます。

また、並列スピードアップの可能性を判断するための比較的単純な式についても言及しています。

NQモデル

N x Q > 10000

ここで、
N =データ項目の数
Q =項目ごとの作業量


13

JBは頭の爪を打ちました。私が追加できる唯一のことは、Java 8は純粋な並列処理を行わないということです。はい、記事を書き、F / Jを30年間行っているので、問題を理解しています。


10
ストリームは外部ではなく内部反復を行うため、反復可能ではありません。とにかくストリームの理由はそれだけです。学業に問題がある場合は、関数型プログラミングが適していない可能性があります。関数型プログラミング===数学===アカデミック。いいえ、J8-FJは壊れていません。ほとんどの人がf ******マニュアルを読んでいないだけです。Javaドキュメントは、それが並列実行フレームワークではないことを非常に明確に述べています。それがすべてのスプリッター機能の理由です。はい、それは学術的であり、はい、それを使用する方法を知っていれば機能します。はい、カスタムエグゼキュータを使用する方が簡単です
Kr0e

1
Streamにはiterator()メソッドがあるため、必要に応じて外部で反復できます。私の理解では、イテレータは1回しか使用できず、それが問題ないかどうか誰も判断できないため、Iterableは実装されていません。
Trejkaz

14
正直に言うと、あなたの論文全体が巨大で精巧な怒りのように読まれる-そしてそれはその信憑性をかなり否定する...私ははるかに積極的でない低調でそれをやり直すことをお勧めします。... im just sayan
specializ

記事に関するいくつかの質問...まず第一に、どうしてバランスの取れたツリー構造を有向非巡回グラフと見かけ上等しくするのですか?はい、バランスツリー DAGですが、リンクリストと、配列以外のほとんどすべてのオブジェクト指向データ構造もそうです。また、再帰的分解がバランスの取れたツリー構造でのみ機能し、したがって商業的に関連がないと言う場合、そのアサーションをどのように正当化しますか?それは配列ベースのデータ構造、たとえば/ でも同様に機能するはずです(確かに問題を詳細に検討することなく)。ArrayListHashMap
Jules

1
このスレッドは2013年のもので、それ以来多くの変更があります。このセクションは詳細な回答ではなくコメント用です。
16

3

他の回答では、並列処理における時期尚早の最適化とオーバーヘッドコストを回避するためのプロファイリングがすでにカバーされています。この回答は、並列ストリーミングのデータ構造の理想的な選択を説明しています。

原則として、並列処理によるパフォーマンスの向上は、上のストリームに最適ですArrayListHashMapHashSet、およびConcurrentHashMapインスタンス。配列; int範囲; とlong範囲。これらのデータ構造に共通しているのは、データ構造をすべて正確かつ安価に任意のサイズのサブ範囲に分割できるため、並列スレッド間で作業を簡単に分割できることです。ストリームライブラリがこのタスクを実行するために使用する抽象化はspliteratorで、spliteratoron Streamおよびand メソッドによって返されます。Iterableます。

これらのデータ構造のすべてに共通するもう1つの重要な要素は、順次処理したときに参照の局所性が優れていることです。順次要素参照は一緒にメモリに格納されます。これらの参照によって参照されるオブジェクトは、メモリ内で互いに近接していない可能性があり、参照の局所性が低下します。参照の局所性は、一括操作の並列化にとって非常に重要であることがわかります。それがないと、スレッドは多くの時間をアイドル状態で費やし、メモリからプロセッサのキャッシュにデータが転送されるのを待ちます。参照の局所性が最も高いデータ構造はプリミティブ配列です。これは、データ自体が連続してメモリに格納されるためです。

出典:項目#48 Joshua Blochによるストリームの並列化、効果的なJava 3eの作成には注意が必要


2

無限ストリームを制限付きで並列化しないでください。これが起こることです:

    public static void main(String[] args) {
        // let's count to 1 in parallel
        System.out.println(
            IntStream.iterate(0, i -> i + 1)
                .parallel()
                .skip(1)
                .findFirst()
                .getAsInt());
    }

結果

    Exception in thread "main" java.lang.OutOfMemoryError
        at ...
        at java.base/java.util.stream.IntPipeline.findFirst(IntPipeline.java:528)
        at InfiniteTest.main(InfiniteTest.java:24)
    Caused by: java.lang.OutOfMemoryError: Java heap space
        at java.base/java.util.stream.SpinedBuffer$OfInt.newArray(SpinedBuffer.java:750)
        at ...

使っても同じ .limit(...)

ここでの説明: Java 8、ストリームで.parallelを使用するとOOMエラーが発生する

同様に、ストリームが順序付けされており、処理したい要素がはるかに多い場合は、並列を使用しないでください。例:

public static void main(String[] args) {
    // let's count to 1 in parallel
    System.out.println(
            IntStream.range(1, 1000_000_000)
                    .parallel()
                    .skip(100)
                    .findFirst()
                    .getAsInt());
}

並列スレッドは、重要な1から0ではなく、多数の数値範囲で機能する可能性があるため、実行時間が長くなり、非常に長い時間がかかる可能性があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.