この記事では、正しい結果を得ながら、可能な限り効率的な PLINQ クエリを記述するのに役立つ情報を提供します。
PLINQ の主な目的は、マルチコア コンピューターでクエリ デリゲートを並列に実行することで、LINQ to Objects クエリの実行を高速化することです。 PLINQ は、ソース コレクション内の各要素の処理が独立し、個々のデリゲート間で共有状態が関係しない場合に最適に実行されます。 このような操作は LINQ to Objects と PLINQ で一般的であり、多くの場合、複数のスレッドでのスケジュール設定に簡単に対応できるため、"楽しく並列" と呼ばれます。 ただし、すべてのクエリが完全に正常な並列操作で構成されているわけではありません。 ほとんどの場合、クエリには、並列化できない演算子や、並列実行の速度が低下する演算子が含まれます。 また、完全に並列なクエリであっても、PLINQ はデータ ソースをパーティション分割し、スレッドの作業をスケジュールし、通常はクエリが完了したときに結果をマージする必要があります。 これらすべての操作は、並列化の計算コストに追加されます。並列化を追加するこれらのコストは 、オーバーヘッドと呼ばれます。 PLINQ クエリで最適なパフォーマンスを実現するために、目標は、楽しく平行な部分を最大化し、オーバーヘッドを必要とする部分を最小限に抑することです。
PLINQ クエリのパフォーマンスに影響を与える要因
次のセクションでは、並列クエリのパフォーマンスに影響を与える最も重要な要因の一部を示します。 これらは一般的なステートメントであり、すべてのケースでクエリのパフォーマンスを予測するのに十分ではありません。 常に、代表的な構成と負荷の範囲を持つコンピューター上の特定のクエリの実際のパフォーマンスを測定することが重要です。
全体的な作業の計算コスト。
高速化を実現するには、PLINQ クエリにオーバーヘッドをオフセットするのに十分な並列処理が必要です。 この作業は、各デリゲートの計算コストにソース コレクション内の要素の数を乗算して表すことができます。 演算を並列化できる場合、計算コストが高いほど、高速化の機会が大きくなります。 たとえば、関数の実行に 1 ミリ秒かかる場合、1000 要素を超えるシーケンシャル クエリはその操作を実行するのに 1 秒かかりますが、4 つのコアを持つコンピューターでの並列クエリには 250 ミリ秒しかかかりません。 これにより、750 ミリ秒の高速化が実現します。 関数が各要素に対して 1 秒実行する必要がある場合、高速化は 750 秒になります。 デリゲートのコストが非常に高い場合、PLINQ はソース コレクション内の少数の項目のみで大幅な高速化を提供する可能性があります。 逆に、単純なデリゲートを含む小さなソース コレクションは、通常、PLINQ の候補として適していません。
次の例では、Select 関数に多くの作業が含まれていると仮定すると、queryA はおそらく PLINQ の候補として適しています。 Select ステートメントに十分な作業がないため、queryB はおそらく適していません。並列処理のオーバーヘッドは、ほとんどの場合、またはすべての高速化をオフセットするためです。
Dim queryA = From num In numberList.AsParallel() Select ExpensiveFunction(num); 'good for PLINQ Dim queryB = From num In numberList.AsParallel() Where num Mod 2 > 0 Select num; 'not as good for PLINQ
var queryA = from num in numberList.AsParallel() select ExpensiveFunction(num); //good for PLINQ var queryB = from num in numberList.AsParallel() where num % 2 > 0 select num; //not as good for PLINQ
システム上の論理コアの数 (並列処理の次数)。
この点は、前のセクションに対する明白な併置であり、より多くのコアを持つマシンでは、作業をより多くの同時実行スレッドに分割できるため、並列クエリが高速に実行されます。 全体的な高速化の量は、クエリの全体的な作業の割合によって異なります。 ただし、すべてのクエリが 8 コア コンピューターで 4 コア コンピューターの 2 倍の速度で実行されるとは限りません。 最適なパフォーマンスを得るためにクエリをチューニングする場合は、さまざまなコア数のコンピューターで実際の結果を測定することが重要です。 この点は、ポイント 1 に関連しています。より大きなコンピューティング リソースを利用するには、より大きなデータセットが必要です。
操作の数と種類。
PLINQ は、ソース シーケンス内の要素の順序を維持する必要がある状況に対して AsOrdered 演算子を提供します。 順序付けにはコストがかかりますが、通常、このコストは控えめです。 GroupBy 操作と Join 操作も同様にオーバーヘッドが発生します。 PLINQ は、ソース コレクション内の要素を任意の順序で処理でき、準備ができたらすぐに次の演算子に渡す場合に最適なパフォーマンスを発揮します。 詳細については、「 PLINQ での注文の保持」を参照してください。
クエリ実行の形式。
ToArray または ToList を呼び出してクエリの結果を格納する場合は、すべての並列スレッドの結果を単一のデータ構造にマージする必要があります。 これには、避けられない計算コストが伴います。 同様に、foreach (Visual Basic の For Each) ループを使用して結果を反復処理する場合、ワーカー スレッドからの結果を列挙子スレッドにシリアル化する必要があります。 ただし、各スレッドの結果に基づいて何らかのアクションを実行するだけの場合は、ForAll メソッドを使用して複数のスレッドでこの作業を実行できます。
マージ オプションの種類。
PLINQ は、出力をバッファーに格納し、結果セット全体が生成された後にチャンクまたはすべてを一度に生成するか、または生成された個々の結果をストリーミングするように構成できます。 前者は全体的な実行時間が短縮され、後者は生成された要素間の待機時間が短縮されます。 マージ オプションは常にクエリの全体的なパフォーマンスに大きな影響を与えるわけではありませんが、ユーザーが結果を見るのを待機する必要がある時間を制御するため、認識されるパフォーマンスに影響を与える可能性があります。 詳細については、「 PLINQ のマージ オプション」を参照してください。
パーティション分割の種類。
場合によっては、インデックス可能なソース コレクションに対する PLINQ クエリによって、作業負荷が不均衡になる可能性があります。 このような場合は、カスタム パーティショナーを作成することで、クエリのパフォーマンスを向上させることができます。 詳細については、「 PLINQ と TPL のカスタム パーティショナー」を参照してください。
PLINQ がシーケンシャル モードを選択する場合
PLINQ は、少なくともクエリが順番に実行されるのと同じ速度で、常にクエリの実行を試みます。 PLINQ では、ユーザー デリゲートの計算コストや入力ソースの大きさは調べませんが、特定のクエリ "図形" が検索されます。具体的には、通常、クエリが並列モードでより遅く実行される原因となるクエリ演算子または演算子の組み合わせを検索します。 このような図形が見つかると、PLINQ は既定でシーケンシャル モードにフォールバックします。
ただし、特定のクエリのパフォーマンスを測定した後、並列モードで実際に実行速度が速くなる場合があります。 このような場合は、WithExecutionMode メソッドを使用して ParallelExecutionMode.ForceParallelism フラグを使用して、クエリを並列化するように PLINQ に指示できます。 詳細については、「 方法: PLINQ で実行モードを指定する」を参照してください。
次の一覧では、PLINQ が既定でシーケンシャル モードで実行するクエリ図形について説明します。
元のインデックスを削除または並べ替えた順序付け演算子またはフィルター演算子の後に Select 句、インデックス付き Where 句、インデックス付き SelectMany 句、または ElementAt 句を含むクエリ。
Take、TakeWhile、Skip、SkipWhile 演算子を含み、ソース シーケンス内のインデックスが元の順序にないクエリ。
データ ソースの 1 つが最初に順序付けされたインデックスを持ち、もう一方のデータ ソースがインデックス可能である (つまり、配列または IList(T)) 場合を除き、Zip または SequenceEquals を含むクエリ。
インデックス可能なデータ ソースに適用されない限り、Concat を含むクエリ。
インデックス可能なデータ ソースに適用されない限り、Reverse を含むクエリ。
こちらも参照ください
.NET