Java 8とラムダを使用すると、コレクションをストリームとして反復処理することが容易になります。また、パラレルストリームを使用することも簡単にできます。 docs からの2つの例、parallelStreamを使用した2番目の例:
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()));
順序を気にしない限り、並列を使用することは常に有益ですか?作業をより多くのコアに分割するのが早いと思います。
他に考慮事項がありますか?パラレルストリームはいつ使用すべきですか、そして、非パラレルはいつ使用すべきですか?
(この質問は、並列ストリームをいつどのように使用するかについての議論を引き起こすためのものです。常に使用するのは良い考えではないと思います。)
パラレルストリームは、シーケンシャルストリームに比べてはるかに高いオーバーヘッドを持ちます。スレッドの調整にはかなりの時間がかかります。私はデフォルトでシーケンシャルストリームを使用し、次の場合にのみパラレルストリームを検討します。
大量のアイテムを処理する必要があります(または各アイテムの処理に時間がかかり、並列処理が可能です)。
そもそもパフォーマンスの問題があります
マルチスレッド環境ではまだプロセスを実行していません(例えば、Webコンテナーで、並行して処理する要求が既に多数ある場合は、各要求の内部に並列処理の層を追加すると、プラスの効果よりもマイナスの影響があります)
あなたの例では、パフォーマンスはとにかくSystem.out.println()
への同期化されたアクセスによって動かされるでしょう、そしてこのプロセスを並行して行っても効果がないか、あるいは悪いことさえあります。
さらに、並列ストリームがすべての同期問題を魔法のように解決するわけではないことを忘れないでください。プロセスで使用される述語と関数によって共有リソースが使用される場合は、すべてがスレッドセーフであることを確認する必要があります。特に、副作用とは、並行して使用する場合に本当に心配しなければならないものです。
いずれにせよ、測定、推測しないでください!並列処理がそれだけの価値があるかどうかは、測定値によってのみわかります。
Stream APIは、実行方法から抽象化された方法で計算を簡単に記述できるように設計されているため、順次と並列の切り替えが容易になります。
しかし、それが簡単だからといって、常に良い考えを意味するわけではありません。実際、できる限り単純に.parallel()
を落とすのは 悪い という考えです。
まず、より多くのコアが利用可能な場合、並列処理はより高速な実行の可能性以外に利点をもたらさないことに注意してください。問題を解決することに加えて、それはサブタスクのディスパッチと調整を実行しなければならないので、並列実行は常に逐次的なものより多くの仕事を必要とするでしょう。希望は、複数のプロセッサにまたがって作業を分割することで、より早く答えに到達できることです。これが実際に行われるかどうかは、データセットのサイズ、各要素に対してどれだけの量の計算を行っているか、計算の性質(具体的には、1つの要素の処理が他の要素の処理と相互作用しますか?) 、利用可能なプロセッサの数、およびそれらのプロセッサと競合する他のタスクの数。
さらに、並列処理は、逐次的な実装では隠されていることが多い計算において、しばしば非決定性を露呈することにも注意してください。時にはこれは重要ではない、あるいは関連する操作を制限することによって軽減することができます(すなわち、簡約演算子はステートレスで連想的でなければなりません)。
実際には、並列処理によって計算速度が向上することもあれば、実行速度が低下することもありますし、遅くなることもあります。最初に逐次実行を使用して開発し、次に(A)パフォーマンスの向上に実際に利点があることがわかっている場合、および(B)実際にパフォーマンスの向上をもたらす場合に並列処理を適用することをお勧めします。 (A)はビジネス上の問題であり、技術的な問題ではありません。あなたがパフォーマンスの専門家であれば、通常コードを見て(B)を決定することができますが、賢明な方法は測定することです。 (そして、(A)が納得できるまでは気にしないでください。コードが十分速い場合は、他の場所に脳のサイクルを適用したほうがよいでしょう。)
並列処理の最も単純なパフォーマンスモデルは "NQ"モデルです。ここで、Nは要素数、Qは要素ごとの計算です。一般に、パフォーマンス上の利点を得る前に、製品のNQがしきい値を超える必要があります。 "1からNまでの数字を足し合わせる"のような低Q問題では、一般的にN = 1000とN = 10000の間の損益を見るでしょう。 Qが高い問題では、低いしきい値で損益分岐点が表示されます。
しかし現実はかなり複雑です。したがって、専門知識を習得するまでは、最初に逐次処理によって実際にコストが発生している時期を特定し、次に並列処理が役立つかどうかを測定します。
私は presentation of Brian Goetz (Java Language Architect&Lambda Expressionsの仕様リード)のうちの1つを見ました。彼は、並列化に進む前に考慮すべき次の4つの点について詳細に説明します。
分割/分解コスト
- 場合によっては、分割するだけで作業を行うよりも費用がかかります。
タスクの派遣/管理コスト
- 作業を別のスレッドに渡すのにかかる時間内に多くの作業を実行できます。
結果の組み合わせコスト
- 結合には、大量のデータのコピーが含まれることがあります。たとえば、数字を追加するのは安価で、集合をマージするのは高価です。
ローカリティ
- 部屋の中の象。これは誰もが見逃す可能性がある重要な点です。キャッシュミスを考慮する必要があります。キャッシュミスのためにCPUがデータを待機している場合は、並列化によって何も得られません。そのため、次のインデックス(現在のインデックス付近)がキャッシュされ、CPUがキャッシュミスを経験する可能性が少なくなるため、配列ベースのソースが最適化されます。
彼はまた、並列スピードアップの可能性を判断するための比較的単純な式にも言及しています。
NQモデル :
N x Q > 10000
ここで、
N =データ数
Q = 1項目あたりの作業量
JBは頭の上に釘を打った。私が追加できる唯一のことは、Java8が純粋な並列処理を行わないことです。それは paraquential はいです。
無限のストリームを制限付きで並列化しないでください。ここで何が起こるかです:
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());
}
これは、並列スレッドが0〜100の重要な番号ではなく、多数の番号範囲で動作する可能性があり、非常に長い時間がかかるため、はるかに長く実行される可能性があります。
他の回答では、並列処理における時期尚早の最適化とオーバーヘッドコストを回避するためのプロファイリングをすでに取り上げています。この回答は、パラレルストリーミング用のデータ構造の理想的な選択を説明しています。
原則として、並列処理によるパフォーマンスの向上は、
ArrayList
、HashMap
、HashSet
、およびConcurrentHashMap
インスタンスを介したストリームで最も効果的です。配列int
の範囲とlong
の範囲。これらのデータ構造に共通しているのは、それらをすべて正確かつ安価に任意のサイズのサブレンジに分割できることです。これにより、並列スレッド間で作業を簡単に分割できます。このタスクを実行するためにstreamsライブラリによって使用される抽象化はspliteratorです。これはspliterator
およびStream
のIterable
メソッドによって返されます。これらすべてのデータ構造に共通しているもう1つの重要な要素は、順次処理されたときに優れた優れた局所性を提供することです。順次要素参照はメモリにまとめて格納されます。これらの参照によって参照されるオブジェクトは、メモリー内で互いに接近していない可能性があり、これにより参照の局所性が低下します。参照の局所性は、一括操作を並列化するために非常に重要であることがわかります。それがないと、スレッドはメモリからプロセッサのキャッシュにデータが転送されるのを待つためにアイドル時間の多くを費やします。データ自体がメモリに連続して格納されているため、最良の参照ローカリティを持つデータ構造はプリミティブ配列です。
出典:Item#48ストリームを並列にし、効果的なJava 3eにするときには注意を使うJoshua Bloch