web-dev-qa-db-ja.com

Parallel.Forでの期待外れのパフォーマンス

Parallel.Forを使用して、計算時間を短縮しようとしています。私は8コアのIntelCore i7 Q840 CPUを持っていますが、シーケンシャルforループと比較して4のパフォーマンス比しか得られません。これはParallel.Forで得られるのと同じくらい良いですか、それともメソッド呼び出しを微調整してパフォーマンスを向上させることができますか?

これが私のテストコードです。

var loops = 200;
var perloop = 10000000;

var sum = 0.0;
for (var k = 0; k < loops; ++k)
{
    var sumk = 0.0;
    for (var i = 0; i < perloop; ++i) sumk += (1.0 / i) * i;
    sum += sumk;
}

並列:

sum = 0.0;
Parallel.For(0, loops,
                k =>
                    {
                        var sumk = 0.0;
                        for (var i = 0; i < perloop; ++i) sumk += (1.0 / i) * i;
                        sum += sumk;
                    });

私が並列化しているループには、「グローバルに」定義された変数sumを使用した計算が含まれますが、これは並列化されたループ内の合計時間のごくわずかな部分にすぎないはずです。

リリースビルド(「コードの最適化」フラグが設定されている)では、シーケンシャルforループは私のコンピューターでは33.7秒かかりますが、Parallel.Forループは8.4秒かかります。わずか4.0の比率。

タスクマネージャーでは、CPU使用率が順次計算では10〜11%であるのに対し、並列計算では70%にすぎないことがわかります。明示的に設定しようとしました

ParallelOptions.MaxDegreesOfParallelism = Environment.ProcessorCount

しかし、役に立たない。すべてのCPUパワーが並列計算に割り当てられない理由は私にはわかりません。

Sequential vs. parallel CPU utilization

SO before で同様の質問が提起され、さらに残念な結果が出ていることに気づきました。しかし、その質問には、サードパーティでの劣った並列化も含まれていました。私の主な関心事は、コアライブラリの基本的な操作の並列化です。

[〜#〜]更新[〜#〜]

いくつかのコメントで、私が使用しているCPUには4つの物理コアしかないことが指摘されました。これは、ハイパースレッディングが有効になっている場合、システムには8コアとして表示されます。そのために、ハイパースレッディングを無効にして再ベンチマークしました。

ハイパースレッディングdisabledを使用すると、計算が高速になり、並列と(私が思っていた)順次の両方になりますforループ。 forループ中のCPU使用率は最大で約1です。 Parallel.Forループ中は45%(!!!)および100%。

forループの計算時間は15.6秒(ハイパースレッディング有効の2倍以上)、Parallel.Forの計算時間は6.2秒(25%)ハイパースレッディングが有効の場合よりも優れています。 Parallel.Forのパフォーマンス比は2.5のみになり、4つの実際のコアで実行されます。

そのため、ハイパースレッディングが無効になっているにもかかわらず、パフォーマンス比は予想よりも大幅に低くなっています。一方、forループ中にCPU使用率が非常に高いのは興味深いですか?このループでも、ある種の内部並列化が行われている可能性がありますか?

18

グローバル変数を使用すると、ロックを使用していない場合でも、重大な同期の問題が発生する可能性があります。変数に値を割り当てる場合、各コアはシステムメモリ内の同じ場所にアクセスするか、他のコアが終了するのを待ってからアクセスする必要があります。ライター Interlocked.Add メソッドを使用してOSレベルでアトミックに合計に値を追加することで、ロックなしの破損を回避できますが、競合による遅延が発生します。

これを行う適切な方法は、スレッドローカル変数を更新して部分合計を作成し、最後にそれらすべてを単一のグローバル合計に追加することです。 Parallel.For これを行うオーバーロードがあります。 MSDNには、合計を使用した例もあります 方法:スレッドローカル変数を持つParallel.Forループを作成する

        int[] nums = Enumerable.Range(0, 1000000).ToArray();
        long total = 0;

        // Use type parameter to make subtotal a long, not an int
        Parallel.For<long>(0, nums.Length, () => 0, (j, loop, subtotal) =>
        {
            subtotal += nums[j];
            return subtotal;
        },
            (x) => Interlocked.Add(ref total, x)
        );

各スレッドは、独自の小計値を更新し、インターロックを使用してグローバル合計を更新します。終了したら追加します。

24

Parallel.ForとParallel.ForEachは、適切と思われるある程度の並列処理を使用して、スレッドのセットアップと破棄のコストと、各スレッドが実行すると予想される作業のバランスを取ります。 。NET 4.5は、以前の.NETバージョンと比較して、パフォーマンスにいくつかの改善を加えました(スピンアップするスレッドの数に関するよりインテリジェントな決定を含む)。

コアごとに1つのスレッドをスピンアップしたとしても、コンテキストスイッチ、 偽共有 の問題、リソースロック、およびその他の問題により、線形スケーラビリティを達成できない場合があることに注意してください(通常、必ずしも特定のコード例)。

6
Eric J.

コードが反復ごとに他のタスクで作業するのが「簡単すぎる」ため、計算ゲインが非常に低いと思います。parallel.forは反復ごとに新しいタスクを作成するだけなので、スレッドでそれらを処理するには時間がかかります。私はそれをこのようにします:

int[] nums = Enumerable.Range(0, 1000000).ToArray();
long total = 0;

Parallel.ForEach(
    Partitioner.Create(0, nums.Length),
    () => 0,
    (part, loopState, partSum) =>
    {
        for (int i = part.Item1; i < part.Item2; i++)
        {
            partSum += nums[i];
        }
        return partSum;
    },
    (partSum) =>
    {
        Interlocked.Add(ref total, partSum);
    }
);

Partitionerは、各タスクのジョブの最適な部分を作成し、スレッドを使用したサービスタスクの時間が短縮されます。可能であれば、このソリューションのベンチマークを行い、速度が向上するかどうかをお知らせください。

5
Jacob Sobus

各例のforeachとparallel

    for (int i = 0; i < 10; i++)
    {
        int[] array = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29 };
        Stopwatch watch = new Stopwatch();
        watch.Start();
        //Parallel foreach
        Parallel.ForEach(array, line =>
        {
            for (int x = 0; x < 1000000; x++)
            {

            }

        });

        watch.Stop();
        Console.WriteLine("Parallel.ForEach {0}", watch.Elapsed.Milliseconds);
        watch = new Stopwatch();
        //foreach
        watch.Start();
        foreach (int item in array)
        {
            for (int z = 0; z < 10000000; z++)
            {

            }
        }
        watch.Stop();
        Console.WriteLine("ForEach {0}", watch.Elapsed.Milliseconds);

        Console.WriteLine("####");
    }
    Console.ReadKey();

enter image description here

私のCPU

インテル®Core™i7-620Mプロセッサー(4Mキャッシュ、2.66 GHz)

1
mesutpiskin