Parallel.ForEach比正常foreach慢

时间:2016-09-16 21:09:37

标签: c# .net parallel-processing parallel.foreach

我在C#控制台应用程序中使用Parallel.ForEach,但似乎无法正确使用它。我创建了一个包含随机数的数组,我有一个连续的foreach和一个找到数组中最大值的Parallel.ForEach。使用c ++中大致相同的代码,我开始看到在数组中使用3M值的多个线程的权衡。但即使在100M值下,Parallel.ForEach也是两倍慢。我做错了什么?

class Program
{
    static void Main(string[] args)
    {
        dostuff();

    }

    static void dostuff() {
        Console.WriteLine("How large do you want the array to be?");
        int size = int.Parse(Console.ReadLine());

        int[] arr = new int[size];
        Random rand = new Random();
        for (int i = 0; i < size; i++)
        {
            arr[i] = rand.Next(0, int.MaxValue);
        }

        var watchSeq = System.Diagnostics.Stopwatch.StartNew();
        var largestSeq = FindLargestSequentially(arr);
        watchSeq.Stop();
        var elapsedSeq = watchSeq.ElapsedMilliseconds;
        Console.WriteLine("Finished sequential in: " + elapsedSeq + "ms. Largest = " + largestSeq);

        var watchPar = System.Diagnostics.Stopwatch.StartNew();
        var largestPar = FindLargestParallel(arr);
        watchPar.Stop();
        var elapsedPar = watchPar.ElapsedMilliseconds;
        Console.WriteLine("Finished parallel in: " + elapsedPar + "ms Largest = " + largestPar);

        dostuff();
    }

    static int FindLargestSequentially(int[] arr) {
        int largest = arr[0];
        foreach (int i in arr) {
            if (largest < i) {
                largest = i;
            }
        }
        return largest;
    }

    static int FindLargestParallel(int[] arr) {
        int largest = arr[0];
        Parallel.ForEach<int, int>(arr, () => 0, (i, loop, subtotal) =>
        {
            if (i > subtotal)
                subtotal = i;
            return subtotal;
        },
        (finalResult) => {
            Console.WriteLine("Thread finished with result: " + finalResult);
            if (largest < finalResult) largest = finalResult;
        }
        );
        return largest;
    }
}

5 个答案:

答案 0 :(得分:6)

有一个非常小的代表团体的性能影响。

我们可以使用分区实现更好的性能。在这种情况下,正文委托执行高数据量的工作。

static int FindLargestParallelRange(int[] arr)
{
    object locker = new object();
    int largest = arr[0];
    Parallel.ForEach(Partitioner.Create(0, arr.Length), () => arr[0], (range, loop, subtotal) =>
    {
        for (int i = range.Item1; i < range.Item2; i++)
            if (arr[i] > subtotal)
                subtotal = arr[i];
        return subtotal;
    },
    (finalResult) =>
    {
        lock (locker)
            if (largest < finalResult)
                largest = finalResult;
    });
    return largest;
}

注意同步localFinally委托。另请注意,需要正确初始化localInit:() => arr[0]而不是() => 0

使用PLINQ进行分区:

static int FindLargestPlinqRange(int[] arr)
{
    return Partitioner.Create(0, arr.Length)
        .AsParallel()
        .Select(range =>
        {
            int largest = arr[0];
            for (int i = range.Item1; i < range.Item2; i++)
                if (arr[i] > largest)
                    largest = arr[i];
            return largest;
        })
        .Max();
}

我强烈推荐Stephen Toub的免费书籍Patterns of Parallel Programming

答案 1 :(得分:2)

正如其他回答者所提到的那样,你在这里针对每一项目所采取的行动是如此微不足道,以至于有很多其他因素最终会比你正在做的实际工作承担更多的重量。这些可能包括:

  • JIT优化
  • CPU分支预测
  • I / O(在计时器运行时输出线程结果)
  • 调用代理的费用
  • 任务管理的成本
  • 系统错误地猜测哪种线程策略是最优的
  • 内存/ cpu缓存
  • 记忆压力
  • 环境(调试)

