13

驚くべきことに、私が作成した小さなテスト ケースでは、PLINQ を使用してもメリットはありませんでした。実際、通常の LINQ よりもさらに悪かったのです。

テストコードは次のとおりです。

    int repeatedCount = 10000000;
    private void button1_Click(object sender, EventArgs e)
    {
        var currTime = DateTime.Now;
        var strList = Enumerable.Repeat(10, repeatedCount);
        var result = strList.AsParallel().Sum();

        var currTime2 = DateTime.Now;
        textBox1.Text = (currTime2.Ticks-currTime.Ticks).ToString();

    }

    private void button2_Click(object sender, EventArgs e)
    {
        var currTime = DateTime.Now;
        var strList = Enumerable.Repeat(10, repeatedCount);
        var result = strList.Sum();

        var currTime2 = DateTime.Now;
        textBox2.Text = (currTime2.Ticks - currTime.Ticks).ToString();
    }

結果?

textbox1: 3437500
textbox2: 781250

したがって、LINQ は PLINQ よりも短い時間で同様の操作を完了できます。

私は何を間違っていますか?それとも、私が知らないひねりがありますか?

編集:ストップウォッチを使用するようにコードを更新しましたが、同じ動作が持続しました。JIT の効果を割り引くために、実際に と と の両方を順不同でクリックしてみましbutton1button2。得られた時間は異なる可能性がありますが、質的な動作は変わりませんでした。この場合、PLINQ は確かに低速でした。

4

9 に答える 9

23

最初: DateTime を使用して実行時間を測定するのをやめます。代わりにストップウォッチを使用してください。テストコードは次のようになります。

var watch = new Stopwatch();

var strList = Enumerable.Repeat(10, 10000000);

watch.Start();
var result = strList.Sum();
watch.Stop();

Console.WriteLine("Linear: {0}", watch.ElapsedMilliseconds);

watch.Reset();

watch.Start();
var parallelResult = strList.AsParallel().Sum();
watch.Stop();

Console.WriteLine("Parallel: {0}", watch.ElapsedMilliseconds);

Console.ReadKey();

2 つ目:並列で実行すると、オーバーヘッドが追加されます。この場合、PLINQ は、要素を並列で安全に合計できるように、コレクションを分割する最適な方法を見つけ出す必要があります。その後、作成されたさまざまなスレッドからの結果を結合し、それらも合計する必要があります。これは簡単な作業ではありません。

上記のコードを使用すると、Sum() を使用すると約 95 ミリ秒の呼び出しになることがわかります。.AsParallel().Sum() を呼び出すと、約 185 ミリ秒になります。

並列でタスクを実行することは、それを実行して何かを得た場合にのみ良い考えです。この場合、Sum は、PLINQ を使用しても得られない単純なタスクです。

于 2010-07-28T15:33:35.903 に答える
22

これは典型的な間違いです。「簡単なテストを実行して、このシングルスレッド コードとこのマルチスレッド コードのパフォーマンスを比較しよう」と考えています。

単純なテストは、マルチスレッドのパフォーマンスを測定するために実行できる最悪の種類のテストです。

通常、一部の操作を並列化すると、並列化するステップでかなりの作業が必要な場合にパフォーマンスが向上します。手順が単純な場合 (例: クイック*)、作業を並列化するオーバーヘッドは、他の方法で得られるわずかなパフォーマンスの向上よりもはるかに小さくなります。


この類推を考えてみましょう。

建物を建設中です。作業員が 1 人の場合、1 つの壁を作るまでレンガを 1 つずつ積み上げ、次の壁にも同じ作業を行い、すべての壁が構築され接続されるまで続けます。これは、並列化の恩恵を受けることができる、時間のかかる骨の折れる作業です。

これを行う正しい方法は、壁の構築を並列化することです。たとえば、さらに 3 人の労働者を雇い、各労働者に独自の壁を構築させて、4 つの壁を同時に構築できるようにします。3 つの余分なワーカーを見つけてタスクを割り当てるのにかかる時間は、以前は 1 を構築するのにかかった時間で 4 つの壁を構築することによって得られる節約と比較すると、取るに足らないものです。

それを行う間違った方法は、レンガの敷設を並列化することです。さらに約 1000 人の労働者を雇い、各労働者が一度に 1 つのレンガを敷設する責任を負います。「1 人で 1 分間に 2 個のレンガを積むことができるなら、1000 人の作業員で 1 分間に 2000 個のレンガを積むことができるはずだから、この仕事はすぐに終わるだろう!」と思うかもしれません。しかし現実には、ワークロードをこのような微視的なレベルで並列化することにより、膨大な量のエネルギーを収集してすべてのワーカーを調整し、タスクを割り当てて (「レンガをすぐそこに置く」)、膨大な量のエネルギーを無駄にしています。仕事が他人の仕事などに干渉している。

したがって、この類推の教訓は次のとおりです。一般に、並列化を使用して実質的な作業単位(壁など)を分割しますが、実質的でない単位 (レンガなど) は通常の順次的な方法で処理します。


Thread.Sleep(100)*このため、高速に実行されるコードを取得し、その末尾に (またはその他の乱数を)追加することで、より作業集約的なコンテキストでの並列化によるパフォーマンスの向上を実際にかなり正確に見積もることができます。突然、このコードの順次実行は反復ごとに 100 ミリ秒遅くなり、並列実行の速度は大幅に低下します。

