繁体   English   中英

Parallel.ForEach 比正常的 foreach 慢

[英]Parallel.ForEach slower than normal foreach

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

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

这是拥有一个非常小的代表机构的性能后果。

我们可以使用分区获得更好的性能。 在这种情况下,主体代表执行具有高数据量的工作。

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

正如其他回答者所提到的,您尝试针对此处的每个项目执行的操作是如此微不足道,以至于有多种其他因素最终会比您正在做的实际工作承担更多的重量。 这些可能包括:

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

单次运行每种方法并不是一种充分的测试方法,因为它使上述许多因素在一次迭代中的权重大于另一次。 您应该从更强大的基准测试策略开始。

此外,您的实现实际上是危险的错误。 文档特别指出:

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

您还没有同步您的最终委托,因此您的函数很容易出现竞争条件,从而导致它产生不正确的结果。

在大多数情况下,最好的方法是利用比我们更聪明的人所做的工作。 在我的测试中,以下方法似乎是最快的:

return arr.AsParallel().Max();

Parallel Foreach 循环应该运行得更慢,因为所使用的算法不是并行的,并且需要做更多的工作来运行该算法。

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

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

在下面的并行 For 循环中,数组被划分为由变量 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);
}

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

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

请记住,在典型的台式机上,只有 2 到 4 个物理 CPU 内核可用,因此您将永远不会有更多的实际工作。 所以如果并行处理开销是单线程操作的2-4倍以上,那么并行版本难免会变慢,这是你观察到的。

您是否尝试在 32 核机器上运行它? ;-)

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

尝试将集合拆分为批次并并行运行批次,其中批次的数量对应于您的 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”。

结果:(以秒为单位)

在此处输入图片说明

结论:
在超过 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($"");
    }

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM