73

画像がデータベースに保存されているデータベースを、ハードドライブ上のファイルを指すデータベースのレコードに移行しようとしています。このメソッドを使用してデータをクエリするParallel.ForEachプロセスを高速化するために使用しようとしていました。

しかし、OutOfMemory例外が発生していることに気づきました。列挙型のバッチをクエリして、クエリの間隔を空けるためのオーバーヘッドのコストを軽減することを知ってParallel.ForEachいます(したがって、クエリの間隔を空けるのではなく、一度に一連のクエリを実行すると、ソースの次のレコードがメモリにキャッシュされる可能性が高くなりますアウト)。この問題は、返されるレコードの1つが1〜4 Mbバイトの配列であり、キャッシュによってアドレス空間全体が使い果たされていることが原因です(ターゲットプラットフォームは32ビットであるため、プログラムはx86モードで実行する必要があります)マシーン)

キャッシュを無効にする方法や、TPLのサイズを小さくする方法はありますか?


これは、問題を表示するためのサンプルプログラムです。これは、x86モードでコンパイルして、時間がかかるか、マシンで発生していない場合に問題を表示する必要があります。アレイのサイズが大きくなります(マシン1 << 20で約30秒かかり、4 << 20ほぼ瞬時に発生しました)

class Program
{

    static void Main(string[] args)
    {
        Parallel.ForEach(CreateData(), (data) =>
            {
                data[0] = 1;
            });
    }

    static IEnumerable<byte[]> CreateData()
    {
        while (true)
        {
            yield return new byte[1 << 20]; //1Mb array
        }
    }
}
4

4 に答える 4

106

のデフォルト オプションはParallel.ForEach 、タスクが CPU バウンドで線形にスケーリングする場合にのみ適切に機能します。タスクが CPU バウンドの場合、すべてが完全に機能します。クアッドコアがあり、他のプロセスが実行されていない場合Parallel.ForEachは、4 つのプロセッサすべてを使用します。クアッドコアがあり、コンピュータ上の他のプロセスが 1 つのフル CPU をParallel.ForEach使用している場合、約 3 つのプロセッサを使用します。

しかし、タスクが CPU バウンドでない場合は、タスクParallel.ForEachを開始し続け、すべての CPU をビジー状態に保つために懸命に努力します。しかし、いくつのタスクが並行して実行されていても、常に未使用の CPU 処理能力が増えるため、タスクが作成され続けます。

タスクが CPU バウンドかどうかはどうすればわかりますか? うまくいけば、それを検査するだけです。素数を因数分解していれば、それは明らかです。しかし、他のケースはそれほど明白ではありません。タスクが CPU バウンドかどうかを経験的に判断する方法は、最大並列度を制限しParallelOptions.MaximumDegreeOfParallelism、プログラムの動作を観察することです。タスクが CPU バウンドの場合、クアッドコア システムでは次のようなパターンが表示されます。

  • ParallelOptions.MaximumDegreeOfParallelism = 1: 1 つのフル CPU または 25% の CPU 使用率を使用する
  • ParallelOptions.MaximumDegreeOfParallelism = 2: 2 つの CPU または 50% の CPU 使用率を使用
  • ParallelOptions.MaximumDegreeOfParallelism = 4: すべての CPU または 100% の CPU 使用率を使用

このように動作する場合は、デフォルトParallel.ForEachのオプションを使用して良い結果を得ることができます。線形の CPU 使用率は、適切なタスク スケジューリングを意味します。

しかし、Intel i7 でサンプル アプリケーションを実行すると、設定した最大並列度に関係なく、約 20% の CPU 使用率が得られます。どうしてこれなの?大量のメモリが割り当てられているため、ガベージ コレクターがスレッドをブロックしています。アプリケーションはリソースにバインドされており、リソースはメモリです。

同様に、データベース サーバーに対して実行時間の長いクエリを実行する I/O バウンド タスクも、ローカル コンピューターで利用可能なすべての CPU リソースを効果的に利用することはできません。そのような場合、タスク スケジューラは新しいタスクの開始を「いつ停止するかを知る」ことができません。

タスクが CPU バウンドでない場合、または CPU 使用率が並列度の最大値に比例しない場合はParallel.ForEach、一度に多くのタスクを開始しないようにアドバイスする必要があります。最も簡単な方法は、オーバーラップする I/O バウンド タスクに対してある程度の並列処理を許可する数を指定することですが、ローカル コンピューターのリソース要求を圧倒したり、リモート サーバーに過剰な負担をかけたりすることはありません。最良の結果を得るには、試行錯誤が必要です。