于 2010-07-28T16:17:34.797 に答える
8

他の人はあなたのベンチマークのいくつかの欠陥を指摘しています。簡単にするための短いコンソールアプリは次のとおりです。

using System;
using System.Diagnostics;
using System.Linq;

public class Test
{
    const int Iterations = 1000000000;

    static void Main()
    {
        // Make sure everything's JITted
        Time(Sequential, 1);
        Time(Parallel, 1);
        Time(Parallel2, 1);
        // Now run the real tests
        Time(Sequential, Iterations);
        Time(Parallel,   Iterations);
        Time(Parallel2,  Iterations);
    }

    static void Time(Func<int, int> action, int count)
    {
        GC.Collect();
        Stopwatch sw = Stopwatch.StartNew();
        int check = action(count);
        if (count != check)
        {
            Console.WriteLine("Check for {0} failed!", action.Method.Name);
        }
        sw.Stop();
        Console.WriteLine("Time for {0} with count={1}: {2}ms",
                          action.Method.Name, count,
                          (long) sw.ElapsedMilliseconds);
    }

    static int Sequential(int count)
    {
        var strList = Enumerable.Repeat(1, count);
        return strList.Sum();
    }

    static int Parallel(int count)
    {
        var strList = Enumerable.Repeat(1, count);
        return strList.AsParallel().Sum();
    }

    static int Parallel2(int count)
    {
        var strList = ParallelEnumerable.Repeat(1, count);
        return strList.Sum();
    }
}

コンパイル:

csc /o+ /debug- Test.cs

私のクアッドコアi7ラップトップでの結果。最大2コアの高速、または4コアの低速実行。基本的にParallelEnumerable.Repeat勝ち、シーケンスバージョンが続き、通常の並列化が続きEnumerable.Repeatます。

Time for Sequential with count=1: 117ms
Time for Parallel with count=1: 181ms
Time for Parallel2 with count=1: 12ms
Time for Sequential with count=1000000000: 9152ms
Time for Parallel with count=1000000000: 44144ms
Time for Parallel2 with count=1000000000: 3154ms

この回答の以前のバージョンは、要素の数が間違っているために恥ずかしいほどの欠陥があったことに注意してください。上記の結果には、はるかに自信があります。

于 2010-07-28T15:44:52.790 に答える
1

JIT時間を考慮していない可能性はありますか?テストを 2 回実行し、最初の結果セットを破棄する必要があります。

また、パフォーマンスのタイミングを取得するために DateTime を使用しないでくださいStopwatch。代わりにクラスを使用してください。

var swatch = new Stopwatch();
swatch.StartNew();

var strList = Enumerable.Repeat(10, repeatedCount); 
var result = strList.AsParallel().Sum(); 

swatch.Stop();
textBox1.Text = swatch.Elapsed;

PLINQ は、シーケンスの処理にいくらかのオーバーヘッドを追加します。しかし、あなたの場合の大きさの違いは過度に思えます。PLINQ は、複数のコア/CPU でロジックを実行する利点がオーバーヘッド コストを上回る場合に意味があります。複数のコアがない場合、並列処理を実行しても実際の利点はありません。PLINQ はそのようなケースを検出し、処理を順番に実行する必要があります。

編集: この種の組み込みパフォーマンス テストを作成するときは、デバッガーで実行していないこと、または Intellitrace を有効にして実行していないことを確認する必要があります。パフォーマンスのタイミングが大幅に歪む可能性があるためです。

于 2010-07-28T15:32:28.757 に答える
0

オーバーヘッドに関するジャスティンのコメントはまさに正しいです。

PLINQ の使用を超えて、一般的に並行ソフトウェアを作成するときに考慮すべきこと:

作業項目の「粒度」について常に考える必要があります。 一部の問題は、フレーム全体を同時にレイトレーシングするなど、非常に高いレベルで「チャンク」できるため、並列化に非常に適しています (この種の問題は恥ずかしいほど並列と呼ばれます)。作業の「チャンク」が非常に大きい場合、複数のスレッドを作成および管理するオーバーヘッドは、実行したい実際の作業と比較して無視できます。

PLINQ を使用すると、並行プログラミングが容易になりますが、作業の粒度を無視できるわけではありません。

于 2010-07-28T16:05:41.120 に答える
0

コンテキストスイッチの数を増やしていて、スレッドが i/o 完了などを待機することの利点となるアクションを実行していないため、実際にそうなる可能性があります。単一の CPU ボックスで実行している場合、これはさらに悪化します。

于 2010-07-28T15:34:32.077 に答える
0

タイミング メトリックには Stopwatch クラスを使用することをお勧めします。あなたの場合、それは間隔のより良い尺度です。

于 2010-07-28T15:35:14.740 に答える
0

この記事の副作用のセクションをお読みください。

http://msdn.microsoft.com/en-us/magazine/cc163329.aspx

PLINQ には追加のデータ処理パターンがあり、純粋に応答時間が常に高速であると考える前に理解する必要がある多くの状況に遭遇する可能性があると思います。

于 2010-07-28T15:41:15.457 に答える