通过单个并行for循环获得Min,Max,Sum

时间:2017-02-23 14:54:15

标签: c# parallel-processing task-parallel-library

我试图从大型数组获得最小值,最大值和总和(平均值)。我很乐意用parallel.for替换我的常规循环。

UInt16 tempMin = (UInt16)(Math.Pow(2,mfvm.cameras[openCamIndex].bitDepth) - 1);
UInt16 tempMax = 0;
UInt64 tempSum = 0;

for (int i = 0; i < acquisition.frameDataShorts.Length; i++)
{
    if (acquisition.frameDataShorts[i] < tempMin)
        tempMin = acquisition.frameDataShorts[i];

    if (acquisition.frameDataShorts[i] > tempMax)
        tempMax = acquisition.frameDataShorts[i];

    tempSum += acquisition.frameDataShorts[i];
}

我知道如何使用任务自行切割数组来解决这个问题。但是,我很想学习如何使用parallel.for。因为据我所知,它应该能够非常优雅地做到这一点。

我发现this tutorial from MSDN用于计算Sum,但是我不知道如何扩展它以在一个段落中完成所有三件事(min,max和sum)。

结果: 好的,我尝试过PLINQ解决方案,我看到了一些重大改进。 3次传球(Min,Max,Sum)在我的i7(2x4核心)上比连续的aproach快4倍。但是我在Xeon(2x8核心)上尝试了相同的代码,结果完全不同。并行(再次3遍)实际上是顺序aproach的两倍(这比我的i7快5倍)。

最后,我自己将数组与Task Factory分开,我在所有计算机上的结果都略胜一筹。

3 个答案:

答案 0 :(得分:1)

我认为parallel.for不适合这里,但试试这个:

public class MyArrayHandler {

    public async Task GetMinMaxSum() {
        var myArray = Enumerable.Range(0, 1000);

        var maxTask = Task.Run(() => myArray.Max());
        var minTask = Task.Run(() => myArray.Min());
        var sumTask = Task.Run(() => myArray.Sum());

        var results = await Task.WhenAll(maxTask,
                                         minTask,
                                         sumTask);
        var max = results[0];
        var min = results[1];
        var sum = results[2];
    }
}

修改 由于有关性能的评论,我只是为了好玩,我做了几次测量。另外,找到了Fastest way to find sum

@ 10,000,000值

GetMinMax:218ms

GetMinMaxAsync:308ms

public class MinMaxSumTests {

    [Test]
    public async Task GetMinMaxSumAsync() {            
        var myArray = Enumerable.Range(0, 10000000).Select(x => (long)x).ToArray();
        var sw = new Stopwatch();
        sw.Start();

        var maxTask = Task.Run(() => myArray.Max());
        var minTask = Task.Run(() => myArray.Min());
        var sumTask = Task.Run(() => myArray.Sum());

        var results = await Task.WhenAll(maxTask,
                                         minTask,
                                         sumTask);
        var max = results[0];
        var min = results[1];
        var sum = results[2];
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds);
    }

    [Test]
    public void GetMinMaxSum() {            
        var myArray = Enumerable.Range(0, 10000000).Select(x => (long)x).ToArray();
        var sw = new Stopwatch();
        sw.Start();

        long tempMin = 0;
        long tempMax = 0;
        long tempSum = 0;

        for (int i = 0; i < myArray.Length; i++) {
            if (myArray[i] < tempMin)
                tempMin = myArray[i];

            if (myArray[i] > tempMax)
                tempMax = myArray[i];

            tempSum += myArray[i];
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds);
    }
}

答案 1 :(得分:1)

我认为这里的主要问题是每次迭代都要记住三个不同的变量。您可以将Tuple用于此目的:

var lockObject = new object();
var arr = Enumerable.Range(0, 1000000).ToArray();
long total = 0;
var min = arr[0];
var max = arr[0];

Parallel.For(0, arr.Length,
    () => new Tuple<long, int, int>(0, arr[0], arr[0]),
    (i, loop, temp) => new Tuple<long, int, int>(temp.Item1 + arr[i], Math.Min(temp.Item2, arr[i]),
        Math.Max(temp.Item3, arr[i])),
    x =>
    {
        lock (lockObject)
        {
            total += x.Item1;
            min = Math.Min(min, x.Item2);
            max = Math.Max(max, x.Item3);
        }
    }
);

但是,我必须警告你,这个实现比你在问题中演示的简单for循环方法慢了大约10倍(在我的机器上),所以请谨慎行事。

答案 2 :(得分:1)

请勿重新发明轮子,MinMax Sum,类似的操作是聚合。从.NET v3.5开始,您就拥有了LINQ扩展方法的便捷版本,这些方法已经为您提供了解决方案:

using System.Linq;

var sequence = Enumerable.Range(0, 10).Select(s => (uint)s).ToList();

Console.WriteLine(sequence.Sum(s => (double)s));
Console.WriteLine(sequence.Max());
Console.WriteLine(sequence.Min());

虽然它们被声明为IEnumerable的扩展名,但它们对IListArray类型有一些内部改进,因此您应衡量代码的方式将在IEnumerable上对这些类型进行处理。

在你的情况下,这还不够,因为你显然不想多次迭代其他一个数组,所以魔术就在这里:PLINQ(a.k.a。Parallel-LINQ)。您只需要添加一个方法来并行聚合数组:

var sequence = Enumerable.Range(0, 10000000).Select(s => (uint)s).AsParallel();

Console.WriteLine(sequence.Sum(s => (double)s));
Console.WriteLine(sequence.Max());
Console.WriteLine(sequence.Min());

此选项为项目的同步添加了一些开销,但它可以很好地扩展,为小型和大型枚举提供类似的时间。来自MSDN:

  每当您需要将并行聚合模式应用于.NET应用程序时,

PLINQ通常是推荐的方法。它的声明性使其比其他方法更不容易出错,并且它在多核计算机上的性能与它们相比具有竞争力。

     

使用PLINQ实现并行聚合并不需要在代码中添加锁。相反,所有同步都在PLINQ内部发生。

但是,如果您仍想调查不同类型操作的性能,可以使用Parallel.ForParallel.ForaEach方法重载一些聚合方法,如下所示:

double[] sequence = ...
object lockObject = new object();
double sum = 0.0d;

Parallel.ForEach(
    // The values to be aggregated 
    sequence,

    // The local initial partial result
    () => 0.0d,

    // The loop body
    (x, loopState, partialResult) =>
    {
        return Normalize(x) + partialResult;
    },

    // The final step of each local context            
    (localPartialSum) =>
    {
        // Enforce serial access to single, shared result
        lock (lockObject)
        {
            sum += localPartialSum;
        }
    }
);
return sum;

如果您需要为数据添加其他分区,可以使用Partitioner方法:

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

Parallel.ForEach(
    // The input intervals
    rangePartitioner, 
    // same code here);

同样Aggregate方法可用于PLINQ,具有一些合并逻辑
(再次来自MSDN的插图):

enter image description here

有用的链接: