并行C#。为什么我在创建数字数组时看不到加速

时间:2015-03-13 20:26:12

标签: c# parallel-processing

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

然而,当代码不平行时,创建时间是秒的一半,而当代码WAS并行时,创建时间是6.03s(sic!)。

怎么回事?

我认为没有比这更简单的任务来显示并行性带来的好处,就像我所做的那样将巨大的任务分配给较小的任务。

有人可以解释一下吗?

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

        }
    }
}

4 个答案:

答案 0 :(得分:2)

虽然其他答案确实有效,但他们没有提供正确的解决方案。您可以使用线程来提高性能,但必须以正确的方式执行。在您的情况下,您只需将整个数组划分为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();
        }
    }
}

答案 1 :(得分:1)

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

与此相比,创建和切换线程以分割初始化循环的成本是 huge :线程切换可能有数万个CPU周期,线程越多,线程越多,越多切换必须使它们继续运行。

因此,在您的示例中,线程切换代码可能会消耗掉分割您的非常长的循环所获得的任何可能的收益。如果你尝试在循环中做一些更复杂的事情,使用Parallel循环会获得更多,因为线程切换的成本(仍然很大)会因单循环迭代的结果而相形见绌。

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

答案 2 :(得分:1)

正如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

答案 3 :(得分:1)

在一个简单的循环和使用简单的并行性之间,我没有像你那样(在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上有一篇有趣的文章。