Arrays.parallelPrefix Java 8。
このオーバーロードメソッドは、入力配列の各要素に対して累積的に操作を実行します。たとえばドキュメントから:
提供された関数を使用して、指定された配列の各要素を所定の位置に並行して累積します。たとえば、配列が最初に[2、1、0、3]を保持し、演算が加算を実行する場合、戻り時に配列は[2、3、3、6]を保持します。並列プレフィックス計算は、通常、大きな配列のシーケンシャルループよりも効率的です。
では、用語の操作が前の用語の操作結果に依存している場合など、Java parallel
でこのタスクをどのように実現しますか?
自分でコードを試してみましたが、ForkJoinTasks
を使用していますが、結果をマージして最終的な配列を取得する方法はそれほど簡単ではありません。
Eran's answer で説明されているように、この操作は関数の結合性プロパティを利用します。
次に、2つの基本的な手順があります。最初のものは、実際の前置演算(評価のために前の要素を必要とするという意味で)であり、配列の一部に並列に適用されます。各部分演算の結果(結果の最後の要素と同じ)は、残りの配列のオフセットです。
例えば。次の配列に対して、プレフィックス操作として合計を使用し、4つのプロセッサ
4 9 5 1 0 5 1 6 6 4 6 5 1 6 9 3
我々が得る
4 → 13 → 18 → 19 0 → 5 → 6 → 12 6 → 10 → 16 → 21 1 → 7 → 16 → 19
↓ ↓ ↓ ↓
19 12 21 19
次に、結合性を利用して、最初にプレフィックス操作をオフセットに適用します
↓ ↓ ↓ ↓
19 → 31 → 52 → 71
次に、次のチャンクの各要素にこれらのオフセットを適用する第2フェーズに進みます。これは、前の要素への依存関係がなくなったため、完全に並列化可能な操作です。
19 19 19 19 31 31 31 31 52 52 52 52
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
4 13 18 19 19 24 25 31 37 41 47 52 53 59 68 71
8つのスレッドに同じ例を使用すると、
4 9 5 1 0 5 1 6 6 4 6 5 1 6 9 3
4 → 13 5 → 6 0 → 5 1 → 7 6 → 10 6 → 11 1 → 7 9 → 12
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
13 6 5 7 10 11 7 12
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
13 → 19 → 24 → 31 → 41 → 52 → 59 → 71
13 13 19 19 24 24 31 31 41 41 52 52 59 59
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
4 13 18 19 19 24 25 31 37 41 47 52 53 59 68 71
両方のステップでワークチャンクを同じに保つという単純な戦略を使用した場合、つまり、2番目のフェーズで1つのアイドルワーカースレッドを受け入れる場合でも、明確なメリットがあることがわかります。最初のフェーズには約⅛nが必要であり、2番目のフェーズには⅛nが必要です。操作には合計¼nが必要です(nは順次プレフィックス評価のコストです)配列全体の)。もちろん、大雑把かつ最良の場合のみです。
対照的に、プロセッサが2つしかない場合
4 9 5 1 0 5 1 6 6 4 6 5 1 6 9 3
4 → 13 → 18 → 19 → 19 → 24 → 25 → 31 6 → 10 → 16 → 21 → 22 → 28 → 37 → 40
↓ ↓
31 40
↓ ↓
31 → 71
31 31 31 31 31 31 31 31
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
4 13 18 19 19 24 25 31 37 41 47 52 53 59 68 71
利益を得ることができるのは、第2フェーズの作業を再割り当てするときだけです。前述のとおり、これは可能です。なぜなら、第2フェーズの作業には要素間の依存関係がなくなるためです。そのため、この操作を任意に分割できますが、実装が複雑になり、追加のオーバーヘッドが発生する可能性があります。
2番目のフェーズの作業を両方のプロセッサに分割する場合、最初のフェーズには約½nが必要で、2番目のフェーズには¼nが必要で、合計が¾nになります。
追加の注意として、2番目のフェーズの準備で計算されたオフセットは、チャンクの最後の要素の結果と同じであることに気付くかもしれません。そのため、単純にその値を割り当てることにより、必要な操作の数をチャンクごとに1つ減らすことができます。しかし、典型的なシナリオは、多数の要素を持つ少数のチャンク(プロセッサの数に応じたスケーリング)のみを使用することであるため、チャンクごとに1つの操作を保存することは関係ありません。
主なポイントは、演算子が
副作用なし、結合的関数
この意味は
(a op b) op c == a op (b op c)
したがって、配列を2つに分割し、各半分にparallelPrefix
メソッドを再帰的に適用する場合、配列の後半の各要素と最後の要素に操作を適用することにより、部分的な結果を後でマージできます前半の。
考えます [2, 1, 0, 3]
追加例付き。配列を2つに分割し、各半分で操作を実行すると、次のようになります。
[2, 3] and [0, 3]
次に、それらをマージするために、後半の各要素に3(前半の最後の要素)を追加し、以下を取得します。
[2, 3, 3, 6]
編集:この答えは、配列のプレフィックスを並列に計算する1つの方法を示唆しています。これは必ずしも最も効率的な方法ではなく、JDK実装で使用される方法でもありません。その問題を解決するための並列アルゴリズムについてさらに読むことができます こちら 。
私は両方の答えを読みましたが、まだこれがどのように行われるかを完全に理解できなかったので、代わりに例を描くことにしました。ここに私が思いついたものがあります、これが私たちが始めたアレイであると仮定してください(3つのCPUで):
7, 9, 6, 1, 8, 7, 3, 4, 9
したがって、3つのスレッドのそれぞれは、動作するチャンクを取得します。
Thread 1: 7, 9, 6
Thread 2: 1, 8, 7
Thread 3: 3, 4, 9
ドキュメントではassociative関数が義務付けられているため、最初のThreadで合計を計算し、1で一部の部分合計を計算できます。 7, 9, 6
は次のようになります。
7, 9, 6 -> 7, 16, 22
したがって、最初のスレッドの合計は22
-しかし、他のスレッドはまだそれについて考えていないので、代わりに行うことは、例えばx
としてそれに対して動作します。したがって、スレッド2は次のようになります。
1, 8, 7 -> 1 (+x), 9 (+x), 16(+x)
したがって、2番目のスレッドの合計はx + 16
、したがってThread 3
、次のようになります。
3, 4, 9 -> 3 (+ x + 16), 7 (+ x + 16), 16 (+ x + 16)
3, 4, 9 -> x + 19, x + 23, x + 32
このようにして、x
を知るとすぐに、他のすべての結果もわかります。
免責事項:これがどのように実装されているのかわかりません(そしてコードを見てみましたが、あまりにも複雑です)。