作为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
,那就更好了。
答案 0 :(得分:4)
您需要确保并行进程不共享任何状态。
例如,在阶乘的情况下,我会做以下事情:
这是一种简化的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
计算的结果(不打印数字)是:
答案 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);