C# 为什么通过成对执行计算来计算连续整数数组的乘积会更快?

C# 为什么通过成对执行计算来计算连续整数数组的乘积会更快?,c#,algorithm,performance,time,C#,Algorithm,Performance,Time,当我试图创建自己的阶乘函数时,我发现如果成对计算,计算速度是原来的两倍。像这样: 一组1:2*3*4。。。50000*50001=4.1秒 2人一组:(2*3)*(4*5)*(6*7)。。。(50000*50001)=2.0秒 三人一组:(2*3*4)*(5*6*7)。。。(49999*50000*50001)=4.8秒 这是我用来测试这个的c Stopwatch timer = new Stopwatch(); timer.Start(); // Seperate the calculati

当我试图创建自己的阶乘函数时,我发现如果成对计算,计算速度是原来的两倍。像这样:

一组1:2*3*4。。。50000*50001=4.1秒

2人一组:(2*3)*(4*5)*(6*7)。。。(50000*50001)=2.0秒

三人一组:(2*3*4)*(5*6*7)。。。(49999*50000*50001)=4.8秒

这是我用来测试这个的c

Stopwatch timer = new Stopwatch();
timer.Start();

// Seperate the calculation into groups of this size.
int k = 2;

BigInteger total = 1;

// Iterates from 2 to 50002, but instead of incrementing 'i' by one, it increments it 'k' times,
// and the inner loop calculates the product of 'i' to 'i+k', and multiplies 'total' by that result.
for (var i = 2; i < 50000 + 2; i += k)
{
    BigInteger partialTotal = 1;
    for (var j = 0; j < k; j++)
    {
        // Stops if it exceeds 50000.
        if (i + j >= 50000) break;
        partialTotal *= i + j;
    }
    total *= partialTotal;
}

Console.WriteLine(timer.ElapsedMilliseconds / 1000.0 + "s");
秒表计时器=新秒表();
timer.Start();
//将计算分为此大小的组。
int k=2;
BigInteger合计=1;
//从2迭代到50002,但不是将“i”增加1,而是将其增加“k”倍,
//内部循环计算“i”到“i+k”的乘积,并将“total”乘以该结果。
对于(变量i=2;i<50000+2;i+=k)
{
BigInteger partialTotal=1;
对于(var j=0;j=50000)中断;
部分总*=i+j;
}
合计*=部分合计;
}
控制台写入线(timer.elapsedmillyses/1000.0+“s”);
我在不同的层次上进行了测试,并将几个测试的平均时间放在条形图中。我希望随着我增加组数,它会变得更有效,但3组的效率最低,4组与1组相比没有改善

是什么导致了这种差异,有没有最佳的计算方法?

我认为您有一个bug(“+”而不是“*”)

部分总*=i+j

很高兴检查您是否得到了正确的答案,而不仅仅是有趣的性能指标


但我很好奇是什么促使你这么做的。如果您确实发现了差异,我希望这与寄存器和/或内存分配的优化有关。我希望它是0-30%或者类似的,而不是50%。

执行
biginger
乘法所需的时间取决于乘积的大小

两种方法的乘法次数相同,但如果成对乘以因子,则乘积的平均大小要比将每个因子与所有较小因子的乘积相乘时小得多


如果您总是将尚未相乘的两个最小因子(原始因子或中间乘积)相乘,直到得到完整的乘积,您可以做得更好。

biginger
对于31位或更少的数字有一个快速的情况。当您进行成对乘法时,这意味着采用特定的快速路径,将值乘以单个
ulong
,并更明确地设置值:

public void Mul(ref BigIntegerBuilder reg1, ref BigIntegerBuilder reg2) {
  ...
  if (reg1._iuLast == 0) {
    if (reg2._iuLast == 0)
      Set((ulong)reg1._uSmall * reg2._uSmall);
    else {
      ...
    }
  }
  else if (reg2._iuLast == 0) {
    ...
  }
  else {
    ...
  }
}
像这样一个100%可预测的分支非常适合JIT,而且这个快速路径应该得到非常好的优化。
\u rgu[0]
\u rgu[1]
甚至可能是内联的。这是非常便宜的,因此有效地将实际操作的数量减少了两倍

