BigInteger阶乘的并行计算

时间:2013-09-20 07:32:43

标签: c# .net-4.0 parallel-processing biginteger factorial

作为BigDecimal库的一部分,我需要计算任何给定非负整数的阶乘。所以我使用.Net 4.0的System.Numerics.BigInteger来存储大量的数字。这是我正在使用的功能:

private BigInteger Factorial(BigInteger x)
{
     BigInteger res = x;
     x--;
     while (x > 1)
     {
          res *= x;
          x--;
     }
     return res;
}

它工作但没有优化。现在我想使用并行计算,所以这是我尝试过的:(我没有并行编程的经验)

public BigInteger Factorial(long x)
{
     BigInteger res = 1;
     ParallelLoopResult r = Parallel.For(2L, (x + 1), i =>
          res *= i
     );
     return res;
}

奇怪的问题是上面的函数适用于像5这样的小数字!但不适用于像1000这样的大数字!每次都会返回完全不同的结果。所以我意识到它不是线程安全的,问题在于变量res。我想知道正确的实施是什么? 如果我可以使用BigInteger代替变量x,那就更好了。

3 个答案:

答案 0 :(得分:4)

您需要确保并行进程不共享任何状态。

例如,在阶乘的情况下,我会做以下事情:

  • 设置并行度(DOP) - 通常是计算机上用于计算绑定操作的处理器数量
  • 将问题划分为独立子集
  • 并行处理每个子集
  • 汇总从并行流程中获得的结果

这是一种简化的Map-Reduce

问题在于将一组数字相乘。将此集划分为子集的一种方法是使用N并行for循环,其中每个循环的值为i(其中0 < i <= N),步长为N(和{ {1}} = N)。

这是执行此操作的代码:

DOP

在我的机器上,这会在大约30秒内计算/// <summary> /// The max number of parallel tasks /// </summary> static readonly int DegreeOfParallelism = Environment.ProcessorCount; public BigInteger Factorial(long x) { // Make as many parallel tasks as our DOP // And make them operate on separate subsets of data var parallelTasks = Enumerable.Range(1, DegreeOfParallelism) .Select(i => Task.Factory.StartNew(() => Multiply(x, i), TaskCreationOptions.LongRunning)) .ToArray(); // after all tasks are done... Task.WaitAll(parallelTasks); // ... take the partial results and multiply them together BigInteger finalResult = 1; foreach (var partialResult in parallelTasks.Select(t => t.Result)) { finalResult *= partialResult; } return finalResult; } /// <summary> /// Multiplies all the integers up to upperBound, with a step equal to DOP /// starting from a different int /// </summary> /// <param name="upperBoud"></param> /// <param name="startFrom"></param> /// <returns></returns> public BigInteger Multiply(long upperBound, int startFrom) { BigInteger result = 1; for (var i = startFrom; i <= upperBound; i += DegreeOfParallelism) result *= i; return result; } ,结果为what Wolfram Alpha says it should be

更新

在运行了一些测试之后,我发现了一些我没想到的事情:将100000!结果打印到控制台需要大约18秒(结果有100000!个数字)。

单独456574计算的结果(不打印数字)是:

  • 〜10秒并行执行
  • 〜16秒进行顺序执行

答案 1 :(得分:3)

前言

基于一些初始和非常简单的基准测试,并行版本对于非常大的因子(大于~1000!)工作得更快。对于较小的,并行处理的开销胜过其他一切,顺序版本更快。

代码

话虽如此,这就是我在LINQPad中的工作方式:

public static class Math
{
    // Sequential execution
    public static System.Numerics.BigInteger Factorial(System.Numerics.BigInteger x)
    {
        System.Numerics.BigInteger res = x;
        x--;
        while (x > 1)
        {
            res *= x;
            x--;
        }
        return res;
    }

    public static System.Numerics.BigInteger FactorialPar(System.Numerics.BigInteger x)
    {
        return NextBigInt().TakeWhile(i => i <= x).AsParallel().Aggregate((acc, item) => acc * item);
    }

    public static IEnumerable<System.Numerics.BigInteger> NextBigInt()
    {
        System.Numerics.BigInteger x = 0;
        while(true)
        {
            yield return (++x);
        }
    }
}

适用于小型(5!= 120,6!= 720)和大型(~8000!)因子。正如我所提到的那样,对于大型阶乘,速度会提高(2 - 3倍),但对于小型阶段会有严重的性能损失(最多两个数量级)(在LINQPad中进行预热后的结果) :

  
      
  • 6! x 20 - &gt; Serial avg ticks / std dev:4.2 / 2.014,Paralell avg ticks / std dev:102.6 / 39.599(并行执行速度慢25倍......)

  •   
  • 300! x 20 - &gt; Serial avg ticks / std dev:104.35,parallel avg ticks / std dev:405.55 / 175.44(并行运行顺序为1/4,速度为快)

  •   
  • 1000! x 20-> Serial avg ticks / std dev:2672.05 / 615.744,parallel avg ticks / std dev:3778.65 / 3197.308(并行运行在序列速度的~70 - 90%)

  •   
  • 10000! x 20 - &gt; Serial avg ticks / std dev:286774.95 / 13666.607,parallel avg ticks / std dev:144932.25 / 16671.931(parallel is 2x fast)
  •   

拿那些含盐的人来说,你需要编译一个发布版本并将其作为一个独立的版本运行以获得“真正的”结果,但是有一个值得考虑的趋势。

100000! (使用打印和所有内容)在我的机器上花了26秒,在LINQPad中并行执行。

答案 2 :(得分:2)

尝试使用更简单的解决方案:

Func<int, BigInteger> factorialAgg = n => n < 2 ? BigInteger.One 
                      : Enumerable.Range(2, n-1)
                        .AsParallel()
                        .Aggregate(BigInteger.One, (r, i) => r * i);

var result = factorialAgg(100000);