12

私はしばらくの間、この質問を熟考してきました:

複数の CPU があるという事実を利用して、マルチコア マシン上でより高速な基本データ構造 (つまり、リンク リスト、ハッシュ テーブル、セット、スキップリスト、ブルーム フィルター、レッド ブラック ツリーなど) を構築できますか?

pthread で予備実験を行ったところ、pthread_create() は 30us 程度かかることがわかりましたが、単純な hash_map の挿入は、シングル コアの場合よりもはるかに短い時間で済みました。したがって、同期プリミティブとスレッドの作成が非常に遅いため、より高速な hash_map<> を作成することを想像するのが難しくなりました。ツリーのトラバーサルとバランシングを並行して行うことも想像できますが、やはり、同期プリミティブはランタイムを短くするのではなく、長くするように見えます。

「CPUが増えたので、もっと速くできるはずだ」というのは今でも直感的に感じますが、その声明の証明または反証に頭を悩ませることはできません. 私は C++ でかなりの実験をしてきましたが、他の言語がこのタスクに対してより良い解決策 (erlang?) を提供するのではないかと疑っています。考え?

編集の詳細: 頻繁に使用されるプログラミング/データ構造パラダイムがいくつかあり、高速化できる可能性があると思います。たとえば、基本的に次のようなコードを頻繁に書いていることに気付きます (実際のデータは "rand()" に置き換えられています)。

static const int N = 1000000; 
static const int M = 10000000; // 10x more lookups 
hash_map<int, int> m; 
// batch insert a bunch of interesting data 
for (int i = 0; i < N; i++) m[rand()] = rand(); 

// Do some random access lookups. 
for (int i = 0; i < M; i++) m[rand()]++;

この種のパラダイムは、名前と値の設定と構成データ、バッチ処理などによく使用されます。10 倍 (またはそれ以上) の検索/挿入比により、従来の hash_map<> はこの種の操作に理想的です。

これは、挿入フェーズと検索フェーズで簡単に半分に分割できます。並行世界では、2 つの半分の間に「フラッシュ キュー」操作が存在する場合があります。より難しいのは、インターリーブされた挿入 + ルックアップ バージョンです。

hash_map<int, int> m; 

for (int i = 0; i < N; i++) { 
   if (rand() % LOOKUP_RATIO == 0) 
     hash_map[rand()]++;  // "lookup" 
   else 
     hash_map[rand()] = rand();  // "insert" 
}

そのシナリオでは、各ルックアップの前に挿入キューがフラッシュされている限り、挿入は非同期である可能性があり、LOOKUP_RATIO が十分に大きい場合 (たとえば、>1000)、上記のバッチの例と非常に似ていますが、いくつかのキューイングがあります。ただし、キューイングは同期プリミティブを意味します。

次のスニペットを想像してみてください。

hash_map<int,int> a;
hash_map<int,int> b; 
for (int i = 0; i < N; i++) { 
  // the following 2 lines could be executed in parallel 
  a[rand()] = rand(); 
  b[rand()] = rand(); 
}

したがって、ルックアップは次の方法で「並列」に実行できます。

int lookup(int value) { 
  // The following 2 lines could be executed in parallel: 
  v1 = a[value]; 
  v2 = b[value]; 
  if (v1)  // pseudo code for "value existed in a" 
    return v1; 
  else 
    return v2; 
}
4

8 に答える 8

6

問題は、共有データ自体が並列コンピューティングの悩みの種であるということです。理想的には、各コアが個別のデータで動作するようにする必要があります。そうしないと、同期に関連するオーバーヘッドが発生します。(状態を共有せずに通信するにはどうすればよいですか? メッセージの受け渡しによって。)

また、データ構造の高速化について話すのは少し奇妙です。異なるデータ構造に対する異なる操作には異なる特性があるため、データ構造に対する操作の高速化について話す方が自然だと思います。高速化したい特定のタイプのアクセスはありますか?

EDIT、追加の詳細に応じて:目標は、並行してアクセスできる単一のハッシュマップを持つことであり、その基盤は複数のハッシュテーブルである可能性がありますが、このデータ構造のユーザーに透過的に提示されると想定しています単一のハッシュ テーブルとして。当然のことながら、ロックのスピンに多くの時間を費やすことを懸念します。また、このレベルでは、キャッシュの一貫性の問題に注意する必要があります。つまり、コアまたはプロセッサに同じデータを指す個別のキャッシュがあり、一方がデータを変更すると、もう一方のキャッシュされたデータは無効になります。これが繰り返し発生すると、莫大なコストがかかる可能性があり、並列処理は単一のコアで実行するよりも悪い可能性があります。そのため、共有データには非常に注意を払っています。

私の本能は、それぞれがハッシュ テーブルの異なるセクションを所有するスレッドのプールを持つことです。ハッシュは、最初にキーからハッシュ テーブル セクションにマップされ、次にそのセクション内のオフセットにマップされます。更新は、ハッシュ テーブルのそのセクションを所有するスレッドにメッセージとして渡されます。そうすれば、同じものを一度に変更しようとする人は誰もいません。当然のことながら、非同期メッセージ パッシングの同時実行機能を備えた言語 (Erlang) では、他の言語よりも簡単です。

于 2009-02-24T22:31:28.887 に答える
3

pthread_create()まず、時間をハッシュマップ操作と比較するのは適切ではないと思います。競合している場合と競合していない場合の両方で、ロック (解除) 時間と比較してください。

それでも、そうです、同期時間はボトルネックであり、悪化しています。他のほとんどのデータ構造体がキャッシュに(またはシャドーレジスタにさえ)留まろうとしている間、CPU間バス/ブリッジ/チャネルに移動する必要があるためです。 .

