在Java中,哪个阶乘函数的实现通常更快?
我正在考虑阶乘函数的两种可能实现。总的来说,我不确定哪一个更快。我能想到为什么两者都可能更快的理由。(我实际上并不是在尝试实现一个快速的阶乘函数;我只是对此感到好奇。) 方法1:在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
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));
}
}