【问题标题】:Parallel always seems slower while calculating prime在计算素数时,并行似乎总是较慢
【发布时间】:2014-07-27 12:11:16
【问题描述】:

这可能并不奇怪,只是我似乎无法解释。我只是对并行编程进行了一些试验,我想我会实现可以并行的最直接的例子;计算素数。

问题是:我似乎无法让 4 个逻辑处理器比单线程计算更快。这是为什么? (我有一个 i7-4500u)

这是我的代码(您基本上可以将其粘贴到新的控制台应用程序中):

    static void Main(string[] args)
    {
        var p = new Program();
        p.Start();
    }

    private void Start()
    {
        StartMonitoringTask();

        // This puts my cpu at 33%, but is really fast.
        for (long current = 3; current < long.MaxValue; current++)
        {
            DeterminePrimeAndAddToTotal(current);
        }

        // This puts my cpu at 100%, but is way slower.
        Parallel.For(3, long.MaxValue, (current) => DeterminePrimeAndAddToTotal(current));
    }

    private long lastPrime = 0;
    private long totalFound = 0;

    private void DeterminePrimeAndAddToTotal(long primeOrNot)
    {
        bool isPrime = true;

        if (primeOrNot % 2 == 0) return; // even number? never prime.

        long root = (long)Math.Sqrt((long)primeOrNot);
        for (int i = 3; i <= root; i += 2) // check only uneven numbers.
        {
            if (primeOrNot % i == 0)
            {
                isPrime = false;
                break;
            }
        }

        if(isPrime)
        {
            totalFound++;
            lastPrime = primeOrNot;
        }
    }

    /// <summary>
    /// This just starts a task to monitor the progress.
    /// It's outputs the results to the console every second or so.
    /// </summary>
    private void StartMonitoringTask()
    {
        Task.Factory.StartNew(() =>
        {
            var sw = Stopwatch.StartNew();

            while (true)
            {
                Task.Delay(1000).Wait();

                Console.WriteLine(
                    "found: " + totalFound + 
                    ", last: " + lastPrime + 
                    ", " + (totalFound / (sw.ElapsedMilliseconds / 1000)) + " p/s");
            }
        }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
    }

更新(基于弗罗德的回答): Frode 的回答似乎是合理的,所以为了证明这一点,我在 for 和并行循环之外添加了 Parallel.Invoke 的一些操作。像这样:

        var numberOfActions = 20;

        var actions = new List<Action>();
        long chunkSize = int.MaxValue / numberOfActions;

        for (int i = 0; i < numberOfActions; i++)
        {
            long from, to;

            from = i == 0 ? 3 : (i * chunkSize);
            to = (i + 1) * chunkSize;

            actions.Add(new Action(() => { for (long j = from; j < to; j++) DeterminePrimeAndAddToTotal(j); }));
        }

        Parallel.Invoke(actions.ToArray());

这似乎和 Parallel.For 一样慢。我错过了什么?

【问题讨论】:

  • 你怎么知道它更慢...你这里有一个竞争条件,这很容易“降低”结果。
  • @ShlomiBorovitz 我要么评论 for 循环,要么评论并行循环。我在这里没有评论它们。除此之外,两个循环的条件是相同的。
  • 你的意思是注释掉......无论如何,这与竞争条件问题无关。
  • @ShlomiBorovitz 怎么样?如果你运行代码,你会得到什么结果?
  • 我正在寻找答案。在那之前,你不能指望竞争条件是确定性的 - 所以......回答这个问题有什么意义?

标签: c# parallel-processing task-parallel-library primes


【解决方案1】:

为 int64 范围内的每个整数值旋转新线程的成本非常高。

将 int64 范围拆分为 10 个块,并改为使用 Parallel.Invoke(Action[] action) 执行每个 for-chunk。

那么您肯定会看到性能提升。

    Parallel.Invoke(
        ()=> { for(int i=3;i<a;i++) DeterminePrimeAndAddToTotal(i); },
        ()=> { for(int i=a;i<b;i++) DeterminePrimeAndAddToTotal(i); },
        ()=> { for(int i=b;i<c;i++) DeterminePrimeAndAddToTotal(i); },
        ...

【讨论】:

    【解决方案2】:

    我将忽略素数检测中的错误(2 是素数,尽管它是偶数)。

    您有两个基本问题:错误的基准测试和竞争条件。

    关于基准测试:您必须在实际条件下进行基准测试(或尽可能接近)...
    在现实生活中,代码是为“发布”而不是“调试”编译的。
    在现实生活中,代码在没有附加调试器的情况下运行。

    当然,通常,当您对某些代码进行基准测试时,您不希望包含 JIT 时间或其他一次性惩罚。

    至于比赛条件,请阅读this Wiki article。有一个完美的例子来说明您的代码中发生的事情(两个线程试图同时增加一个值)。那么有两种方法可以解决这种竞争条件:

    • 互斥:防止超过 1 个线程一次访问该变量。
    • 删除共享变量:完全消除比赛的可能性(但并非总是可能的。

    我会选择第二种解决方案,因为它的惩罚更少。
    为此,我将使用 PLINQ。
    您可以测试代码,但您应该在不调试的情况下运行它。并行部分确实运行得更快(在只有 2 个内核的弱计算机上,它的运行速度是原来的两倍)。

    private const int ITERATIONS = 1000000;
        static void Main(string[] args)
        {
            var p = new Test();
            p.Start();
        }
    
        private void Start()
        {
            Console.WriteLine();
            DeterminePrimeAndAddToTotal(1);
    
            var primes = Enumerable.Range(2, ITERATIONS)
                                   .Select(num => new { Number = num, IsPrime = DeterminePrimeAndAddToTotal(num) })
                                   .Where(value => value.IsPrime);
    
            var parallelPrimes = Enumerable.Range(2, ITERATIONS)
                                           .AsParallel()
                                           .Select(num => new { Number = num, IsPrime = DeterminePrimeAndAddToTotal(num) })
                                           .Where(value => value.IsPrime);
    
            var watch = Stopwatch.StartNew();
    
            Console.WriteLine(primes.Count());
            watch.Stop();
            var nonParallelTime = watch.ElapsedMilliseconds;
    
            watch = Stopwatch.StartNew();
    
            Console.WriteLine(parallelPrimes.Count());
    
            watch.Stop();
            var parallelTime = watch.ElapsedMilliseconds;
    
            Console.WriteLine("parallel/non-parallel");
            Console.WriteLine(string.Format("{0}/{1}", parallelTime, nonParallelTime));
        }
    
        private bool DeterminePrimeAndAddToTotal(long primeOrNot)
        {
            bool isPrime = primeOrNot <= 2 || (primeOrNot % 2 != 0);
    
            long root = (long)Math.Sqrt((long)primeOrNot);
    
            for (int i = 3; i <= root && isPrime; i += 2) // check only uneven numbers.
            {
                if (primeOrNot % i == 0)
                {
                    isPrime = false;
                }
            }
    
            return isPrime;
        }
    

    虽然这仍然是一个糟糕的基准......(它只是用于测试并行性,而不是用于检查任何有价值的信息);

    【讨论】:

    • 我从 3 点开始循环,您可能已经注意到了。我暂时忽略 1 和 2。不忽略 1 和 2 会导致在方法中进行额外的(几乎是不必要的)检查。
    • @GerbenRampaart 很好...这是这里最不重要的问题。
    • 我很快就挂了。对不起。当我回来时,我会仔细阅读你的答案。暂时谢谢。
    • 我一直在运行 Release。
    • 我删除了最后找到的素数的存储,并用 Interlocked.Increment(ref totalCount) 替换了 totalFound++。我真的不明白比赛条件如何对性能产生如此大的影响。我只写值。
    猜你喜欢
    • 1970-01-01
    • 2011-11-15
    • 2019-06-07
    • 1970-01-01
    • 2019-11-19
    • 1970-01-01
    • 1970-01-01
    • 2012-05-10
    • 1970-01-01
    相关资源
    最近更新 更多