12

D プログラミング言語の並列化ライブラリに取り組んでいます。基本的なプリミティブ (並列 foreach、map、reduce、タスク/フューチャー) にかなり満足しているので、より高レベルの並列アルゴリズムについて考え始めています。並列化のより明白な候補の中にソートがあります。

私の最初の質問は、並べ替えアルゴリズムの並列化されたバージョンは現実の世界で役立つのか、それともほとんど学術的なものなのかということです。それらが役立つ場合、どこで役立ちますか? 私が個人的に仕事でそれらを使用することはめったにありません。単純に、単一の sort() 呼び出しよりもはるかに粒度の粗いレベルの並列処理を使用して、すべてのコアを 100% に固定するためです。

第二に、大規模な配列の場合、クイックソートはほとんど恥ずかしいほど並列であるように見えますが、得られるはずであると信じているほぼ線形のスピードアップを得ることができません。クイック ソートの場合、本質的にシリアルな部分は最初のパーティションだけです。各パーティションの後で、2 つのサブアレイを並列にソートすることで、クイック ソートを並列化してみました。簡略化された疑似コードでは:

// I tweaked this number a bunch.  Anything smaller than this and the 
// overhead is smaller than the parallelization gains.
const  smallestToParallelize = 500; 

void quickSort(T)(T[] array) {
    if(array.length < someConstant) {
        insertionSort(array);
        return;
    }

    size_t pivotPosition = partition(array);

    if(array.length >= smallestToParallelize) {
        // Sort left subarray in a task pool thread.
        auto myTask = taskPool.execute(quickSort(array[0..pivotPosition]));
        quickSort(array[pivotPosition + 1..$]);
        myTask.workWait();
    } else {
        // Regular serial quick sort.
        quickSort(array[0..pivotPosition]);
        quickSort(array[pivotPosition + 1..$]);
    }
}

最初のパーティションにかかる時間が無視できる非常に大きな配列の場合でも、アルゴリズムの純粋なシリアル バージョンと比較して、デュアル コアでは約 30% の高速化しか得られません。ボトルネックは共有メモリアクセスだと思います。このボトルネックを解消する方法、またはボトルネックが他に何であるかについての洞察はありますか?

編集:私のタスクプールには、システム内のコア数から1を引いた数に等しい固定数のスレッドがあります(メインスレッドも機能するため)。また、私が使用している待機のタイプは作業待機です。つまり、タスクが開始されたが終了していない場合、スレッド呼び出しworkWait()はプールから他のジョブを盗み、待機中のジョブが完了するまでそれらを実行します。タスクが開始されていない場合は、現在のスレッドで完了します。これは、待機が非効率的ではないことを意味します。実行する作業がある限り、すべてのスレッドはビジー状態になります。

4

5 に答える 5

7

私は並列ソートの専門家ではないことを覚えておいてください。人々は並列ソートを研究のキャリアにしていますが...

1) それらは現実の世界で役に立ちますか。

もちろん、高価なもの(文字列など)をソートする必要があり、すべてのコアをペグしていない場合はそうです。

  • コンテキストに基づいて文字列の大きな動的リストをソートする必要がある UI コードを考えてください
  • 粒子をソートする必要がある barnes-hut n-bodies sim のようなものを考えてください

2) クイックソートは直線的なスピードアップをもたらすように見えますが、そうではありません。パーティションのステップはシーケンシャルなボトルネックです。プロファイリングを行うとこれがわかり、クアッド コアでは 2 ~ 3 倍になる傾向があります。

小規模なシステムで高速化を実現したい場合は、タスクごとのオーバーヘッドが非常に小さいことを確認する必要があります。理想的には、実行中のスレッドが多すぎないようにする必要があります。つまり、デュアルでは 2 つを超えないようにする必要があります。芯。スレッド プールはおそらく適切な抽象化ではありません。

大規模なシステムで高速化を実現したい場合は、スキャン ベースの並列ソートを調べる必要があります。これに関する論文があります。マージソートと同様に、ビットニックソートも非常に簡単に並列化できます。並列基数ソートも便利です。PPL に 1 つあります (Visual Studio 11 が嫌いでない場合)。

于 2010-02-13T23:24:14.213 に答える
3

私は専門家ではありませんが...これが私が見るものです:

まず、経験則として、問題の小さな部分を最初から見るアルゴリズムは、並列アルゴリズムとしてうまく機能する傾向があると聞いています。

実装を見て、並列/シリアルスイッチを逆に実行してみてください。配列を分割し、N個のセグメントができるまで並列に並べ替えてから、シリアルに切り替えます。並列ケースごとに多かれ少なかれ新しいスレッドを取得している場合、Nはコア数になります。OTOHスレッドプールが固定サイズであり、短期間のデリゲートのキューとして機能する場合は、コア数のN〜2倍を使用します(1つのパーティションがより速く終了したためにコアがアイドル状態にならないようにするため)。

