Parallel.Invoke 运行缓慢

Parallel.Invoke works slowly

我编写了代码 Methode #1 和 Methode #2 来比较性能。方法 #1 使用构造,方法 #2 使用 Parallel.Invoke。在第二种情况下工作非常缓慢。我不明白为什么会这样?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using Mpir.NET;
using System.Runtime.Serialization;
using System.Diagnostics;

namespace ConsoleApplication4
{
    class Program
    {
        public class numbers
        {
            public numbers(mpz_t p, mpz_t q)
            {
                this.q = q;
                this.p = p;
            }
            mpz_t q;
            mpz_t p;
        };

        static void Main(string[] args)
        {
            Int32 arraySize = 400;
            ConcurrentBag<numbers> pairsCB = new ConcurrentBag<numbers>();
            ConcurrentBag<Action> cbTasks = new ConcurrentBag<Action>();
            HashSet<numbers> uniqueCB = new HashSet<numbers>(pairsCB.ToArray());

            mpz_t[] numbersArray = new mpz_t[arraySize];

            for (Int32 i = 0; i < arraySize; i++)
            {
                numbersArray[i] = i*i+i+1; 
            }


            //Methode #1 
            Stopwatch stopwatch1 = new Stopwatch();
            stopwatch1.Start();

            for (Int32 j = 0; j < arraySize; j++)
            {
                checkDivisible(numbersArray[j], pairsCB);
            }

            uniqueCB = new HashSet<numbers>(pairsCB.ToArray());
            stopwatch1.Stop();
            Console.WriteLine("Methode Count Unique Pairs  Count:{0}\tTime elapsed:{1}", uniqueCB.Count(), stopwatch1.Elapsed);

            //Methode #2 
            Stopwatch stopwatch2 = new Stopwatch();
            stopwatch2.Start();
            pairsCB = new ConcurrentBag<numbers>();

            for(Int32 j = 0; j < arraySize; j++)
            {
                mpz_t value = numbersArray[j];
                cbTasks.Add(new Action(() => checkDivisible(value, pairsCB)));
            }

            Action[] tasks =  cbTasks.ToArray();

            Parallel.Invoke(tasks);

            stopwatch2.Stop();
            Console.WriteLine("Methode Count Unique Pairs  Count:{0}\tTime elapsed:{1}", uniqueCB.Count(), stopwatch2.Elapsed);


            Console.ReadKey();
        }

        private static void checkDivisible(mpz_t n, ConcurrentBag<numbers>  pq)
        {
            mpz_t p = 1; 
            mpz_t q = 1;    

            for (Int32 i = 2; i < n; i++)
            {
                if (n % i == 0)
                {
                    q = i;
                    p = n / i;
                    pq.Add(new numbers(p, q));
                }
            }
        }
    }
}

您可以使用的替代方法:

//Methode #2 
Stopwatch stopwatch2 = new Stopwatch();
stopwatch2.Start();
pairsCB = new ConcurrentBag<numbers>();

Parallel.For(0, arraySize, (index) => 
{
     checkDivisible(numbersArray[index], pairsCB);
});

stopwatch2.Stop();

arraySize = 1000 的输出

Methode Count Unique Pairs  Count:3878  Time elapsed:00:00:01.5671572
Methode Count Unique Pairs  Count:3878  Time elapsed:00:00:00.7917211

第二种方法较慢的原因有很多。

  1. checkDivisible 没有做足够的工作来证明并行化。并行化和同步化的开销可能大于任何好处。
  2. ConcurrentBag 是一个特殊用途的集合,它将数据存储在线程本地存储中,确保线程可以快速访问它写入的项目。在其他场景下,它实际上比其他并发收集慢。
  3. 所有对 checkDivisible 的调用都写入同一个集合,这成为一个热点。最好从每次调用中 return 一个简单的数组,最后在最后一步合并所有数组。
  4. 并发调用过多。 Parallel.Invoke 必须单独安排每个操作。 Parallel.ForParallel.ForEach 另一方面知道所有调用都是相同的,因此他们可以根据处理器数量对数据进行分区,确保并行化开销最小。

第一步修改checkDivisible:

    private static List<number> checkDivisible(mpz_t n)
    {
        mpz_t p = 1; 
        mpz_t q = 1;    

        List<number> nums=new List<numbers>();

        for (Int32 i = 2; i < n; i++)
        {
            if (n % i == 0)
            {
                q = i;
                p = n / i;
                nums.Add(new numbers(p, q));
            }
        }
        return numbers;
    }

我更喜欢迭代器方法,因为它避免了创建 List 只是为了收集结果。

然后你可以使用Parallel.For :

var results=new ConcurrentQueue<IList<numbers>>();

Parallel.For(0, arraySize, (index) => 
{
     var local=checkDivisible(numbersArray[index]);
     results.Add(local);
});

var final=results.SelectMany(r=>r).ToList();

最后一步可以是任何你想要的,以确保结果是你想要的形式,例如。使用 ToDictionary 或 ToLookup 根据键合并结果。

另一种选择是使用 PLINQ 以更简洁的方式完成同样的事情。将 checkDivision 更改为迭代器:

    private static IEnumerable<number> checkDivisible(mpz_t n)
    {
        mpz_t p = 1; 
        mpz_t q = 1;    

        for (Int32 i = 2; i < n; i++)
        {
            if (n % i == 0)
            {
                q = i;
                p = n / i;
                yield return new numbers(p, q);
            }
        }
    }

你可以这样写:

var results= (from n in numbersArray.AsParallel()
             from number in checkDivisible(n)
             select n).ToList();

和Parallel.For一样,PLINQ会根据机器的核数对numbersArray中的数据进行分区,并行处理分区,最后合并到一个列表中。