在计算素数时,并行似乎总是较慢

时间:2014-07-27 12:11:17

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

这可能并不奇怪,这只是我无法解释的东西。我只是尝试了一些并行编程,我想我会实现一个可以并行的最简单的例子;计算素数。

问题是:我似乎无法让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的回答): 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一样慢。但是。我错过了什么?

2 个答案:

答案 0 :(得分:0)

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

将int64-range拆分为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); },
        ...

答案 1 :(得分:0)

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

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

关于替补标记:你必须在现实条件下进行基准测试(或者你可以达到最接近的标准)...... 在现实生活中,代码编译为&#34;发布&#34;而不是&#34;调试&#34;。
在现实生活中,代码运行时没有连接调试器。

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

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

  • 互斥:防止一次有多个线程访问变量。
  • 删除共享变量:完全取消竞赛的可能性(但并非总是可行。

我将采用第二种解决方案,因为它的惩罚较少 为实现这一目标,我将使用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;
    }

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