上記の例はすべて、グラフ内の 2 つの頂点間の最短経路 (2 つの場所間のルートまたはパス、ある場所から別の場所へ 1 枚の紙を取得するアクションの数または複雑さ) を見つける問題を表しています。このクラスの最短経路問題はSSSP (Single Source Shortest Path)と呼ばれ、これらの問題を解決するための基本アルゴリズムは、計算複雑度がO(n^2)
です。
しかし、すべての頂点間の最短経路をすべて見つける必要がある場合もあります。次の例を考えてみましょう。自宅、職場、劇場間の定期的な移動のマップを作成しているとします。このシナリオでは、 work ⭢ home
、 home ⭢ work
、 work ⭢ theatre
、 theatre ⭢ work
、 home ⭢ theatre
、 theatre ⭢ home
の 6 つのルートが作成されます (逆のルートは、一方通行などの理由で異なる場合があります)。
A(k, n) = n! / (n - m)! // where n - is a number of elements, // k - is a number of elements in permutation (in our case k = 2)
これにより、4 つの場所に 12 のルート、10 の場所には 90 のルートが得られます (これは印象的です)。注: これは、場所間の中間点を考慮していない場合です。つまり、自宅から職場に行くには、4 つの道路を渡り、川に沿って歩き、橋を渡る必要があります。いくつかのルートに共通の中間点があると想像してください...そうですね...結果として、多数の頂点を持つ非常に大きなグラフが得られ、各頂点は場所または重要な中間点のいずれかを表します。グラフ内のすべての頂点ペア間の最短経路をすべて見つける必要がある問題のクラスは、 APSP (全ペア最短経路)と呼ばれ、これらの問題を解決するための基本アルゴリズムは、 O(n^3)
の計算複雑度を持つです。
フロイド・ワーシャルアルゴリズムは、グラフ内の頂点のペア間の最短経路をすべて見つけます。このアルゴリズムは、ロバート・フロイドによって [1] で公開されました (詳細については、「参考文献」セクションを参照してください)。同じ年に、ピーター・インガーマンは [2] で、3 つのネストされたfor
ループの形式でアルゴリズムの最新の実装を説明しました。
algorithm FloydWarshall(W) do for k = 0 to N - 1 do for i = 0 to N - 1 do for j = 0 to N - 1 do W[i, j] = min(W[i, j], W[i, k] + W[k, j]) end for end for end for end algorithm // where W - is a weight matrix of N x N size, // min() - is a function which returns lesser of it's arguments
行业行驶で表現されたグラフを扱ったことがない場合は、上記のアルゴリズムが何を行うのか掌握するのが難しいかもしれません。そこで、掌握を深めるために、グラフを行业行驶で表現する方式 と、そのような表現が极短経路問題を解決するのになぜ役立つのかを見てみましょう。
下の図は、5 つの頂点を持つグラフを示しています。左側のグラフは、円と矢印で構成された視覚的な形式で表示されています。各円は頂点を表し、矢印は方向のあるエッジを表します。円内の数字は頂点番号に対応し、エッジの上の数字はエッジの重みに対応します。右側では、同じグラフが重みマトリックスの形式で表示されています。重みマトリックスはの一種で、各マトリックス セルには「重み」、つまり頂点i
(行) と頂点j
(列) の間の距離が含まれます。重みマトリックスには、頂点間の「パス」に関する情報 ( i
からj
に移動するときに経由する頂点のリスト) は含まれず、これらの頂点間の重み (距離) のみが含まれます。
重みマトリックスでは、セルの値が頂点間の重みに等しいことがわかります。そのため、頂点0
(行0
) からのパスを調べると、 0
から1
へのパスが 1 つしかないことがわかります。ただし、視覚的に表現すると、頂点0
から頂点2
と3
(頂点1
経由) へのパスが明確にわかります。この理由は単純です。初期状態では、重みマトリックスには隣接する頂点間の距離のみが含まれます。ただし、この情報だけで残りを見つけるのに十分です。
どのように動作するか見てみましょう。セルW[0, 1]
に注目してください。その値は、頂点0
から頂点1
への重みが1
のパスがあることを示しています (簡単に言うと、 0 ⭢ 1: 1
と記述できます)。この知識があれば、行1
のすべてのセル (頂点1
からのすべてのパスのすべての重みを含む) をスキャンし、この情報を行0
にバックポートして、重みを1
( W[0, 1]
の値) 増やすことができます。
同じ手順を使用して、頂点0
から他の頂点までのパスを見つけることができます。検索中に、同じ頂点につながるパスが複数あることがあり、さらに重要なことに、これらのパスの重みが異なる可能性があります。このような状況の例を下の図に示します。頂点0
から頂点2
までの検索で、重みの小さい頂点3
へのパスがもう 1 つ見つかりました。
パスは 2 つあります。元のパス0 ⭢ 3: 4
と、先ほど発見した新しいパス0 ⭢ 2 ⭢ 3: 3
(重み行列にはパスが含まれていないため、元のパスにどの頂点が含まれているかはわかりません)。ここで、最短のパスを保持し、セルW[0, 3]
に3
書き込むことを決定しました。
algorithm FloydWarshall(W) do for k = 0 to N - 1 do for i = 0 to N - 1 do for j = 0 to N - 1 do W[i, j] = min(W[i, j], W[i, k] + W[k, j]) end for end for end for end algorithm
k
上のfor
最も外側のサイクルでは、グラフ内のすべての頂点が反復処理され、各反復で、変数k
パスを検索している頂点を表します。 i
上のfor
最も内側のサイクルでも、グラフ内のすべての頂点が反復処理され、各反復で、 i
パスを検索している頂点を表します。最後に、 j
上のfor
最も内側のサイクルでは、グラフ内のすべての頂点が反復処理され、各反復で、 j
パスを検索している頂点を表します。これらを組み合わせると、次のようになります。各反復k
で、すべての頂点i
から頂点k
を経由してすべての頂点j
に至るパスを検索します。サイクル内では、パスi ⭢ j
( W[i, j]
で表される) とパスi ⭢ k ⭢ j
( W[I, k]
とW[k, j]
の合計で表される) を比較し、最短のパスをW[i, j]
に書き戻します。
ソース コードと実験グラフは、GitHub ので入手できます。実験グラフは、 Data/Sparse-Graphs.zip
ディレクトリにあります。この記事のすべてのベンチマークは、 ファイルに実装されています。
NO_EDGE = (int.MaxValue / 2) - 1
で表されます。
1 に関して。マトリックスについて話すとき、セルを「行」と「列」で定義します。このため、マトリックスを「正方形」または「長方形」の形で想像するのが自然なようです (図 4a)。
i = row * row_size + col; // where row - cell row index, // col - cell column index, // row_size - number of cells in a row.
重み行列の線形配列は、フロイド・ワーシャル アルゴリズムを効果的に実行するための前提条件です。その理由は単純ではなく、詳細な説明には別の投稿、またはいくつかの投稿が必要です。ただし、現時点では、このような表現によってが大幅に向上し、実際にアルゴリズムのパフォーマンスに大きな影響を与えることに注意してください。
- 著者注
2 について。アルゴリズムの擬似コードをよく見ると、2 つの頂点間のパスの存在に関連するチェックは見つかりません。代わりに、擬似コードではmin()
関数が使用されています。理由は簡単です。元々、2 つの頂点間にパスがない場合、セルの値はinfinity
に設定され、JavaScript を除くすべての言語では、すべての値はinfinity
未満です。整数の場合、「パスなし」の値としてint.MaxValue
使用したくなるかもしれません。ただし、 i ⭢ k
パスとk ⭢ j
パスの両方の値がint.MaxValue
に等しい場合、整数オーバーフローが発生します。そのため、 int.MaxValue
の半分より 1 小さい値を使用します。
- 思慮深い読者
確かに可能ですが、残念ながらパフォーマンスが大幅に低下します。簡単に言うと、CPU は分岐評価結果の統計を保持します。たとえば、 if
文の一部がtrue
またはfalse
に評価された場合などです。この統計を使用して、実際のif
文が評価される前に「統計的に予測された分岐」のコードを事前に実行します (これを呼びます)。これにより、コードの実行がより効率的になります。ただし、CPU の予測が不正確な場合、CPU は停止して正しい分岐を計算する必要があるため、正しい予測と無条件実行に比べてパフォーマンスが大幅に低下します。
各反復k
で重みマトリックスの重要な部分を更新するため、コード パターンがなく、すべての分岐がデータのみに基づいているため、CPU 分岐統計は役に立たなくなります。そのため、このようなチェックでは、が大量に発生します。
うーん、理論的な部分は終わったようです。アルゴリズムを実装しましょう (この実装をBaseline
と呼びます)。
public void Baseline(int[] matrix, int sz) { for (var k = 0; k < sz; ++k) { for (var i = 0; i < sz; ++i) { for (var j = 0; j < sz; ++j) { var distance = matrix[i * sz + k] + matrix[k * sz + j]; if (matrix[i * sz + j] > distance) { matrix[i * sz + j] = distance; } } } } }
上記のコードは、1 つの例外を除いて、前述の疑似コードとほぼ同じコピーです。Math.Min Math.Min()
の代わりに、必要な場合にのみセルを更新するためにif
を使用します。
- 思慮深い読者
理由は簡単です。執筆時点では、JIT はif
とMath.Min
両方の実装に対してほぼ同等のコードを出力します。詳細はで確認できますが、メイン ループ本体のスニペットを以下に示します。
// // == Assembly code of implementation of innermost loop for of j using if. // 53: movsxd r14, r14d // // compare matrix[i * sz + j] and distance (Condition) // 56: cmp [rdx+r14*4+0x10], ebx 5b: jle short 64 // // if matrix[i * sz + j] greater than distance write distance to matrix // 5d: movsxd rbp, ebp 60: mov [rdx+rbp*4+0x10], ebx 64: // end of loop // // == Assembly code of implementation of innermost loop for of j using Math.Min. // 4f: movsxd rbp, ebp 52: mov r14d, [rdx+rbp*4+0x10] // // compare matrix[i * sz + j] and distance (Condition) // 57: cmp r14d, ebx. // // if matrix[i * sz + j] less than distance write value to matrix // 5a: jle short 5e // // otherwise write distance to matrix // 5c: jmp short 61 5e: mov ebx, r14d 61: mov [rdx+rbp*4+0x10], ebx 65: // end of loop
if
とMath.Min
どちらを使用しても、条件チェックが行われていることがわかります。ただし、 if
の場合は不要な書き込みはありません。
var max = v * (v - 1)) / 2; // where v - is a number of vertexes in a graph.
ロックしましょう!
的方法 | サイズ | 人均 | エラー | 標準测量误差 |
---|---|---|---|---|
ベースライン | 300 | 27.525 ミリ秒 | 0.1937 ミリ秒 | 0.1617ミリ秒 |
ベースライン | 600 | 217.897 ミリ秒 | 1.6415 ミリ秒 | 1.5355 ミリ秒 |
ベースライン | 1200 | 1,763.335 ミリ秒 | 7.4561 ミリ秒 | 6.2262 ミリ秒 |
ベースライン | 2400 | 14,533.335 ミリ秒 | 63.3518 ミリ秒 | 52.9016 ミリ秒 |
ベースライン | 4800 | 119,768.219 ミリ秒 | 181.5514 ミリ秒 | 160.9406 ミリ秒 |
結果からわかるように、計算時間はグラフのサイズに比べて大幅に増加します。300 頂点のグラフでは 27 ミリ秒、2400 頂点のグラフでは 14.5 秒、4800 頂点のグラフでは 119 秒、つまりほぼ 2 分かかります。
ただし、私たちの実験では、有向非巡回グラフを使用します。このグラフには、頂点1
から頂点2
へのパスがある場合、頂点2
から頂点1
へのパスは存在しないという素晴らしい特性があります。私たちにとって、これは、 i
からk
へのパスがない場合にスキップできる隣接していない頂点が多数あることを意味します (この実装をSpartialOptimisation
と呼びます)。
public void SpartialOptimisation(int[] matrix, int sz) { for (var k = 0; k < sz; ++k) { for (var i = 0; i < sz; ++i) { if (matrix[i * sz + k] == NO_EDGE) { continue; } for (var j = 0; j < sz; ++j) { var distance = matrix[i * sz + k] + matrix[k * sz + j]; if (matrix[i * sz + j] > distance) { matrix[i * sz + j] = distance; } } } } }
以下は、同じグラフ セットに対する以前の実装 ( Baseline
) と現在の実装 ( SpartialOptimisation
) の結果です (最速の結果は太字で強調表示されています)。
步骤 | サイズ | 平衡 | エラー | 標準差别 | 百分率 |
---|---|---|---|---|---|
ベースライン | 300 | 27.525 ミリ秒 | 0.1937ミリ秒 | 0.1617ミリ秒 | 1.00 |
部分最適化 | 300 | 12.399ミリ秒 | 0.0943 ミリ秒 | 0.0882 ミリ秒 | 0.45 |
ベースライン | 600 | 217.897 ミリ秒 | 1.6415ミリ秒 | 1.5355 ミリ秒 | 1.00 |
部分最適化 | 600 | 99.122 ミリ秒 | 0.8230 ミリ秒 | 0.7698 ミリ秒 | 0.45 |
ベースライン | 1200 | 1,763.335 ミリ秒 | 7.4561 ミリ秒 | 6.2262 ミリ秒 | 1.00 |
部分最適化 | 1200 | 766.675 ミリ秒 | 6.1147 ミリ秒 | 5.7197 ミリ秒 | 0.43 |
ベースライン | 2400 | 14,533.335 ミリ秒 | 63.3518 ミリ秒 | 52.9016 ミリ秒 | 1.00 |
部分最適化 | 2400 | 6,507.878 ミリ秒 | 28.2317 ミリ秒 | 26.4079 ミリ秒 | 0.45 |
ベースライン | 4800 | 119,768.219 ミリ秒 | 181.5514 ミリ秒 | 160.9406 ミリ秒 | 1.00 |
部分最適化 | 4800 | 55,590.374 ミリ秒 | 414.6051 ミリ秒 | 387.8218 ミリ秒 | 0.46 |
私の PC には、8 つの論理プロセッサ ( ) またはテクノロジを備えた 4 つのコアを備えたInter Core i7-7700 CPU 3.60GHz (Kaby Lake)
プロセッサが搭載されています。コアが複数あるということは、作業に使える「手」が増えるようなものです。では、作業のどの部分を複数のワーカー間で効率的に分割できるかを確認し、それを並列化してみましょう。
for of k
ループから始めましょう。各反復で、ループは頂点k
を通るすべての頂点からすべての頂点へのパスを計算します。新しいパスと更新されたパスは重み行列に格納されます。反復を見ると、結果に影響を与えずに 0、1、2、3 または 3、1、2、0 の任意の順序で実行できることに気付くかもしれません。ただし、それらは順番に実行する必要があります。そうしないと、一部の反復では、新しいパスまたは更新されたパスが重み行列にまだ書き込まれていないため、それらを使用できなくなります。このような不一致は、間違いなく結果を台無しにします。そのため、引き続き調べる必要があります。
次の候補はfor of i
ループです。各反復で、ループは頂点i
から頂点k
へのパス (セル: W[i, k]
)、頂点k
から頂点j
へのパス (セル: W[k, j
]) を読み取り、次にi
からj
への既知のパス (セル: W[i, j]
) がi ⭢ k ⭢ j
パス (合計: W[i, k]
+ W[k, j]
) より短いかどうかを確認します。アクセス パターンを詳しく見ると、各反復でi
ループがk
行からデータを読み取り、 i
行を更新していることに気付くかもしれません。これは基本的に、反復が独立しており、任意の順序で実行できるだけでなく、並列に実行できることを意味しています。
これは有望そうなので、実装してみましょう (この実装をSpartialParallelOptimisations
と呼びます)。
for of j
も並列化できます。ただし、この場合、最も内側のサイクルの並列化は非常に非効率的です。にいくつかの簡単な変更を加えることで、自分で検証できます。
- 著者注
public void SpartialParallelOptimisations(int[] matrix, int sz) { for (var k = 0; k < sz; ++k) { Parallel.For(0, sz, i => { if (matrix[i * sz + k] == NO_EDGE) { return; } for (var j = 0; j < sz; ++j) { var distance = matrix[i * sz + k] + matrix[k * sz + j]; if (matrix[i * sz + j] > distance) { matrix[i * sz + j] = distance; } } }); } }
以下は、同じグラフ セットに対するBaseline
、 SpartialOptimisation
、およびSpartialParallelOptimisations
実装の結果です (並列化はクラスを使用して行われます)。
最简单的方法 | サイズ | 年均 | エラー | 標準误差 | 占比 |
---|---|---|---|---|---|
ベースライン | 300 | 27.525 ミリ秒 | 0.1937ミリ秒 | 0.1617ミリ秒 | 1.00 |
一些最適化 | 300 | 12.399ミリ秒 | 0.0943 ミリ秒 | 0.0882 ミリ秒 | 0.45 |
部分並列最適化 | 300 | 6.252 ミリ秒 | 0.0211ミリ秒 | 0.0187ミリ秒 | 0.23 |
ベースライン | 600 | 217.897 ミリ秒 | 1.6415 ミリ秒 | 1.5355 ミリ秒 | 1.00 |
地方最適化 | 600 | 99.122 ミリ秒 | 0.8230 ミリ秒 | 0.7698 ミリ秒 | 0.45 |
部分並列最適化 | 600 | 35.825 ミリ秒 | 0.1003 ミリ秒 | 0.0837 ミリ秒 | 0.16 |
ベースライン | 1200 | 1,763.335 ミリ秒 | 7.4561 ミリ秒 | 6.2262 ミリ秒 | 1.00 |
环节最適化 | 1200 | 766.675 ミリ秒 | 6.1147 ミリ秒 | 5.7197 ミリ秒 | 0.43 |
部分並列最適化 | 1200 | 248.801 ミリ秒 | 0.6040 ミリ秒 | 0.5043 ミリ秒 | 0.14 |
ベースライン | 2400 | 14,533.335 ミリ秒 | 63.3518 ミリ秒 | 52.9016 ミリ秒 | 1.00 |
位置最適化 | 2400 | 6,507.878 ミリ秒 | 28.2317 ミリ秒 | 26.4079 ミリ秒 | 0.45 |
部分並列最適化 | 2400 | 2,076.403 ミリ秒 | 20.8320 ミリ秒 | 19.4863 ミリ秒 | 0.14 |
ベースライン | 4800 | 119,768.219 ミリ秒 | 181.5514 ミリ秒 | 160.9406 ミリ秒 | 1.00 |
个部分最適化 | 4800 | 55,590.374 ミリ秒 | 414.6051 ミリ秒 | 387.8218 ミリ秒 | 0.46 |
部分並列最適化 | 4800 | 15,614.506 ミリ秒 | 115.6996 ミリ秒 | 102.5647 ミリ秒 | 0.13 |
結果から、 for of i
ループの並列化により、以前の実装 ( SpartialOptimisation
) と比較して計算時間が 2 ~ 4 倍短縮されたことがわかります。これは非常に印象的ですが、純粋な計算の並列化は利用可能なすべての計算リソースを消費し、システム内の他のアプリケーションのリソース不足につながる可能性があることを覚えておくことが重要です。
var a = new [] { 5, 7, 8, 1 }; var b = new [] { 4, 2, 2, 6 }; var c = new [] { 0, 0, 0, 0 }; for (var i = 0; i < 4; ++i) c[i] = a[i] + b[i];
非常に単純化した方法では、CPU レベルでの上記のfor
ループの反復0
の実行は次のように表すことができます。
ここで何が起こっているか見てみましょう。CPU はメモリから配列a
とb
の値を CPU レジスタにロードします (1 つのレジスタには 1 つの値しか保持できません)。次に、CPU はこれらのレジストリに対してスカラー加算演算を実行し、その結果をメイン メモリ (配列c
に直接) に書き込みます。このプロセスはループが終了する前に 4 回繰り返されます。
ここで何が起こっているか見てみましょう。CPU はメモリから配列a
とb
の値を CPU レジスタにロードします (ただし、今回は 1 つのベクトル レジスタが2 つの配列値を保持できます)。次に、CPU はこれらのレジストリに対してベクトル加算演算を実行し、結果をメイン メモリ (配列c
に直接) に書き込みます。一度に 2 つの値を操作するため、このプロセスは 4 回ではなく 2 回繰り返されます。
public void SpartialVectorOptimisations(int[] matrix, int sz) { for (var k = 0; k < sz; ++k) { for (var i = 0; i < sz; ++i) { if (matrix[i * sz + k] == NO_EDGE) { continue; } var ik_vec = new Vector<int>(matrix[i * sz + k]); var j = 0; for (; j < sz - Vector<int>.Count; j += Vector<int>.Count) { var ij_vec = new Vector<int>(matrix, i * sz + j); var ikj_vec = new Vector<int>(matrix, k * sz + j) + ik_vec; var lt_vec = Vector.LessThan(ij_vec, ikj_vec); if (lt_vec == new Vector<int>(-1)) { continue; } var r_vec = Vector.ConditionalSelect(lt_vec, ij_vec, ikj_vec); r_vec.CopyTo(matrix, i * sz + j); } for (; j < sz; ++j) { var distance = matrix[i * sz + k] + matrix[k * sz + j]; if (matrix[i * sz + j] > distance) { matrix[i * sz + j] = distance; } } } } }
ベクトル化されたコードは少し其妙に見えるかもしれないので、ステップごとに説明していきましょう。
i ⭢ k
パスのベクトルを作成します: var ik_vec = new Vector<int>(matrix[i * sz + k])
。結果として、ベクトルがint
型の値を 4 つ保持でき、 i ⭢ k
パスの重みが 5 に等しい場合、4 つの 5 のベクトル ([5, 5, 5, 5]) が作成されます。次に、各反復で、頂点i
から頂点j, j + 1, ..., j + Vector<int>.Count
へのパスを同時に計算します。
プロパティVector<int>.Count
ベクター レジスタに収まるint
型の要素の数を返します。ベクター レジスタのサイズは CPU モデルによって異なるため、このプロパティは CPU によって異なる値を返すことがあります。
- 著者注
ij_vec
とikj_vec
に読み込みます。ij_vec
ベクトルとikj_vec
ベクトルを比較し、最小値を新しいベクトルr_vec
に選択します。r_vec
重み行列に書き戻します。
1と3 は非常に簡単ですが、 2 は説明が必要です。前述のように、ベクトルでは複数の値を同時に操作します。したがって、一部の操作の結果は 1 つにはなりません。つまり、比較操作Vector.LessThan(ij_vec, ikj_vec)
複数の値を比較するため、 true
またはfalse
返すことができません。そのため、代わりに、ベクトルij_vec
とikj_vec
の対応する値の比較結果を含む新しいベクトルを返します ( ij_vec
の値がikj_vec
の値より小さい場合は-1
、それ以外の場合は0
)。返されたベクトル (それ自体) はあまり役に立ちませんが、 Vector.ConditionalSelect(lt_vec, ij_vec, ikj_vec)
ベクトル操作を使用して、 ij_vec
ベクトルとikj_vec
ベクトルから必要な値を抽出し、新しいベクトルr_vec
にすることができます。この操作は、入力ベクトルの対応する 2 つの値のうち小さい方に等しい値を持つ新しいベクトルを返します。つまり、ベクトルlt_vec
の値が-1
に等しい場合、操作はij_vec
から値を選択し、それ以外の場合はikj_vec
から値を選択します。
2 番の他に、説明が必要な部分がもう 1 つあります。2 番目の非ベクトル化ループです。これは、重み行列のサイズがVector<int>.Count
値の積ではない場合に必要です。その場合、メイン ループはすべての値を処理できず (ベクトルの一部をロードできないため)、2 番目の非ベクトル化ループが残りの反復を計算します。
方式 | サイズ | 均 | エラー | 標準较差 | 比列 |
---|---|---|---|---|---|
ベースライン | 300 | 27.525 ミリ秒 | 0.1937 ミリ秒 | 0.1617ミリ秒 | 1.00 |
部位最適化 | 300 | 12.399ミリ秒 | 0.0943 ミリ秒 | 0.0882 ミリ秒 | 0.45 |
局部並列最適化 | 300 | 6.252 ミリ秒 | 0.0211ミリ秒 | 0.0187ミリ秒 | 0.23 |
部分ベクトル最適化 | 300 | 3.056ミリ秒 | 0.0301ミリ秒 | 0.0281 ミリ秒 | 0.11 |
ベースライン | 600 | 217.897 ミリ秒 | 1.6415ミリ秒 | 1.5355 ミリ秒 | 1.00 |
这部分最適化 | 600 | 99.122 ミリ秒 | 0.8230 ミリ秒 | 0.7698 ミリ秒 | 0.45 |
地方並列最適化 | 600 | 35.825 ミリ秒 | 0.1003 ミリ秒 | 0.0837 ミリ秒 | 0.16 |
部分ベクトル最適化 | 600 | 24.378 ミリ秒 | 0.4320 ミリ秒 | 0.4041 ミリ秒 | 0.11 |
ベースライン | 1200 | 1,763.335 ミリ秒 | 7.4561 ミリ秒 | 6.2262 ミリ秒 | 1.00 |
一部分最適化 | 1200 | 766.675 ミリ秒 | 6.1147 ミリ秒 | 5.7197 ミリ秒 | 0.43 |
这部分並列最適化 | 1200 | 248.801 ミリ秒 | 0.6040 ミリ秒 | 0.5043 ミリ秒 | 0.14 |
部分ベクトル最適化 | 1200 | 185.628 ミリ秒 | 2.1240 ミリ秒 | 1.9868 ミリ秒 | 0.11 |
ベースライン | 2400 | 14,533.335 ミリ秒 | 63.3518 ミリ秒 | 52.9016 ミリ秒 | 1.00 |
部门最適化 | 2400 | 6,507.878 ミリ秒 | 28.2317 ミリ秒 | 26.4079 ミリ秒 | 0.45 |
部分並列最適化 | 2400 | 2,076.403 ミリ秒 | 20.8320 ミリ秒 | 19.4863 ミリ秒 | 0.14 |
部位ベクトル最適化 | 2400 | 2,568.676 ミリ秒 | 31.7359 ミリ秒 | 29.6858 ミリ秒 | 0.18 |
ベースライン | 4800 | 119,768.219 ミリ秒 | 181.5514 ミリ秒 | 160.9406 ミリ秒 | 1.00 |
这部分最適化 | 4800 | 55,590.374 ミリ秒 | 414.6051 ミリ秒 | 387.8218 ミリ秒 | 0.46 |
部分並列最適化 | 4800 | 15,614.506 ミリ秒 | 115.6996 ミリ秒 | 102.5647 ミリ秒 | 0.13 |
区域ベクトル最適化 | 4800 | 18,257.991 ミリ秒 | 84.5978 ミリ秒 | 79.1329 ミリ秒 | 0.15 |
結果から明らかなように、ベクトル化により計算時間が大幅に短縮されました。非並列化バージョン ( SpartialOptimisation
) と比較して 3 倍から 4 倍短縮されました。ここで興味深いのは、ベクトル化バージョンは、より小さなグラフ (2400 頂点未満) でも並列バージョン ( SpartialParallelOptimisations
) よりも優れていることです。
ベクトル化の実際の応用に興味がある場合は、 Array.Sort
ベクトル化したの一連の投稿を読むことをお勧めします (これらの結果は、後にのガベージ コレクターの更新に反映されました)。
最後の実装は並列化とベクトル化の取り組みを組み合わせたもので、正直に言うと個性に欠けています 🙂 なぜなら、 SpartialParallelOptimisations
のParallel.For
本体をSpartialVectorOptimisations
のベクトル化されたループに置き換えただけだからです。
public void SpartialParallelVectorOptimisations(int[] matrix, int sz) { for (var k = 0; k < sz; ++k) { Parallel.For(0, sz, i => { if (matrix[i * sz + k] == NO_EDGE) { return; } var ik_vec = new Vector<int>(matrix[i * sz + k]); var j = 0; for (; j < sz - Vector<int>.Count; j += Vector<int>.Count) { var ij_vec = new Vector<int>(matrix, i * sz + j); var ikj_vec = new Vector<int>(matrix, k * sz + j) + ik_vec; var lt_vec = Vector.LessThan(ij_vec, ikj_vec); if (lt_vec == new Vector<int>(-1)) { continue; } var r_vec = Vector.ConditionalSelect(lt_vec, ij_vec, ikj_vec); r_vec.CopyTo(matrix, i * sz + j); } for (; j < sz; ++j) { var distance = matrix[i * sz + k] + matrix[k * sz + j]; if (matrix[i * sz + j] > distance) { matrix[i * sz + j] = distance; } } }); } }
この发表のすべての結果は以下的に記載されています。予想どおり、並列処理とベクトル化を同時に采用すると最良の結果が得られ、首先の実装と比較して計算時間が最明显 25 倍 (1200 頂点のグラフの場合) 短縮されました。
形式 | サイズ | 一般 | エラー | 標準问题 | 比例 |
---|---|---|---|---|---|
ベースライン | 300 | 27.525 ミリ秒 | 0.1937ミリ秒 | 0.1617ミリ秒 | 1.00 |
地方最適化 | 300 | 12.399ミリ秒 | 0.0943 ミリ秒 | 0.0882 ミリ秒 | 0.45 |
区域並列最適化 | 300 | 6.252 ミリ秒 | 0.0211ミリ秒 | 0.0187ミリ秒 | 0.23 |
环节ベクトル最適化 | 300 | 3.056ミリ秒 | 0.0301ミリ秒 | 0.0281 ミリ秒 | 0.11 |
部分並列ベクトル最適化 | 300 | 3.008 ミリ秒 | 0.0075ミリ秒 | 0.0066ミリ秒 | 0.11 |
ベースライン | 600 | 217.897 ミリ秒 | 1.6415 ミリ秒 | 1.5355 ミリ秒 | 1.00 |
那部分最適化 | 600 | 99.122 ミリ秒 | 0.8230 ミリ秒 | 0.7698 ミリ秒 | 0.45 |
地方並列最適化 | 600 | 35.825 ミリ秒 | 0.1003 ミリ秒 | 0.0837 ミリ秒 | 0.16 |
环节ベクトル最適化 | 600 | 24.378 ミリ秒 | 0.4320 ミリ秒 | 0.4041 ミリ秒 | 0.11 |
部分並列ベクトル最適化 | 600 | 13.425ミリ秒 | 0.0319ミリ秒 | 0.0283 ミリ秒 | 0.06 |
ベースライン | 1200 | 1,763.335 ミリ秒 | 7.4561 ミリ秒 | 6.2262 ミリ秒 | 1.00 |
一些最適化 | 1200 | 766.675 ミリ秒 | 6.1147 ミリ秒 | 5.7197 ミリ秒 | 0.43 |
部件並列最適化 | 1200 | 248.801 ミリ秒 | 0.6040 ミリ秒 | 0.5043 ミリ秒 | 0.14 |
部件ベクトル最適化 | 1200 | 185.628 ミリ秒 | 2.1240 ミリ秒 | 1.9868 ミリ秒 | 0.11 |
部分並列ベクトル最適化 | 1200 | 76.770 ミリ秒 | 0.3021 ミリ秒 | 0.2522 ミリ秒 | 0.04 |
ベースライン | 2400 | 14,533.335 ミリ秒 | 63.3518 ミリ秒 | 52.9016 ミリ秒 | 1.00 |
地方最適化 | 2400 | 6,507.878 ミリ秒 | 28.2317 ミリ秒 | 26.4079 ミリ秒 | 0.45 |
个部分並列最適化 | 2400 | 2,076.403 ミリ秒 | 20.8320 ミリ秒 | 19.4863 ミリ秒 | 0.14 |
一些ベクトル最適化 | 2400 | 2,568.676 ミリ秒 | 31.7359 ミリ秒 | 29.6858 ミリ秒 | 0.18 |
部分並列ベクトル最適化 | 2400 | 1,281.877 ミリ秒 | 25.1127 ミリ秒 | 64.8239 ミリ秒 | 0.09 |
ベースライン | 4800 | 119,768.219 ミリ秒 | 181.5514 ミリ秒 | 160.9406 ミリ秒 | 1.00 |
区域最適化 | 4800 | 55,590.374 ミリ秒 | 414.6051 ミリ秒 | 387.8218 ミリ秒 | 0.46 |
部份並列最適化 | 4800 | 15,614.506 ミリ秒 | 115.6996 ミリ秒 | 102.5647 ミリ秒 | 0.13 |
有些ベクトル最適化 | 4800 | 18,257.991 ミリ秒 | 84.5978 ミリ秒 | 79.1329 ミリ秒 | 0.15 |
部分並列ベクトル最適化 | 4800 | 12,785.824 ミリ秒 | 98.6947 ミリ秒 | 87.4903 ミリ秒 | 0.11 |