Scala, хвостовая рекурсия против не хвостовой рекурсии, почему хвостовая рекурсия медленнее?
Я объяснял другу, что я ожидал, что не хвостовая рекурсивная функция в Scala будет медленнее, чем хвостовые рекурсивные, поэтому я решил проверить это. Я написал хорошую старую факториальную функцию в обоих направлениях и попытался сравнить результаты. Вот код:
def main(args: Array[String]): Unit = {
val N = 2000 // not too much or else stackoverflows
var spent1: Long = 0
var spent2: Long = 0
for ( i <- 1 to 100 ) { // repeat to average the results
val t0 = System.nanoTime
factorial(N)
val t1 = System.nanoTime
tailRecFact(N)
val t2 = System.nanoTime
spent1 += t1 - t0
spent2 += t2 - t1
}
println(spent1/1000000f) // get milliseconds
println(spent2/1000000f)
}
@tailrec
def tailRecFact(n: BigInt, s: BigInt = 1): BigInt = if (n == 1) s else tailRecFact(n - 1, s * n)
def factorial(n: BigInt): BigInt = if (n == 1) 1 else n * factorial(n - 1)
результаты меня смущают, я получаю такой вывод:
578.2985
870.22125
значение не хвост рекурсивной функции на 30% быстрее, чем хвост рекурсивный, а количество операций одинаковое!
что бы объяснить эти результаты?
2 ответов
В дополнение к проблеме, показанной @monkjack (i.e умножение small * big происходит быстрее, чем big * small, что объясняет большую часть разницы), ваш алгоритм отличается в каждом случае, поэтому они не сопоставимы.
в хвостовой рекурсивной версии вы mutiplying big-to-small:
n * n-1 * n-2 * ... * 2 * 1
в не-хвостовой рекурсивной версии вы умножаете small-to-big:
n * (n-1 * (n-2 * (... * (2 * 1))))
Если вы измените хвост-рекурсивную версию, чтобы она умножает от малого до большого:
def tailRecFact2(n: BigInt) = {
def loop(x: BigInt, out: BigInt): BigInt =
if (x > n) out else loop(x + 1, x * out)
loop(1, 1)
}
тогда хвостовая рекурсия примерно на 20% быстрее обычной рекурсии, а не на 10% медленнее, как это, если вы просто сделаете коррекцию monkjack. Это связано с тем, что умножение маленьких Бигинтов быстрее, чем умножение больших.
Это на самом деле не там, где вы бы сначала посмотреть.Причина в вашем методе рекурсии хвоста, вы делаете больше работы с его умножением. Попробуйте поменять местами порядок параметров n и s в рекурсивном вызове, и он выровняется.
def tailRecFact(n: BigInt, s: BigInt): BigInt = if (n == 1) s else tailRecFact(n - 1, n * s)
кроме того, большую часть времени в этом примере занимают операции BigInt, которые затмевают время рекурсивного вызова. Если мы переключим их на Ints (скомпилированные в примитивы Java), то вы можете увидеть, как хвостовая рекурсия (goto) сравнивает с вызовом метода.
object Test extends App {
val N = 2000
val t0 = System.nanoTime()
for ( i <- 1 to 1000 ) {
factorial(N)
}
val t1 = System.nanoTime
for ( i <- 1 to 1000 ) {
tailRecFact(N, 1)
}
val t2 = System.nanoTime
println((t1 - t0) / 1000000f) // get milliseconds
println((t2 - t1) / 1000000f)
def factorial(n: Int): Int = if (n == 1) 1 else n * factorial(n - 1)
@tailrec
final def tailRecFact(n: Int, s: Int): Int = if (n == 1) s else tailRecFact(n - 1, s * n)
}
95.16733
3.987605
для интереса, декомпилированный выход
public final scala.math.BigInt tailRecFact(scala.math.BigInt, scala.math.BigInt);
Code:
0: aload_1
1: iconst_1
2: invokestatic #16 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
5: invokestatic #20 // Method scala/runtime/BoxesRunTime.equalsNumObject:(Ljava/lang/Number;Ljava/lang/Object;)Z
8: ifeq 13
11: aload_2
12: areturn
13: aload_1
14: getstatic #26 // Field scala/math/BigInt$.MODULE$:Lscala/math/BigInt$;
17: iconst_1
18: invokevirtual #30 // Method scala/math/BigInt$.int2bigInt:(I)Lscala/math/BigInt;
21: invokevirtual #36 // Method scala/math/BigInt.$minus:(Lscala/math/BigInt;)Lscala/math/BigInt;
24: aload_1
25: aload_2
26: invokevirtual #39 // Method scala/math/BigInt.$times:(Lscala/math/BigInt;)Lscala/math/BigInt;
29: astore_2
30: astore_1
31: goto 0
public scala.math.BigInt factorial(scala.math.BigInt);
Code:
0: aload_1
1: iconst_1
2: invokestatic #16 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
5: invokestatic #20 // Method scala/runtime/BoxesRunTime.equalsNumObject:(Ljava/lang/Number;Ljava/lang/Object;)Z
8: ifeq 21
11: getstatic #26 // Field scala/math/BigInt$.MODULE$:Lscala/math/BigInt$;
14: iconst_1
15: invokevirtual #30 // Method scala/math/BigInt$.int2bigInt:(I)Lscala/math/BigInt;
18: goto 40
21: aload_1
22: aload_0
23: aload_1
24: getstatic #26 // Field scala/math/BigInt$.MODULE$:Lscala/math/BigInt$;
27: iconst_1
28: invokevirtual #30 // Method scala/math/BigInt$.int2bigInt:(I)Lscala/math/BigInt;
31: invokevirtual #36 // Method scala/math/BigInt.$minus:(Lscala/math/BigInt;)Lscala/math/BigInt;
34: invokevirtual #47 // Method factorial:(Lscala/math/BigInt;)Lscala/math/BigInt;
37: invokevirtual #39 // Method scala/math/BigInt.$times:(Lscala/math/BigInt;)Lscala/math/BigInt;
40: areturn