【问题标题】:Right way to do a Parallel.For to compute data from Array执行 Parallel.For 从 Array 计算数据的正确方法
【发布时间】:2013-05-29 18:33:58
【问题描述】:

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

示例代码:

//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;
    }
});

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

在家里。 CPU Core 2 Quad 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

在工作中。 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

【问题讨论】:

  • 使用允许您使用累加器的重载之一。
  • line.AsParallel().Aggregate ?
  • 你要的是ParallelEnumerable.Aggregate (msdn.microsoft.com/en-us/library/dd383667.aspx)
  • 不过,在这种情况下,您可以使用ParallelEnumerable.Sum。
  • 不是我不能使用 sum 因为我也需要对 x*x 求和。

标签: c# .net-4.0 parallel-processing locking


【解决方案1】:

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

我想出了一种替代方法,它使用Partitioner 将数字列表分成几个部分,这样您就可以分别添加每个部分。还有一些information about using a Partitioner here。

使用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;
}

我运行完整测试程序时的结果(显示在这些结果下方)是:

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

(注意由于舍入误差导致的细微差异。)

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

这是完整的测试程序:

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();
        }
    }
}

【讨论】:

  • 因此,AsParallel().Aggregate 在这种情况下速度较慢!为什么??
  • @Pedro77 因为创建额外线程、管理它们之间的工作、在需要时同步它们等的开销比你从能够并行执行一些代码中获得的成本更高.这部分是由于他的机器上没有很多内核,可能没有足够的工作来抵消开销,并且有一个本质上不太适合并行化的任务(每个操作不能完全独立完成;他们需要相互交互)。
  • @Pedro77 我现在更新了我的答案。如果您真的想要最大速度,我找到了一种更快的方法(嗯,在 my PC 上更快)。
  • @Pedro77: Aggregate 速度较慢,因为方法调用开销很大。创建和填充所有这些新对象也无济于事。
  • @Matthew Watson,非常感谢您提供详细的基准测试。我会尽快在我的家庭和工作电脑上尝试它,然后在这里发布结果。关于开销,请看这里:stackoverflow.com/questions/473782/inline-functions-in-c :)
【解决方案2】:

按照 cmets 中的建议,您可以在 LINQ 中使用 Aggregate 和 AsParallel 来完成此操作。例如:

using System.Linq;

//A class to hold the results.
//This can be improved by making it immutable and using a constructor.
public class Result
{
    public double SumAll { get; set; }
    public double SumAllQ { get; set; }
}

你可以像这样使用 LINQ:

var result = line.AsParallel().Aggregate(new Result(), (input, value) => new Result {SumAll = input.SumAll+value, SumAllQ = input.SumAllQ+value*value});

甚至更好:

var pline = line.AsParallel().WithDegreeOfParallelism(Environment.ProcessorCount);
var result = new Result { SumAll = pline.Sum(), SumAllQ = pline.Sum(x => x * x) };

AsParallel 无法让您直接指定选项,但您可以使用.WithDegreeOfParallelism()、.WithExecutionMode() 或.WithMergeOptions() 来获得更多控制权。您可能必须使用 WithDegreeOfParallelism 才能让它与多个线程一起运行。

【讨论】:

  • 顺便说一句,我不确定这种操作是否真的会从并行执行中受益。您是否进行过任何基准测试以查看并行执行是否会提高性能?幕后仍在进行同步(尽管没有序列化)。
  • 感谢您的回答,我会做一些基准测试,然后在这里发布结果。
  • 您也应该尝试将Result 用作struct,看看它是否更快。
  • @vcsjones:我做了一些改进,但它仍然比简单的循环慢一个数量级。原因不是同步,而是大量的方法调用开销。
  • 那个“sux”.. 但是 .NET 4.5 现在可以选择内联方法:MethodImplOptions.AggressiveInlining。 stackoverflow.com/questions/473782/inline-functions-in-c
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-16
  • 2020-12-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多