2

データの処理に並列化を利用するアプリケーションがあります。

メインプログラムはC#にあり、データを分析するためのルーチンの1つは外部C++dllにあります。このライブラリはデータをスキャンし、データ内に特定の信号が見つかるたびにコールバックを呼び出します。データを収集、並べ替えてから、HDに保存する必要があります。

これは、コールバックによって呼び出されるメソッドと、データを並べ替えて保存するためのメソッドの最初の簡単な実装です。

// collection where saving found signals
List<MySignal> mySignalList = new List<MySignal>();

// method invoked by the callback
private void Collect(int type, long time)
{
    lock(locker) { mySignalList.Add(new MySignal(type, time)); }
}

// store signals to disk
private void Store()
{
    // sort the signals
    mySignalList.Sort();
    // file is a object that manages the writing of data to a FileStream
    file.Write(mySignalList.ToArray());
}

データは、サイズ10000 xnの2次元配列(short [] []データ)で構成され、n変数があります。私はこのように並列化を使用します:

Parallel.For(0, 10000, (int i) =>
{
    // wrapper for the external c++ dll
    ProcessData(data[i]);
}

ここで、10000の配列のそれぞれについて、0から4のコールバックが発生する可能性があると推定します。私はボトルネックに直面しており、CPUリソースが過剰に使用されていないことを考えると、ロック(数千のコールバックと一緒に)が問題であると思います(私は正しいですか、それとも何か他のものがある可能性がありますか?)。ConcurrentBagコレクションを試しましたが、パフォーマンスはさらに悪くなっています(他のユーザーの調査結果と一致しています)。

ロックフリーコードを使用するための可能な解決策は、複数のコレクションを持つことだと思いました。次に、並列プロセスの各スレッドを単一のコレクションで機能させるための戦略が必要になります。コレクションは、たとえばスレッドIDをキーとするディクショナリ内にある可能性がありますが、このための.NET機能はわかりません(並列化を開始する前にディクショナリを初期化するためのスレッドIDを知っている必要があります)。このアイデアは実現可能でしょうか。そうであれば、このための.NETツールは存在しますか?または、プロセスをスピードアップする他のアイデアはありますか?

[編集]ReedCopseyの提案に従い、次のソリューションを使用しました(VS2010のプロファイラーによると、リストのロックと追加の負担がリソースの15%を占める前は、現在は1%にすぎません)。

// master collection where saving found signals
List<MySignal> mySignalList = new List<MySignal>();
// thread-local storage of data (each thread is working on its List<MySignal>)
ThreadLocal<List<MySignal>> threadLocal;

// analyze data
private void AnalizeData()
{
    using(threadLocal = new ThreadLocal<List<MySignal>>(() => 
        { return new List<MySignal>(); }))
    {
        Parallel.For<int>(0, 10000,
        () =>
        { return 0;},
        (i, loopState, localState) =>
        {
            // wrapper for the external c++ dll
            ProcessData(data[i]);
            return 0;
        },
        (localState) =>
        {
            lock(this)
            {
                // add thread-local lists to the master collection
                mySignalList.AddRange(local.Value);
                local.Value.Clear();
            }
        });
    }
}

// method invoked by the callback
private void Collect(int type, long time)
{
    local.Value.Add(new MySignal(type, time));
}
4

4 に答える 4

1

ロックフリーコードを使用するための可能な解決策は、複数のコレクションを持つことだと考えました。次に、並列プロセスの各スレッドを単一のコレクションで機能させるための戦略が必要になります。コレクションは、たとえばスレッドIDをキーとするディクショナリ内にある可能性がありますが、このための.NET機能はわかりません(並列化を開始する前にディクショナリを初期化するためのスレッドIDを知っている必要があります)。このアイデアは実現可能でしょうか。そうであれば、このための.NETツールは存在しますか?または、プロセスをスピードアップする他のアイデアはありますか?

ThreadLocal<T>コレクションを保持するために使用することを検討することをお勧めします。これにより、スレッドごとに個別のコレクションが自動的に割り当てられます。

そうは言っても、Parallel.Forローカル状態で機能するオーバーロードがあり、最後にコレクションパスがあります。これにより、潜在的に、ProcessData各ループ本体が独自のコレクションで機能していたラッパーを生成し、最後に再結合することができます。これにより、(各スレッドが独自のデータセットで作業しているため)再結合フェーズまでロックする必要がなくなる可能性があります。再結合フェーズは、スレッドごとに1回(タスクごとに1回、つまり10000回ではなく)発生します。これにより、取得するロックの数を約25000(0-4 * 10000)から数個に減らすことができます(システムとアルゴリズムに依存しますが、クアッドコアシステムでは、おそらく私の経験では約10です)。

詳細については、Parallel.For/ForEachを使用したデータの集約に関するブログ投稿を参照してください。オーバーロードを示し、それらがどのように機能するかをより詳細に説明します。

于 2011-02-21T17:42:15.637 に答える
1

遭遇している「ボトルネック」の量はわかりません。しかし、ロックを見てみましょう。

私のマシン(クアッドコア、2.4 GHz)では、競合しない場合、ロックのコストは約70ナノ秒です。アイテムをリストに追加するのにどれくらいの時間がかかるかはわかりませんが、数マイクロ秒以上かかるとは想像できません。ただし、ロックの競合を考慮して、リストにアイテムを追加するのに100マイクロ秒かかるとしましょう(10マイクロ秒でさえあることに非常に驚いています)。したがって、リストに40,000個のアイテムを追加する場合、それは4,000,000マイクロ秒、つまり4秒になります。そして、もしそうなら、1つのコアが固定されることを期待します。

使用したことはありませんが、 BlockingCollectionConcurrentBagのパフォーマンスは非常に優れていることがわかりました。

しかし、あなたのボトルネックはどこかにあるのではないかと思います。プロファイリングを行いましたか?

于 2011-02-21T17:58:24.590 に答える
1

C#の基本的なコレクションはスレッドセーフではありません。

あなたが抱えている問題は、add()メソッドを呼び出すためだけにコレクション全体をロックしているという事実によるものです。

コレクション全体ではなく、コレクション内の単一の要素のみをロックするスレッドセーフなコレクションを作成できます。

たとえば、リンクリストを見てみましょう。

add(item (or list))次のことを行うメソッドを実装します。

  1. コレクションをロックします。
  2. A=最後のアイテムを取得します。
  3. 最後のアイテム参照を新しいアイテム(または新しいリストの最後のアイテム)に設定します。
  4. 最後のアイテムをロックします(A)。
  5. アンクロックコレクション。
  6. Aの最後に新しいアイテム/リストを追加します。
  7. ロックされたアイテムのロックを解除します。

これにより、追加時に3つの簡単なタスクでコレクション全体がロックされます。

次に、リストを反復処理するときに、trylock()各オブジェクトに対してを実行します。ロックされている場合は、ロックが解除されるのを待ちます(そうすれば、確実にadd()終了します)。
C#では、オブジェクトに対して空のlock()ブロックを。として実行できますtrylock()。これで、安全に追加しながら、同時にリストを反復処理できるようになりました。

必要に応じて、他のコマンドにも同様のソリューションを実装できます。

于 2011-02-21T18:00:53.773 に答える
0

コレクションの組み込みソリューションには、ロックが含まれます。おそらく読み取り/書き込み中の実際のデータ構造を分離することによって、それを回避する方法があるかもしれませんが、どこかでロックする必要があります。

また、Parallel.For()はスレッドプールを使用することを理解してください。実装は簡単ですが、スレッドの作成/破棄をきめ細かく制御できなくなり、大きな並列タスクを開始するときにスレッドプールに深刻なオーバーヘッドが発生します。

概念的な観点から、私はこのアルゴリズムを高速化するために2つのことを並行して試みます。

  • Threadクラスを使用して、自分でスレッドを作成します。これにより、スレッドプールのスケジューリングの速度低下から解放されます。スレッドプールがスレッドの要求を独自のペースで内部動作にフィードする代わりに、スレッドが開始するように指示すると、スレッドは処理を開始します(またはCPU時間を待機します)。一度に実行するスレッドの数に注意する必要があります。経験則では、スレッドの実行に使用できる「実行ユニット」の2倍以上のアクティブなスレッドがある場合、マルチスレッドの利点はオーバーヘッドによって克服されます。ただし、これを比較的簡単に考慮したシステムを設計できるはずです。
  • 結果のコレクションのディクショナリを作成して、結果のコレクションを分離します。各結果コレクションは、処理を実行するスレッドによって運ばれ、コールバックに渡されるトークンにキー設定されます。ディクショナリは、ロックせずに一度に複数の要素を読み取ることができます。各スレッドはディクショナリ内の異なるコレクションに書き込みを行うため、これらのリストをロックする必要はありません(ロックしたとしても、ロックする必要はありません)。他のスレッドをブロックする)。その結果、新しいスレッドの新しいコレクションが追加されたときに、スレッドをブロックするようにロックする必要がある唯一のコレクションがメインディクショナリになります。トークンのリサイクルに精通しているのであれば、これは頻繁に発生する必要はありません。
于 2011-02-21T17:50:07.523 に答える