那么,为什么三人小组的速度要慢得多呢?很明显,它应该比k=2的速度慢;优化的乘法要少得多。更有趣的是为什么它比
k=1
慢。这一点很容易解释,因为
total
的外部乘法现在遇到了慢路径。对于
k=2
,通过将乘法数和阵列的潜在内联数减半,可以减轻这种影响

然而,这些因素并没有帮助
k=3
,事实上,慢速情况对
k=3
的伤害更大。
k=3
案例中的第二次乘法命中此案例

  if (reg1._iuLast == 0) {
    ...
  }
  else if (reg2._iuLast == 0) {
    Load(ref reg1, 1);
    Mul(reg2._uSmall);
  }
  else {
    ...
  }
哪个分配

  EnsureWritable(1);

  uint uCarry = 0;
  for (int iu = 0; iu <= _iuLast; iu++)
    uCarry = MulCarry(ref _rgu[iu], u, uCarry);

  if (uCarry != 0) {
    SetSizeKeep(_iuLast + 2, 0);
    _rgu[_iuLast] = uCarry;
  }
因此,
rgu
变为长度
3
总计
的代码中的通过次数由

public void Mul(ref BigIntegerBuilder reg1, ref BigIntegerBuilder reg2)
作为

然而,他们“优化”这一优化的方式比以前伤害更大:

if (reg1.CuNonZero <= reg2.CuNonZero) {
  rgu1 = reg1._rgu; cu1 = reg1._iuLast + 1;
  rgu2 = reg2._rgu; cu2 = reg2._iuLast + 1;
}
else {
  rgu1 = reg2._rgu; cu1 = reg2._iuLast + 1;
  rgu2 = reg1._rgu; cu2 = reg1._iuLast + 1;
}

if(reg1.CuNonZero不,假设它是
i+j
,因为在这种情况下,
i
将是偶数,
j
将是0,然后在内部循环中是1。这不是正确的。我确实确保了所有结果都是相同的。
i
是外部循环的当前位置,
j
是内部循环的当前位置e内环。因此,如果它被设置为4组,而外环当前为40,它将相乘,(40+0)*(40+1)*(40+2)*(40+3)。好的,很好,但你完全忽略了我答案的另一部分。请注意,你的代码可能最终成为某种“向量”计算,编译器会识别并使用“多媒体”进行优化指令之类的。如果重要的话,我会深入汇编来澄清。我的猜测是,在2的情况下,内部循环并不存在。(我不记得你说了什么编译器/架构/优化,所以我只能猜测一下。)你对每个组大小运行了多少次测试?为什么不
(int j=i;j
然后
partialTotal*=j;
?您甚至可以将
i+k
之和计算到一个变量中,以避免在循环中多次执行该操作。很可能是因为成对执行这些操作可以将循环开销减少50%。此外,请确保运行测试时使用的是发布版本,并且调试器s未连接(即Ctrl+F5或“调试->无调试启动”)。在附加调试程序的情况下运行会得到非常不正确的结果。我怀疑对的乘法结果在大多数情况下都在
int
范围内,而按顺序或以三元组进行乘法更经常涉及大数字。值得注意的是,使用
long
而不是
biginger
会产生错误性能只能用滴答声来衡量……这似乎是
biginger
的一个实现细节。这如何解释k=2,t时执行时间的急剧下降
public void Mul(ref BigIntegerBuilder reg1, ref BigIntegerBuilder reg2)
    for (int iu1 = 0; iu1 < cu1; iu1++) {
      ...
      for (int iu2 = 0; iu2 < cu2; iu2++, iuRes++)
        uCarry = AddMulCarry(ref _rgu[iuRes], uCur, rgu2[iu2], uCarry);
      ...
    }
      uint uCur = rgu1[iu1];
      if (uCur == 0)
        continue;
if (reg1.CuNonZero <= reg2.CuNonZero) {
  rgu1 = reg1._rgu; cu1 = reg1._iuLast + 1;
  rgu2 = reg2._rgu; cu2 = reg2._iuLast + 1;
}
else {
  rgu1 = reg2._rgu; cu1 = reg2._iuLast + 1;
  rgu2 = reg1._rgu; cu2 = reg1._iuLast + 1;
}