static void Main(string[] args)
{
    Parallel.ForEach(CreateData(),
        new ParallelOptions { MaxDegreeOfParallelism = 4 },
        (data) =>
            {
                data[0] = 1;
            });
}
于 2011-08-08T04:08:56.803 に答える
47

したがって、リックが示唆したことは間違いなく重要なポイントですが、私が欠けていると思うもう 1 つの点は、パーティショニングの議論です。

Parallel::ForEach長さが不明な に対して、チャンク分割戦略を使用するデフォルトのPartitioner<T>実装を使用します。IEnumerable<T>これが意味することはParallel::ForEach、データ セットの処理に使用する各ワーカー スレッドが からいくつかの要素を読み取り、IEnumerable<T>そのスレッドによってのみ処理されることです (今のところ、ワーク スティーリングは無視します)。これは、常にソースに戻って新しい作業を割り当て、別のワーカー スレッドにスケジュールしなければならないという費用を節約するために行われます。したがって、通常、これは良いことです。ただし、特定のシナリオでは、クアッド コアをMaxDegreeOfParallelism使用していて、作業用に 4 つのスレッドに設定し、それぞれが 100 個の要素のチャンクをプルするとします。IEnumerable<T>. その特定のワーカー スレッドだけでも 100 ~ 400 メガバイトですよね?

では、これをどのように解決しますか?簡単です。カスタムPartitioner<T>実装を作成します。さて、チャンクはあなたの場合でもまだ役に立ちます。そのため、単一要素のパーティション分割戦略を使用したくないでしょう。そのために必要なすべてのタスク調整でオーバーヘッドが発生するからです。代わりに、ワークロードの最適なバランスが見つかるまで appsetting を介して調整できる構成可能なバージョンを作成します。幸いなことに、このような実装を作成するのは非常に簡単ですが、実際に自分で作成する必要さえありません。これは、PFX チームが既に作成しており、並列プログラミング サンプル プロジェクトに入れているためです。

于 2011-08-08T14:52:20.100 に答える
15

この問題は、並列度ではなく、パーティショナーに関係しています。解決策は、カスタム データ パーティショナーを実装することです。

データセットが大きい場合、TPL の mono 実装はメモリ不足になることが保証されているようです。これは最近私に起こりました (基本的に、上記のループを実行していたところ、OOM 例外が発生するまでメモリが直線的に増加することがわかりました)。 )。

問題を追跡した後、モノはデフォルトで EnumerablePartitioner クラスを使用して列挙子を分割することがわかりました。このクラスには、タスクにデータを渡すたびに、2 倍ずつ増加する (そして変更不可能な) 係数でデータを「チャンク」するという動作があります。そのため、タスクが最初にデータを要求すると、サイズのチャンクが取得されます。 1、サイズの次の時間は 2*1=2、次回は 2*2=4、次に 2*4=8 など。結果は、タスクに渡されたデータの量であり、したがって、メモリは同時に増加し、タスクの長さとともに増加し、大量のデータが処理されている場合、メモリ不足の例外が必然的に発生します。

おそらく、この動作の本来の理由は、データを取得するために各スレッドが複数回戻ることを避けたいということですが、処理中のすべてのデータがメモリに収まるという仮定に基づいているようです (から読み取る場合はそうではありません)。大きなファイル)。

この問題は、前述のカスタム パーティショナーで回避できます。一度に 1 項目ずつデータを各タスクに単純に返す一般的な例の 1 つを次に示します。

https://gist.github.com/evolvedmicrobe/7997971

最初にそのクラスをインスタンス化し、列挙型自体の代わりに Parallel.For に渡すだけです。

于 2013-12-17T00:45:46.563 に答える
-2

カスタム パーティショナーを使用することが間違いなく最も「正しい」答えですが、より簡単な解決策はガベージ コレクターに追いつくことです。私が試したケースでは、関数内で parallel.for ループを繰り返し呼び出していました。ここで説明されているように、プログラムによって使用されるメモリが直線的に増加し続けるたびに、関数を終了するにもかかわらず。追加した:

//Force garbage collection.
GC.Collect();
// Wait for all finalizers to complete before continuing.
GC.WaitForPendingFinalizers();

超高速ではありませんが、メモリの問題は解決しました。おそらく、CPU 使用率とメモリ使用率が高いと、ガベージ コレクターが効率的に動作しません。

于 2019-04-05T17:06:51.330 に答える