执行 Parallel.For 从数组计算数据的正确方法

Ped*_*o77 2 c# parallel-processing locking .net-4.0

想要:对 x 求和并对 x*x 求和。其中 x = 行[i]。因为多个线程想要读/写“sumAll”和“sumAllQ”,所以我需要锁定其访问权限。问题是锁类型在这里序列化了事物。我需要将此操作拆分为 #"Environment.ProcessorCount" for 循环,每个循环对数组的一部分求和,最后对它们的结果求和。但我怎样才能以编程方式实现呢?

示例代码:

//line is a float[]
Parallel.For(0, line.Length,
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
i =>
{
    x = (double)line[i];
    lock (sumLocker)
    {
        sumAll += x;
        sumAllQ += x * x;
    }
});
Run Code Online (Sandbox Code Playgroud)

编辑 1:Matthew Watson 回答基准结果

在家里。CPU 核心 2 四核 Q9550 @ 2.83 GHz:

Result via Linq:      SumAll=49999950000, SumAllQ=3,33332833333439E+15
Result via loop:      SumAll=49999950000, SumAllQ=3,33332833333439E+15
Result via partition: SumAll=49999950000, SumAllQ=3,333328333335E+15
Via Linq took: 00:00:02.6983044
Via Loop took: 00:00:00.4811901
Via Partition took: 00:00:00.1595113
Run Code Online (Sandbox Code Playgroud)

在上班。CPU i7 930 2.8 GHz:

Result via Linq:      SumAll=49999950000, SumAllQ=3,33332833333439E+15
Result via loop:      SumAll=49999950000, SumAllQ=3,33332833333439E+15
Result via partition: SumAll=49999950000, SumAllQ=3,333328333335E+15
Via Linq took: 00:00:01.5728736
Via Loop took: 00:00:00.3436929
Via Partition took: 00:00:00.0934209
Run Code Online (Sandbox Code Playgroud)

Mat*_*son 5

vcjones 想知道您是否真的会看到任何加速。答案是:这可能取决于您有多少个核心。PLinq 比我的家用 PC(四核)上的普通循环慢。

我提出了一种替代方法,它使用 aPartitioner将数字列表分成几个部分,以便您可以分别将每个部分相加。这里还有一些有关使用分区程序的更多信息

使用该Partitioner方法似乎更快一些,至少在我的家用电脑上是这样。

这是我的测试程序。请注意,您必须在任何调试器之外运行此版本的发布版本才能获得正确的计时。

这段代码中重要的方法是ViaPartition()

Result ViaPartition(double[] numbers)
{
    var result = new Result();

    var rangePartitioner = Partitioner.Create(0, numbers.Length);

    Parallel.ForEach(rangePartitioner, (range, loopState) =>
    {
        var subtotal = new Result();

        for (int i = range.Item1; i < range.Item2; i++)
        {
            double n = numbers[i];
            subtotal.SumAll  += n;
            subtotal.SumAllQ += n*n;
        }

        lock (result)
        {
            result.SumAll  += subtotal.SumAll;
            result.SumAllQ += subtotal.SumAllQ;
        }
    });

    return result;
}
Run Code Online (Sandbox Code Playgroud)

当我运行完整的测试程序时,我的结果(如下所示)是:

Result via Linq:      SumAll=49999950000, SumAllQ=3.33332833333439E+15
Result via loop:      SumAll=49999950000, SumAllQ=3.33332833333439E+15
Result via partition: SumAll=49999950000, SumAllQ=3.333328333335E+15
Via Linq took: 00:00:01.1994524
Via Loop took: 00:00:00.2357107
Via Partition took: 00:00:00.0756707
Run Code Online (Sandbox Code Playgroud)

(请注意由于舍入误差而产生的细微差异。)

看看其他系统的结果会很有趣。

这是完整的测试程序:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace Demo
{
    public class Result
    {
        public double SumAll;
        public double SumAllQ;

        public override string ToString()
        {
            return string.Format("SumAll={0}, SumAllQ={1}", SumAll, SumAllQ);
        }
    }

    class Program
    {
        void run()
        {
            var numbers = Enumerable.Range(0, 1000000).Select(n => n/10.0).ToArray();

            // Prove that the calculation is correct.
            Console.WriteLine("Result via Linq:      " + ViaLinq(numbers));
            Console.WriteLine("Result via loop:      " + ViaLoop(numbers));
            Console.WriteLine("Result via partition: " + ViaPartition(numbers));

            int count = 100;

            TimeViaLinq(numbers, count);
            TimeViaLoop(numbers, count);
            TimeViaPartition(numbers, count);
        }

        void TimeViaLinq(double[] numbers, int count)
        {
            var sw = Stopwatch.StartNew();

            for (int i = 0; i < count; ++i)
                ViaLinq(numbers);

            Console.WriteLine("Via Linq took: " + sw.Elapsed);
        }

        void TimeViaLoop(double[] numbers, int count)
        {
            var sw = Stopwatch.StartNew();

            for (int i = 0; i < count; ++i)
                ViaLoop(numbers);

            Console.WriteLine("Via Loop took: " + sw.Elapsed);
        }

        void TimeViaPartition(double[] numbers, int count)
        {
            var sw = Stopwatch.StartNew();

            for (int i = 0; i < count; ++i)
                ViaPartition(numbers);

            Console.WriteLine("Via Partition took: " + sw.Elapsed);
        }

        Result ViaLinq(double[] numbers)
        {
            return numbers.AsParallel().Aggregate(new Result(), (input, value) => new Result
            {
                SumAll  = input.SumAll+value,
                SumAllQ = input.SumAllQ+value*value
            });
        }

        Result ViaLoop(double[] numbers)
        {
            var result = new Result();

            for (int i = 0; i < numbers.Length; ++i)
            {
                double n = numbers[i];
                result.SumAll  += n;
                result.SumAllQ += n*n;
            }

            return result;
        }

        Result ViaPartition(double[] numbers)
        {
            var result = new Result();

            var rangePartitioner = Partitioner.Create(0, numbers.Length);

            Parallel.ForEach(rangePartitioner, (range, loopState) =>
            {
                var subtotal = new Result();

                for (int i = range.Item1; i < range.Item2; i++)
                {
                    double n = numbers[i];
                    subtotal.SumAll  += n;
                    subtotal.SumAllQ += n*n;
                }

                lock (result)
                {
                    result.SumAll  += subtotal.SumAll;
                    result.SumAllQ += subtotal.SumAllQ;
                }
            });

            return result;
        }

        static void Main()
        {
            new Program().run();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)