忍者ブログ

Memeplexes

プログラミング、3DCGとその他いろいろについて

Parallel.ForEachの使い方

Parallel.ForEach()の使い方をまとめようと思います。
Parallel.ForEach()はデータを並列に処理するときに使います。
参考:Parallel Aggregation


ふつうのforeachを使う

まずは、普通のforeachを使った場合。
Parallel.ForEach()を使わなかった場合どうなるかです。
using System;
using System.Linq;
using System.Diagnostics;

class Program
{
    static void Main(string[] args)
    {
        var numbers = Enumerable.Range(0, 100000000).ToArray();
        ulong sum = 0;

        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();

        foreach (var number in numbers)
        {
            sum += (ulong)number;
        }
        

        stopwatch.Stop();
        Console.WriteLine("sum : " + sum);
        Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds + "ms");
        Console.ReadLine();
    }
}
実行結果はこうなります。

sum : 4999999950000000
449.1242ms

私の環境では実行時間は0.5秒くらいですね。

Parallel.ForEachを使う(エラー!) 

error!
using System;
using System.Linq;
using System.Diagnostics;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        var numbers = Enumerable.Range(0, 100000000).ToArray();
        ulong sum = 0;

        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();

        Parallel.ForEach(
            numbers,
            number =>
        {
            sum += (ulong)number;
        });

        stopwatch.Stop();
        Console.WriteLine("sum : " + sum);
        Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds + "ms");
        Console.ReadLine();
    }
}


実行結果はこうなります。
sum : 1852281973816843
1711.2386ms


遅くなってる!?
なんという事でしょうか並列実行したのに実行時間が増えています。
私の環境には8コアあるというのにです。
おそらくこれはオーバーヘッドのためです。
Parallel.ForEachは気軽には使えないということでしょうか…。

そしてなによりも計算結果が間違っています。
ひどいですね。
これは複数のスレッドが一度にひとつの変数sumを読み書きしたせいでしょう。


lockで計算結果を正す

では計算結果を正しくするにはどうすればいいのでしょう?
sumを読み書きできるのはひとつのスレッドだけということにすればいいのです。
これにはlockを使います。
using System;
using System.Linq;
using System.Diagnostics;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        var numbers = Enumerable.Range(0, 100000000).ToArray();
        ulong sum = 0;

        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();

        Parallel.ForEach(
            numbers,
            number =>
        {
            lock (numbers)
            {
                sum += (ulong)number;
            }
        });

        stopwatch.Stop();
        Console.WriteLine("sum : " + sum);
        Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds + "ms");
        Console.ReadLine();
    }
}



sum : 4999999950000000
9717.7726ms

結果は正しくなりました。
しかし更に遅くなっています。
約10秒です。


スレッドローカルデータを使う

このままではあまりにも遅いので何とかしましょう。
lockを毎回使っているのを何とかするのです。
lockは重いそうですからね。

そこで1スレッドにあるulongの変数を用意して、そこに(lockを使わず)足しあわせ、
最後にlockを使って足し合わせます。
using System;
using System.Linq;
using System.Diagnostics;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        var numbers = Enumerable.Range(0, 100000000).ToArray();
        ulong sum = 0;

        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();

        Parallel.ForEach(
            numbers,
            () => (ulong)0,
            (number, state, localSum) =>
            {
                return localSum + (uint)number;
            },
            threadLocalSum =>
            {
                lock (numbers)
                {
                    sum += threadLocalSum;
                }
            });

        stopwatch.Stop();
        Console.WriteLine("sum : " + sum);
        Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds + "ms");
        Console.ReadLine();
    }
}



実行結果はこうなります。
sum : 4999999950000000
580.938ms
実行時間がさっきよりは短くなりました。
しかし相変わらず普通のforeachよりは遅いです。
グラフにすると次のようになります。



繰り返し回数が1の時異常にパフォーマンスが低くなっているのが謎です。
あと並列のほうが遅いのも変ですね。

結論

今回試したケースではParallel.ForEachよりforeachのほうが速いです…

Parallel.ForEachのほうが速くなるケース

しかしそれだけでは結論として物足りないので、Parallel.ForEachの方が早くなるプログラムを作ってみました。
もっとも私の環境ではという条件付きですが…。

using System;
using System.Linq;
using System.Diagnostics;
using System.Threading.Tasks;

class Program
{
    const int iteration = 1000;
    const int numbers2Count = 10000;

    static void Main(string[] args)
    {
        var numbers = Enumerable.Range(0, iteration);

        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();

        var sum = serialSum(numbers);
        //var sum = parallelSum(numbers);

        stopwatch.Stop();
        Console.WriteLine("sum : " + sum);
        Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds + "ms");
        Console.ReadLine();
    }

    private static ulong serialSum(System.Collections.Generic.IEnumerable<int> numbers)
    {
        ulong sum = 0;

        foreach (var number in numbers)
        {
            var numbers2 = Enumerable.Range(0, numbers2Count);
            sum += (ulong)numbers2.Sum();
        }

        return sum;
    }

    private static ulong parallelSum(System.Collections.Generic.IEnumerable<int> numbers)
    {
        ulong sum = 0;

        Parallel.ForEach(
            numbers,
            () => (ulong)0,
            (number, state, threadLocalSum) =>
            {
                return threadLocalSum + (ulong)Enumerable.Range(0, numbers2Count).Sum();
            },
            threadLocalSum =>
            {
                lock (numbers)
                {
                    sum += threadLocalSum;
                }
            });

        return sum;
    }
}



serialSum() : 83.5064ms
parallelSum() : 52.2337ms

なんとか並列処理のほうが速くなりました!
もしかしてデータ数がある程度少なく、1つあたりの処理が重いほうがParallel.ForEachは速くなるのでしょうか??(←後の実験で半分否定されました。データ数はあまり関係ないようです。)
さらなる実験が必要とされそうです。

するとこうなりました:



最初に時間がかかりまくっているのが謎ですが、それ以降はきちんと理にかなったグラフになっていますね。












拍手[1回]

PR