この問題を解決するには、主に 2 つの方向性があります。

  1. より良い共有構造: ロックフリー構造および/またはトランザクションメモリをチェックしてください。どちらも「lock-modify-release」サイクルを「try-check-commit/rollback」に置き換えることで、アクセシビリティを最大化しようとします。ほとんどの場合、チェックは成功するはずなので、ロールバックが平均的なパフォーマンスに影響することはありません。通常、チェック/コミットはアトミックに行われるため、CPU 帯域幅の点でコストがかかりますが、従来のロックよりもはるかに少なくなります。

  2. 共有が少ない: それが erlang/haskell 言語が強調していることです。小さなメッセージを簡単かつ安価に転送できるため、スレッド間通信はパラメーターを使用した関数呼び出しに似ており、共有メモリよりも少なくなります。同期する必要があるのは 2 つのプロセスのみであり、(理論的には) 非 RAM チャネルを低レイテンシで使用できるため、これははるかにスケーラブルです。

編集:ロックフリー構造について誰も意見を持っていないことに驚いています。300 CPUS まで (ほぼ) 直線的にスケーリングする Java でのロックフリーのハッシュテーブル実装について、これ(pdf) とこれ(ビデオ) を確認してください。

于 2009-02-24T23:00:18.460 に答える
3

私は毎日この質問に取り組んでいます。並列アルゴリズムの各スレッドで独自のリンク リストを作成し、完了したらマスター上でそれらを縫い合わせることができるため、リンク リストのようなものが非常に便利であることがわかりました。スレッドが真に独立している限り、オーバーヘッドはほとんどありません

使用するデータの配列がある場合は、ほとんどの場合、各スレッドで作業する小さな配列を割り当て、完了時に小さな配列をマスター配列にマージする方が良いことがわかります-実際、クラスター化されている場合環境では、「同じ」配列を使用することさえできません!

連想配列 (.NET Dictionary など) を使用するアルゴリズムを実装している場合、ほとんどの場合、スレッド間のどこかで作業を複製することになります。可能であれば、これらを避けるようにしてください。

CUDA (GPU) 環境向けにコーディングしている場合は、動作する前に全世界を配列として再キャストできる (いや、そうすべきです!) ことがすぐにわかります:)

于 2009-02-24T23:14:15.553 に答える
1

1回のルックアップで多くの並列処理が必要になるとは思えません。しかし、検索するアイテムのリスト全体がある場合は、別のケースです。

ハッシュ テーブルを取得し、キーの大きなリストを取得して、ハッシュ テーブルまたはツリーで検索します。キーのリストを 2 つの CPU に分割すると、パフォーマンスが 2 倍になります。

または、挿入するアイテムの大きなリストを取得します。ハッシュ テーブルを CPU ごとの領域に分割し、キー リストを分割します。次に、各 CPU は項目を独自のハッシュ テーブルに詰め込むことができます。

これは、ベクトル、B ツリー、およびバイナリ ツリーにも当てはまりますが、ハッシュ テーブルは、更新のためのロックがわずかに少なくて済むように構築できると思います。

于 2009-02-25T02:02:52.217 に答える
1

この CACM 記事 -マルチコア時代のデータ構造(残念ながらプレミアム コンテンツです)をご覧ください: http://cacm.acm.org/magazines/2011/3/105308-data-structures-in-the-multicore-年齢/全文

論文の初期バージョンはこちら: http://www.cs.tau.ac.il/~shanir/concurrent-data-structures.pdf

于 2011-03-23T16:27:32.917 に答える
1

データ構造を調べて、「これで非同期にできることは何ですか?」と尋ねる必要があると思います。

そして、多くのデータ構造については、私が目にするものはほとんどありません。

しかし、より難解な、またはあまり使用されていない構造については、きっとあるでしょう。ある種のツリーのリバランスは並列化できると思います。グラフをトラバースすることは可能だと思います(ただし、データ構造よりもアルゴリズムの方が多いかもしれません)。二重にリンクされたリストを(両端から)トラバースすることは可能だと思います。

于 2009-02-24T22:31:17.827 に答える
0

ハビエルは良い点を持っています: 操作を並行して実行している場合、スレッドは既に取得されています。

これが行き着くものの多くは、標準的なリーダーとライターの問題だと思います。スレッドが読み取りやその他の非破壊的な操作だけを行っている場合、ハッシュ テーブルを使用するスレッドの数は事実上無制限にできるはずです。ただし、そのうちの1人が書き込みを行う必要がある場合、ハッシュテーブル全体で排他ロックを取得する必要があります(最初にキーを外部でハッシュしない限り、理論的にはハッシュ先のバケットでロックを取得できますが、衝突解決メカニズムによって異なります)。

考慮すべきことの 1 つは、データ構造ごとに 1 つ (または小さなプール) のスレッドを持ち、アクセスを「サービス」として扱うことです。つまり、スレッドがハッシュ マップで何かを検索する代わりに、そのデータ構造にサービスを提供するスレッドに同期要求を発行します。これにより、ロック操作がローカライズされますが (リクエストを処理するスレッドのみがロック手法を認識している必要があります)、リクエスト キューがボトルネックになる可能性があります。

他の誰かが言ったように、並列処理を利用する最良の方法は、データ構造ではなく、アルゴリズムを使用することだと思います。

于 2009-02-25T02:04:41.323 に答える
0

すべてを作業キューに入れます。それが鍵であり、複数のマシンにまたがるスケーリングに近づきます。同期にはコストがかかり、後でコストが高くなるだけです (128 個の CPU でメモリ バリアがあると想像してください)。

于 2009-02-25T02:08:13.263 に答える