在Java中,哪个阶乘函数的实现通常更快?

在Java中,哪个阶乘函数的实现通常更快?,java,optimization,Java,Optimization,我正在考虑阶乘函数的两种可能实现。总的来说,我不确定哪一个更快。我能想到为什么两者都可能更快的理由。(我实际上并不是在尝试实现一个快速的阶乘函数;我只是对此感到好奇。) 方法1: public static BigInteger factorial (int n) { BigInteger product = new BigInteger("1"); for (int i = 1; i<=n; i++) { product = product.mul

我正在考虑阶乘函数的两种可能实现。总的来说,我不确定哪一个更快。我能想到为什么两者都可能更快的理由。(我实际上并不是在尝试实现一个快速的阶乘函数;我只是对此感到好奇。)

方法1:

public static BigInteger factorial (int n) {
     BigInteger product = new BigInteger("1");
     for (int i = 1; i<=n; i++) {
          product = product.multiply(BigInteger.valueOf(i));
     }
     return product;     
}
基本上,方法1执行乘法1*2*…*n,如((1*2)*3)*…,方法2计算相同项的乘积,但顺序相反:((n*(n-1))*(n-2))*

我的问题是:其中哪一个通常运行速度更快

我知道将较大的数字相乘要慢一些,但将许多项相乘时,将乘积的值尽可能长时间保持在较小的位置(方法1),还是在总乘积仍然较小的情况下使用最大的项进行相乘(方法2)


它是否取决于
n
的大小?如果我使用
long
int
而不是
biginger
(除非溢出),或者如果我使用另一种语言,答案会有所不同吗?

在scala代码上做了一些时间映射,该代码与Java相同,看起来像

object RecursiveFactorial {

  def main(args: Array[String]): Unit = {
    val no = 100
    Util.time {
      factorial1(no)
    }
    Util.time {
      factorialN(1,no)
    }
  }

  def factorial1(n:Long) : Long = {
    if ((n == 1) || (n == 0) )
    {1}
    else {n * factorial1(n - 1)}
  }

  def factorialN(i:Long, n:Long) : Long = {
    if (i == n )
    {n}
    else {i * factorialN (i + 1,n)}
  }

}
// Elapsed time: 1038959ns // for factorialN
// Elapsed time: 17645ns   // for factorial1

看来1*.n的阶乘比n..*1快,至少在递归的情况下是这样的

我运行了两个不同的测试,这是您应该如何对java代码进行基准测试的,因为它可以减轻热点预热等的影响:

n = 100
platform: JDK11, intel x86-64 2,9Ghz core i5 laptop.
Benchmark               Mode  Cnt       Score       Error  Units
MyBenchmark.highToLow  thrpt   25  175954,070 ± 20689,017  ops/s
MyBenchmark.lowToHigh  thrpt   25  184311,758 ± 18965,592  ops/s
这看起来像是从低到高的胜利,但事实并非如此——这是相同的数字,这是统计噪声

使用更大的n,并减少迭代和运行时间,可以快速得到答案:

n = 10000
platform: JDK11, intel x86-64 2,9Ghz core i5 laptop.
Benchmark               Mode  Cnt   Score    Error  Units
MyBenchmark.highToLow  thrpt    6  34,683 ±  7,075  ops/s
MyBenchmark.lowToHigh  thrpt    6  31,230 ± 21,437  ops/s
这可以归结为同样的问题;这并不重要

换言之,速度同样快。

我用n=100000进行测试,发现在几次热身后,从低到高的速度始终比从高到低的速度快,但两种方法都被一种形式((1×2)×(3×4))×((5×6)×(7×8))乘以相邻数字对的方法大幅度击败,然后是一对相邻的结果,等等,直到最后有一个答案——目标是让绝大多数乘法都是小数字

当你认为一个大数的乘法比一个较小的数乘法更昂贵(所有其他都相等)时,这是有意义的。与从低到高的方法相比,从高到低的方法会将乘积快速增加许多数量级(第十次乘法的数量级已经是1050,而不是106.5),而从低到高的方法中的乘积在得到最终结果之前从未“赶上”。因此,几乎每一个简单的乘法在从低到高的方法中都是便宜的,有时是大幅度的

作为附加检查,我编写了一些逻辑来跟踪给定方法的粗略“成本”,假设给定乘法的“成本”大致是结果中的位数:

private static BigInteger multiply(final BigInteger a, final BigInteger b) {
    final BigInteger product = a.multiply(b);
    cost += product.bitLength();
    return product;
}
对于n=100000,从低到高的方法总“成本”为722296834;从高到低的方法的总“成本”为79442345171(约高出11%);而反复拆分为一半的方法的总“成本”为25362728(约低96%)。这与我看到的时间一致


下面是一个典型的跑步(包括热身等):

正如您所看到的,确切的时间会有所不同,但变化不大(除了前几次迭代);这些数字非常一致,方法1始终在3.5秒以下,方法2始终在3.6秒以上。接近#3的时间始终低于0.1秒

对于较小的n值(1000和10000),我观察到了相同的趋势,但噪声更多


完整代码(仅用于时间比较,而非“成本”内容):

import java.math.biginger;
导入java.util.function.Supplier;
公共最终课程SO62307487{
公共静态void main(最终字符串…参数){
final int n=Integer.parseInt(args[0]);
对于(int i=0;i<10;++i){
timeIt(“从低到高”,()->系数从低到高(n));
timeIt(“从高到低”,()->从高到低的因子(n));
timeIt(“一分为二”,()->factorialBySplitInHalf(n));
System.out.println();
}
}
私有静态void timeIt(最终字符串id,最终供应商){
final long startNanos=System.nanoTime();
最终BigInteger结果=supplier.get();
final long-endNanos=System.nanoTime();
System.out.printf(
%s:%d位,在%8.3fms中。%n“,
id,result.bitLength(),(endNanos-startNanos)/1000000.0);
}
私有静态BigInteger factoralFromLowToHigh(最终整数n){
BigInteger乘积=BigInteger.1;
for(int i=1;i=1;--i){
乘积=乘积乘以(BigInteger.valueOf(i));
}
退货产品;
}
私有静态BigInteger阶乘BySplitInHalf(final int n){
返回helpSplitInHalf(1,n);
}
私有静态BigInteger helpSplitInHalf(final int first,final int last){
如果(第一个==最后一个){
返回BigInteger.valueOf(第一个);
}
最终int mid=first+(last-first)/2;
返回helpSplitInHalf(第一个,中间)。乘法(helpSplitInHalf(中间+1,最后));
}
}

直觉上,我认为顺序会使性能差异变得微不足道(如果有的话),但唯一的方法是实际对其进行基准测试。给它一个足够大的n,看看它是如何运行的。在溢出之前,使用
long
很可能会更快。我希望这两个不同的循环之间没有什么区别。迭代次数与循环中完成的工作相同。使用基元类型而不是对象进行此计算可能会非常困难
private static BigInteger multiply(final BigInteger a, final BigInteger b) {
    final BigInteger product = a.multiply(b);
    cost += product.bitLength();
    return product;
}
$ javac SO62307487.java && java SO62307487 100000
  low-to-high: 1516705 bits in 3547.334ms.
  high-to-low: 1516705 bits in 3688.083ms.
split-in-half: 1516705 bits in  175.483ms.

  low-to-high: 1516705 bits in 3892.075ms.
  high-to-low: 1516705 bits in 3805.003ms.
split-in-half: 1516705 bits in  116.792ms.

  low-to-high: 1516705 bits in 3444.635ms.
  high-to-low: 1516705 bits in 3976.932ms.
split-in-half: 1516705 bits in   97.262ms.

  low-to-high: 1516705 bits in 3689.550ms.
  high-to-low: 1516705 bits in 3746.681ms.
split-in-half: 1516705 bits in   95.459ms.

  low-to-high: 1516705 bits in 3474.545ms.
  high-to-low: 1516705 bits in 3706.841ms.
split-in-half: 1516705 bits in   96.370ms.

  low-to-high: 1516705 bits in 3427.387ms.
  high-to-low: 1516705 bits in 3700.014ms.
split-in-half: 1516705 bits in   95.865ms.

  low-to-high: 1516705 bits in 3491.601ms.
  high-to-low: 1516705 bits in 3699.362ms.
split-in-half: 1516705 bits in   95.737ms.

  low-to-high: 1516705 bits in 3453.318ms.
  high-to-low: 1516705 bits in 3649.198ms.
split-in-half: 1516705 bits in   95.564ms.

  low-to-high: 1516705 bits in 3436.716ms.
  high-to-low: 1516705 bits in 3698.135ms.
split-in-half: 1516705 bits in   95.649ms.

  low-to-high: 1516705 bits in 3443.338ms.
  high-to-low: 1516705 bits in 3732.814ms.
split-in-half: 1516705 bits in   95.193ms.
import java.math.BigInteger;
import java.util.function.Supplier;

public final class SO62307487 {
    public static void main(final String... args) {
        final int n = Integer.parseInt(args[0]);
        for (int i = 0; i < 10; ++i) {
            timeIt("  low-to-high", () -> factoralFromLowToHigh(n));
            timeIt("  high-to-low", () -> factoralFromHighToLow(n));
            timeIt("split-in-half", () -> factorialBySplitInHalf(n));
            System.out.println();
        }
    }

    private static void timeIt(final String id, final Supplier<BigInteger> supplier) {
        final long startNanos = System.nanoTime();
        final BigInteger result = supplier.get();
        final long endNanos = System.nanoTime();
        System.out.printf(
            "%s: %d bits in %8.3fms.%n",
            id, result.bitLength(), (endNanos - startNanos) / 1000000.0);
    }

    private static BigInteger factoralFromLowToHigh(final int n) {
        BigInteger product = BigInteger.ONE;
        for (int i = 1; i <= n; ++i) {
            product = product.multiply(BigInteger.valueOf(i));
        }
        return product;
    }

    private static BigInteger factoralFromHighToLow(final int n) {
        BigInteger product = BigInteger.ONE;
        for (int i = n; i >= 1 ; --i) {
            product = product.multiply(BigInteger.valueOf(i));
        }
        return product;
    }

    private static BigInteger factorialBySplitInHalf(final int n) {
        return helpSplitInHalf(1, n);
    }

    private static BigInteger helpSplitInHalf(final int first, final int last) {
        if (first == last) {
            return BigInteger.valueOf(first);
        }
        final int mid = first + (last - first) / 2;
        return helpSplitInHalf(first, mid).multiply(helpSplitInHalf(mid + 1, last));
    }
}