C#
.NET
LINQ

PLINQの実行速度と高速な書き方

Parallel LINQ (PLINQ) ってどうなのとふと思ってので速度チェック。
配列全てに同じ処理をかけて値を加工します。
数字と'の違いは並列数を絞っているかどうか(8コア環境なので8に設定)。
1と2は並列毎にオブジェクト作成 vs オブジェクトを共有の違いです。

コード

using System;
using System.Linq;

namespace PlayGround
{
    class Data
    {
        public int Value { get; set; }
    }

    class Processor
    {
        public Data Do(Data data)
        {
            // prosess
            data.Value = 1;

            // and do something
            System.Threading.Thread.Sleep(1);

            return data;
        }
    }

    class Program
    {

        static void Main(string[] args)
        {
            const int N = 5000;
            var list = Enumerable.Range(1, N).Select(x => new Data() { Value = 1 }).ToArray();

            var sw = new System.Diagnostics.Stopwatch();

            // for
            sw.Restart();
            for (var i = 0; i < list.Length; i++)
            {
                list[i] = new Processor().Do(list[i]);
            }
            Console.WriteLine("for      : " + sw.Elapsed);

            // LINQ
            sw.Restart();
            list = list.Select(x => new Processor().Do(x)).ToArray();
            Console.WriteLine("LINQ     : " + sw.Elapsed);

            // PLINQ(1)
            sw.Restart();
            var p = new Processor();
            list.AsParallel().ForAll(x => x = p.Do(x));
            Console.WriteLine("PLINQ(1) : " + sw.Elapsed);

            // PLINQ(1')
            sw.Restart();
            list.AsParallel().WithDegreeOfParallelism(8).ForAll(x => x = p.Do(x));
            Console.WriteLine("PLINQ(1'): " + sw.Elapsed);

            // PLINQ(2)
            sw.Restart();
            list.AsParallel().ForAll(x => x = new Processor().Do(x));
            Console.WriteLine("PLINQ(2) : " + sw.Elapsed);

            // PLINQ(2')
            sw.Restart();
            list.AsParallel().WithDegreeOfParallelism(8).ForAll(x => x = new Processor().Do(x));
            Console.WriteLine("PLINQ(2'): " + sw.Elapsed);

            Console.ReadKey();
        }
    }
}

出力と考察

for      : 00:00:09.5311249
LINQ     : 00:00:09.4429576
PLINQ(1) : 00:00:01.3311439
PLINQ(1'): 00:00:01.2480367
PLINQ(2) : 00:00:01.2460522
PLINQ(2'): 00:00:01.2491046
  • forとLINQは同程度の処理時間
  • PLINQ(並列)にした方が速い
  • 数字と'は今回の処理だと条件によって前後
  • 1から2だと高速に
    • オブジェクトを共有している方がコスト高い
    • ただしオブジェクトの生成処理が重いと逆転することが考えられる
  • 1'から2'だと遅くなった
    • 並列数が少ないので使い回す方がいい?
  • PLINQ内ではかなり誤差っぽい…

ということでPLINQの方が速くて並列数とオブジェクト共有はほぼ同じになりました。

考察(2)

試しにN=1千万件、Sleepなし(件数が多くタスクが軽い)でやってみました。

for      : 00:00:00.1670089
LINQ     : 00:00:00.4834421
PLINQ(1) : 00:00:00.0758994
PLINQ(1'): 00:00:00.0219155
PLINQ(2) : 00:00:00.1694893
PLINQ(2'): 00:00:00.0961720
  • LINQとPLINQ(2)が一番遅い
    • ループ回数が多くてLINQが遅いのは自然な感じ
    • PLINQ(2)は毎回オブジェクトを作るコストが効いたか
  • PLINQ(1')が一番速い
    • 1と比べるとやはり並列数が絞ってある方が並列化自体が足を引っ張らない

考察(3)

N=1000件、Sleep(10)(件数が少なくタスクが重い)でやってみました。

for      : 00:00:15.5049523
LINQ     : 00:00:15.6227760
PLINQ(1) : 00:00:02.0039157
PLINQ(1'): 00:00:01.9642426
PLINQ(2) : 00:00:01.9529492
PLINQ(2'): 00:00:01.9528858
  • 並列数が絞ってある方が気持ち速い
  • オブジェクト使いまわさない方が気持ち速い
  • とはいえ結構誤差

どれが速いのか

PLINQを使いつつ、件数が多い時はオブジェクトを共有することや並列数を絞ることは忘れないのが大体のパターンで通用しそうな感じです。
逆に共有しなかったり絞らないことで得られるメリットはあんまり無さそうな感じ。
共有はスレッドセーフかどうかも問題ですけどね。

簡単なところはこの方針で。
LINQの内容によってはまたStopwatchで調べておきたいですね。