web-dev-qa-db-ja.com

Parallel.ForEachは、ラージオブジェクトで列挙可能なオブジェクトを操作する場合、「メモリ不足」例外を引き起こす可能性があります

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

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

キャッシングを無効にする方法や、TPLのmakeを小さくする方法はありますか?


問題を表示するプログラムの例を次に示します。これは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
        }
    }
}
63

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

同様に、データベースサーバーに対して長時間実行されるクエリを実行する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;
            });
}
94
Rick Sladkey

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

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

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

41
Drew Marsh

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

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

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

おそらく、この動作の元の理由は、各スレッドがデータを取得するために複数回返すことを避けたいということですが、処理されているすべてのデータがメモリに収まる可能性があるという仮定に基づいているようです(大きなファイル)。

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

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

最初にそのクラスをインスタンス化し、列挙可能なものの代わりにParallel.Forに渡します。

14
evolvedmicrobe