その他の調整:

  • ローカルレベルでをスキップし、myTask.wait();すべてのタスクを待機するラッパー関数を使用します。
  • 深度チェックを回避する関数の個別のシリアル実装を作成します。
于 2010-02-14T00:50:09.440 に答える
1

「私の最初の質問は、現実の世界で役立つ並べ替えアルゴリズムの並列化されたバージョンです」-実際の作業で作業しているデータセットのサイズによって異なります。小規模なデータ セットの場合、答えはノーです。大規模なデータ セットの場合、データ セットのサイズだけでなく、システムの特定のアーキテクチャにも依存します。

期待されるパフォーマンスの向上を妨げる要因の 1 つは、システムのキャッシュ レイアウトです。データがコアの L1 キャッシュに収まる場合、並べ替えアルゴリズムの各反復間で L1 キャッシュ ミスのペナルティが発生するため、複数のコア間で並べ替えを行っても得られるものはほとんどありません。

同じ理由が、複数の L2 キャッシュと NUMA (non-uniform memory access) アーキテクチャを持つチップにも当てはまります。したがって、並べ替えを分散するコアが多いほど、それに応じて、smallestToParallelize 定数を増やす必要があります。

あなたが特定したもう 1 つの制限要因は、共有メモリ アクセス、またはメモリ バス上の競合です。メモリ バスは、1 秒あたり特定の数のメモリ アクセスしか満たすことができないため、基本的にメイン メモリの読み取りと書き込み以外は何もしないコアを追加すると、メモリ システムに大きな負荷がかかります。

指摘しておくべき最後の要素は、スレッド プール自体です。これは、皆さんが考えているほど効率的ではない可能性があるためです。共有キューから作業を盗んで生成するスレッドがあるため、そのキューには同期メソッドが必要です。また、それらの実装方法によっては、コード内に非常に長いシリアル セクションが発生する可能性があります。

于 2010-02-18T01:23:05.587 に答える
1

ここでの回答がもはや適用可能かどうか、または私の提案が D に適用可能かどうかはわかりません。

ともかく ...

D で許可されていると仮定すると、プリフェッチ ヒントをキャッシュに提供する可能性は常にあります。問題のコアは、すぐに (すぐではなく) データを特定のキャッシュ レベルにロードする必要があることを要求します。理想的なケースでは、コアが処理を開始するまでにデータがフェッチされています。データが「コールド」でフェッチされた場合よりも、少なくとも待機状態が少なくなる結果となるプリフェッチ プロセスは多かれ少なかれ途中である可能性が高くなります。

キャッシュから RAM への全体的なスループット容量によって依然として制約を受けるため、大量のデータがコアの専用キャッシュに格納されるようにデータを整理しておく必要があります。更新されたデータを書き込みます。

コードとデータは、キャッシュ内の最小サイズの単位であるキャッシュ ライン (それぞれ 64 バイトのフェッチ ユニット) の概念に従って編成する必要があります。これにより、2 つのコアの場合、1 つのコアのみが機能し、作業が整理されていなかった以前と比べて、メモリ システムがコアごとに半分の量 (100% のスケーラビリティを想定) で機能するように、作業を整理する必要があります。4 コアの場合は 4 分の 1 などです。それはかなりの挑戦ですが、決して不可能ではありません。作品を再構築する際の想像力にかかっています。いつものように、考えられない解決策があります...誰かがそれを実行するまでは!

WYSIWYG D が、私が使用している C とどのように比較されるかはわかりませんが、一般的には、スケーラブルなアプリケーションを開発するプロセスは、開発者が実際のマシン コード生成でコンパイラにどれだけ影響を与えることができるかによって改善されると思います。インタープリター言語の場合、インタープリターによって非常に多くのメモリ作業が行われるため、一般的な「バックグラウンド ノイズ」から改善を識別できないリスクがあります。

私はかつてマルチスレッドのシェルソートを書いたことがあり、2 コアでは 1 コアよりも 70% 速く、3 コアでは 1 コアよりも 100% 高速に実行されました。4 コアは 3 コアよりも遅く実行されました。だから私はあなたが直面しているジレンマを知っています。

于 2012-03-07T14:47:37.370 に答える
0

同様の問題に直面している外部ソート[1]を紹介したいと思います。通常、このクラスのアルゴリズムは主に大量のデータに対処するために使用されますが、その主なポイントは、大きなチャンクをより小さく関連性のない問題に分割することです。したがって、並列に実行すると非常に優れています。後で部分的な結果をつなぎ合わせるだけで済みますが、これはそれほど並列ではありません (ただし、実際の並べ替えに比べて比較的安価です)。

External Merge Sort は、不明な量のスレッドでも非常にうまく機能します。作業負荷を任意に分割し、すべての作業単位が完了するまでアイドル状態のスレッドが 1 つあるたびに、n 要素の各チャンクをスレッドに渡します。その時点で結合を開始できます。

[1] http://en.wikipedia.org/wiki/External_sorting

于 2012-03-07T15:00:17.927 に答える