【问题标题】:Parallel C#. Why I can't see speed up when creating numeric array并行 C#。为什么我在创建数值数组时看不到加速
【发布时间】:2015-03-13 20:26:11
【问题描述】:

最近我玩过并行循环。我从简单的任务开始,因为它正在填充一个巨大的数组。

但是,当代码不是并行时,创建时间是半秒,而当代码是并行时,创建时间是 6.03 秒(原文如此!)。

怎么会?

我认为没有比我做的更简单的任务来展示并行性的好处,即将大型任务划分为较小的任务。

谁能解释一下?

12GB RAM,i7 Extreme 980(6 核 + 6 虚拟)3.06G

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

namespace ParallelLoop
{
    class Program
    {

        static void Main(string[] args)
        {

            int Min = 0;
            int Max = 10;
            int ArrSize = 150000000;

            Stopwatch sw2 = new Stopwatch();
            Stopwatch sw3 = new Stopwatch();


            int[] test2 = new int[ArrSize];
            int[] test3 = new int[ArrSize];

            Random randNum = new Random();

            sw2.Start();
            for (int i = 0; i < test2.Length; i++)
            {
                test2[i] = i;
                //test2[i] = randNum.Next(Min, Max);
            }
            sw2.Stop();

            Console.ReadKey();
            Console.WriteLine("Elapsed={0}", sw2.Elapsed);

            sw3.Start();

            Parallel.For(0, test3.Length, (j) =>
                {
                    test3[j] = j;
                    //test3[j] = randNum.Next(Min, Max);
                }
                );

            sw3.Stop();

            Console.WriteLine("Elapsed={0}", sw3.Elapsed);
            Console.ReadKey();

        }
    }
}

【问题讨论】:

  • 这里只是猜测,但是将数组槽设置为整数是如此之快,以至于为此使用线程的成本可能不仅仅是在单个循环中设置所有槽。设置和切换线程非常非常昂贵,因此如果任务非常简单,则不值得将其拆分。如果单个任务很复杂并且您有许多相同的任务,那么使用线程的成本通常与任务成本相比相形见绌。
  • 尝试更复杂的东西,可能是编译器正在使用仅在前者中检测到的重手优化。例如,将您的负载转换为可以更快的矢量化负载。
  • 只有在实际工作时才应该使用并行。在这里,您的问题很简单,一个内核可以轻松地最大化您的内存带宽。在这种情况下,添加并行性只会减慢速度。

标签: c# parallel-processing


【解决方案1】:

虽然其他答案确实有道理,但它们没有提供正确的解决方案。您可以使用线程来提高性能,但您必须以正确的方式进行。在您的情况下,您只需将整个阵列分成 N 个块(其中 N 是您拥有的核心数量),并让每个线程在它自己的块中工作,而不会触及任何其他块。这样一来,他们就不用担心互相阻挡了。

同时注意警告。 Random 不是线程保存的,所以你应该确保每个线程都有它自己的实例。这将减少随机性,但它是并行使用它的唯一方法。

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

namespace ParallelLoop
{
    class Program
    {

        static void Main(string[] args)
        {

            int Min = 0;
            int Max = 10;
            int ArrSize = 150000000;

            Stopwatch sw2 = new Stopwatch();
            Stopwatch sw3 = new Stopwatch();
            Stopwatch sw4 = new Stopwatch();

            int[] test2 = new int[ArrSize];
            int[] test3 = new int[ArrSize];
            int[] test4 = new int[ArrSize];

            Random randNum = new Random();

            sw2.Start();
            for (int i = 0; i < test2.Length; i++)
            {
                test2[i] = i;
                //test2[i] = randNum.Next(Min, Max);
            }
            sw2.Stop();

            //Console.ReadKey();
            Console.WriteLine("Elapsed={0}", sw2.Elapsed);

            sw3.Start();

            Parallel.For(0, test3.Length, (j) =>
            {
                test3[j] = j;
                //test3[j] = randNum.Next(Min, Max);
            }
                );

            sw3.Stop();

            Console.WriteLine("Elapsed={0}", sw3.Elapsed);

            sw4.Start();

            int numberOfCores = 4;

            int itemsPerCore = ArrSize / numberOfCores;

            for (int i = 0; i < numberOfCores; i++)
            {
                int x = i; // for lambda closure
                var thread = new Thread(new ThreadStart(() =>
                {
                    int from = itemsPerCore * x;
                    int to = itemsPerCore * (x + 1);
                    for (int j = from; j < to; j++)
                    {
                        test4[j] = j;
                        //test4[j] = randNum.Next(Min, Max);                        
                    }
                }));

                thread.Start();
            }

            sw4.Stop();

            Console.WriteLine("Elapsed={0}", sw4.Elapsed);

            Console.ReadKey();
        }
    }
}