一次运行每个方法并不是一种适合测试的方法,因为它可以使上述多个因素在一次迭代中比在另一次迭代中更重。您应该从更强大的基准策略开始。

此外,您的实施实际上是危险的错误。 The documentation具体说:

  

每个任务调用一次 localFinally 委托,以对每个任务的本地状态执行最终操作。可以在多个任务上同时调用此委托;因此,您必须同步访问任何共享变量。

您尚未同步最终代表,因此您的功能容易出现竞争条件,从而导致产生错误结果。

在大多数情况下,最好的方法是利用比我们更聪明的人所做的工作。 In my testing,以下方法似乎总体上最快:

return arr.AsParallel().Max();

答案 2 :(得分:1)

并行Foreach循环应该运行得更慢,因为使用的算法不是并行的,并且正在进行更多的工作来运行此算法。

在单线程中,为了找到最大值,我们可以将第一个数字作为最大值,并将其与数组中的每个其他数字进行比较。如果其中一个数字大于我们的第一个数字,我们交换并继续。这样我们就可以访问数组中的每个数字,总共进行N次比较。

在上面的并行循环中,算法会产生开销,因为每个操作都包含在一个带有返回值的函数调用中。因此,除了进行比较之外,它还会在调用堆栈中添加和删除这些调用。另外,由于每次调用都依赖于之前函数调用的值,因此需要按顺序运行。

在下面的Parallel For Loop中,数组被划分为由变量threadNumber确定的显式线程数。这将函数调用的开销限制为较低的数字。

注意,对于较低的值,并行循环执行较慢。但是,对于100M,经过的时间会减少。

static int FindLargestParallel(int[] arr)
{
    var answers = new ConcurrentBag<int>();
    int threadNumber = 4;

    int partitionSize = arr.Length/threadNumber;
    Parallel.For(0, /* starting number */
        threadNumber+1, /* Adding 1 to threadNumber in case array.Length not evenly divisible by threadNumber */
        i =>
        {
            if (i*partitionSize < arr.Length) /* check in case # in array is divisible by # threads */
            {
                var max = arr[i*partitionSize];
                for (var x = i*partitionSize; 
                    x < (i + 1)*partitionSize && x < arr.Length;
                    ++x)
                {
                    if (arr[x] > max)
                        max = arr[x];
                }
                answers.Add(max);
            }
        });

    /* note the shortcut in finding max in the bag */    
    return answers.Max(i=>i);
}

答案 3 :(得分:0)

这里有一些想法:在并行的情况下,涉及线程管理逻辑,确定它想要使用多少个线程。这个线程管理逻辑可能在你的主线程上运行。每次线程返回新的最大值时,管理逻辑就会启动并确定下一个工作项(要在数组中处理的下一个数字)。我很确定这需要某种锁定。在任何情况下,确定下一个项目甚至可能比执行比较操作本身花费更多。

对于我而言,这听起来比一个接一个地处理一个数字的单个线程更多的工作(开销)。在单线程的情况下,有许多优化在起作用:没有边界检查,CPU可以将数据加载到CPU内的第一级缓存中,等等。不确定,哪些优化适用于并行情况。

请记住,在典型的桌面计算机上,只有2到4个物理CPU核心可用,因此您将永远不会有更多的实际工作。因此,如果并行处理开销超过单线程操作的2-4倍,并行版本将不可避免地变慢,您正在观察。

您是否尝试在32核计算机上运行此功能? ; - )

更好的解决方案是确定覆盖整个阵列的非重叠范围(开始+停止索引),并让每个并行任务处理一个范围。这样,每个并行任务都可以在内部执行紧密的单线程循环,并且只有在处理完整个范围后才返回。您甚至可以根据计算机的逻辑核心数确定近乎最佳的范围数。我还没试过这个,但我很确定你会看到对单线程案例的改进。

答案 4 :(得分:0)

尝试将集合拆分为批次并并行运行批次,其中批次的数量对应于您的 CPU 内核数。 我使用以下方法运行了一些方程 1K、10K 和 1M 次:

  1. 一个“for”循环。
  2. 来自 System.Threading.Tasks 库的“Parallel.For”,涵盖整个集合。
  3. 跨 4 个批次的“Parallel.For”。
  4. 来自 System.Threading.Tasks 库的“Parallel.ForEach”,贯穿整个集合。
  5. 跨 4 个批次的“Parallel.ForEach”。

结果:(以秒为单位)

enter image description here

结论:
在超过 10K 记录的情况下,使用“Parallel.ForEach”并行处理批处理具有最佳结果。我相信批处理会有所帮助,因为它利用了所有 CPU 内核(在本例中为 4 个),而且还最大限度地减少了与并行化相关的线程开销。

这是我的代码:

        public void ParallelSpeedTest()
    {
        var rnd = new Random(56);
        int range = 1000000;
        int numberOfCores = 4;
        int batchSize = range / numberOfCores;
        int[] rangeIndexes = Enumerable.Range(0, range).ToArray();
        double[] inputs = rangeIndexes.Select(n => rnd.NextDouble()).ToArray();
        double[] weights = rangeIndexes.Select(n => rnd.NextDouble()).ToArray();
        double[] outputs = new double[rangeIndexes.Length];

        /// Series "for"...
        var startTimeSeries = DateTime.Now;
        for (var i = 0; i < range; i++)
        {
            outputs[i] = Math.Sqrt(Math.Pow(inputs[i] * weights[i], 2));
        }
        var durationSeries = DateTime.Now - startTimeSeries;

        /// "Parallel.For"...
        var startTimeParallel = DateTime.Now;
        Parallel.For(0, range, (i) => {
            outputs[i] = Math.Sqrt(Math.Pow(inputs[i] * weights[i], 2));
        });
        var durationParallelFor = DateTime.Now - startTimeParallel;

        /// "Parallel.For" in Batches...
        var startTimeParallel2 = DateTime.Now;
        Parallel.For(0, numberOfCores, (c) => {
            var endValue = (c == numberOfCores - 1) ? range : (c + 1) * batchSize;
            var startValue = c * batchSize;
            for (var i = startValue; i < endValue; i++)
            {
                outputs[i] = Math.Sqrt(Math.Pow(inputs[i] * weights[i], 2));
            }
        });
        var durationParallelForBatches = DateTime.Now - startTimeParallel2;

        /// "Parallel.ForEach"...
        var startTimeParallelForEach = DateTime.Now;
        Parallel.ForEach(rangeIndexes, (i) => {
            outputs[i] = Math.Sqrt(Math.Pow(inputs[i] * weights[i], 2));
        });
        var durationParallelForEach = DateTime.Now - startTimeParallelForEach;

        /// Parallel.ForEach in Batches...
        List<Tuple<int,int>> ranges = new List<Tuple<int, int>>();
        for (var i = 0; i < numberOfCores; i++)
        {
            int start = i * batchSize;
            int end = (i == numberOfCores - 1) ? range : (i + 1) * batchSize;
            ranges.Add(new Tuple<int,int>(start, end));
        }
        var startTimeParallelBatches = DateTime.Now;
        Parallel.ForEach(ranges, (range) => {
            for(var i = range.Item1; i < range.Item1; i++) {
                outputs[i] = Math.Sqrt(Math.Pow(inputs[i] * weights[i], 2));
            }
        });
        var durationParallelForEachBatches = DateTime.Now - startTimeParallelBatches;

        Debug.Print($"=================================================================");
        Debug.Print($"Given: Set-size: {range}, number-of-batches: {numberOfCores}, batch-size: {batchSize}");
        Debug.Print($".................................................................");
        Debug.Print($"Series For:                       {durationSeries}");
        Debug.Print($"Parallel For:                 {durationParallelFor}");
        Debug.Print($"Parallel For Batches:         {durationParallelForBatches}");
        Debug.Print($"Parallel ForEach:             {durationParallelForEach}");
        Debug.Print($"Parallel ForEach Batches:     {durationParallelForEachBatches}");
        Debug.Print($"");
    }