【讨论】:

    【解决方案2】:

    我决定将我的评论作为答案。我实际上并没有运行问题中包含的示例代码,但您在Parallel 循环中的任务非常简单:将数组槽设置为整数值是 CPU 可以做的最简单的事情,它做到了 非常非常快。

    与此相比,创建和切换线程以拆分初始化循环的成本是巨大的:线程切换可能需要数万个 CPU 周期,并且您拥有的线程越多,必须进行切换以保持它们运行。

    因此,在您的示例中,线程切换代码可能会占用您从拆分原本很长的循环中获得的任何可能收益。如果您尝试在循环中执行更复杂的操作,使用Parallel 循环会获得更多收益,因为线程切换的成本(仍然很大)与单个循环迭代的成本相比相形见绌。

    Joe Duffy 有几篇文章提到了上下文切换的成本 - here's one worth reading - 他提到执行上下文切换的成本在 4,000+ 到 10,000+ CPU 周期之间。

    【讨论】:

      【解决方案3】:

      正如 xxbbcc 所指出的,可能是上下文切换比设置简单数组值花费的时间更长。您可以通过休眠线程来模拟长时间运行的操作,以更好地了解性能提升:

      [TestMethod]
      public void One()
      {
          int Min = 0;
          int Max = 10;
          int ArrSize = 1500;
      
          Stopwatch sw2 = new Stopwatch();
          Stopwatch sw3 = new Stopwatch();
      
      
          int[] test2 = new int[ArrSize];
          int[] test3 = new int[ArrSize];
      
          Random randNum = new Random();
      
          sw2.Start();
          for (int i = 0; i < test2.Length; i++)
          {
              test2[i] = i;
              Thread.Sleep(10);
              //test2[i] = randNum.Next(Min, Max);
          }
          sw2.Stop();
      
          Console.WriteLine("Elapsed={0}", sw2.Elapsed);
      
          sw3.Start();
      
          Parallel.For(0, test3.Length, (j) =>
          {
              test3[j] = j;
              Thread.Sleep(10);
              //test3[j] = randNum.Next(Min, Max);
          }
              );
      
          sw3.Stop();
      
          Console.WriteLine("Elapsed={0}", sw3.Elapsed);
      }
      

      产生输出:

      Elapsed=00:00:16.4813668
      Elapsed=00:00:00.7327932
      

      【讨论】:

        【解决方案4】:

        在简单循环和使用简单并行性之间,我没有得到与您一样的巨大差异(在 i7 920 上,名义上为 2.66 GHz,6 GB RAM - 因此以下代码中的数组大小较小)。

        正如 Euphoric 指出的那样,您需要对工作进行分区 - Parallel.ForEach 过载,需要 RangePartitioner 为您执行此操作,在我的测试中,它在一定程度上提高了速度:

        using System;
        using System.Collections.Concurrent;
        using System.Collections.Generic;
        using System.Diagnostics;
        using System.Threading.Tasks;
        
        namespace ConsoleApplication1
        {
            class Program
            {
                static void Main(string[] args)
                {
                    int ArrSize = 100000000;
        
                    Stopwatch sw2 = new Stopwatch();
        
                    int[] test2 = new int[ArrSize];
                    int[] test3 = new int[ArrSize];
                    int[] test4 = new int[ArrSize];
        
                    Random randNum = new Random();
        
                    sw2.Start();
        
                    for (int i = 0; i < test2.Length; i++)
                    {
                        test2[i] = i;
                    }
        
                    sw2.Stop();
        
                    Console.WriteLine("Linear elapsed:          {0}", sw2.Elapsed);
        
                    sw2.Restart();
        
                    Parallel.For(0, test3.Length, (j) =>
                    {
                        test3[j] = j;
                    }
                        );
        
                    sw2.Stop();
        
                    Console.WriteLine("Simple parallel elapsed: {0}", sw2.Elapsed);
        
                    sw2.Restart();
        
                    var rangePartitioner = Partitioner.Create(0, test4.Length);
                    Parallel.ForEach(rangePartitioner, (range, loopState) =>
                    {
                        for (int j = range.Item1; j < range.Item2; j++)
                        {
                            test4[j] = j;
                        }
                    });
        
                    sw2.Stop();
        
                    Console.WriteLine("Partitioned elapsed:     {0}", sw2.Elapsed);
        
                    Console.ReadLine();
        
                }
            }
        }
        

        示例结果:

        线性经过:00:00:00.2312487
        简单并行经过:00:00:00.3735587
        已分区:00:00:00.1239631

        我为 x64 编译并在发布模式下运行,而不是调试,因为这才是最重要的。

        您还需要考虑处理器的缓存。 Cache-Friendly Code: Solving Manycore's Need for Faster Data Access 有一篇有趣的文章。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-01-14
          • 2022-01-10
          • 1970-01-01
          相关资源
          最近更新 更多