由于难以使用迭代循环,我最近一直在阅读关于Erlang的内容以及如何大量使用尾递归。
这种递归的高使用是否会降低它的速度,所有函数调用以及它们对堆栈的影响是什么?或者尾递归否定了大部分内容?
答案 0 :(得分:17)
重点是Erlang优化尾调用(不仅是递归)。优化尾调用非常简单:如果返回值是通过调用另一个函数计算的,那么另一个函数不仅仅放在调用函数顶部的函数调用堆栈上,而是当前函数的堆栈帧是被其中一个被调用函数替换。这意味着尾调用不会增加堆栈大小。
所以,不,使用尾递归不会减慢Erlang的速度,也不会造成堆栈溢出的风险。
使用尾调用优化,你不仅可以使用简单的尾递归,还可以使用几个函数的相互尾递归(尾调用b,尾调用c,尾调用...)。这有时可能是一个很好的计算模型。
答案 1 :(得分:8)
迭代尾递归通常使用Tail calls实现。 这基本上是递归调用到简单循环的转换。
C#示例:
uint FactorialAccum(uint n, uint accum) {
if(n < 2) return accum;
return FactorialAccum(n - 1, n * accum);
};
uint Factorial(uint n) {
return FactorialAccum(n, 1);
};
到
uint FactorialAccum(uint n, uint accum) {
start:
if(n < 2) return accum;
accum *= n;
n -= 1;
goto start;
};
uint Factorial(uint n) {
return FactorialAccum(n, 1);
};
甚至更好:
uint Factorial(uint n) {
uint accum = 1;
start:
if(n < 2) return accum;
accum *= n;
n -= 1;
goto start;
};
C#不是真正的尾递归,这是因为返回值被修改,大多数编译器都不会将其分解为循环:
int Power(int number, uint power) {
if(power == 0) return 1;
if(power == 1) return number;
return number * Power(number, --power);
}
到
int Power(int number, uint power) {
int result = number;
start:
if(power == 0) return 1;
if(power == 1) return number;
result *= number;
power--;
goto start;
}
答案 2 :(得分:3)
在大多数情况下,它不应影响性能。您正在寻找的不仅仅是尾调用,还有尾调用优化(或尾调用消除)。尾调用优化是一种编译器或运行时技术,可以确定对函数的调用是否相当于“弹出堆栈”以返回正确的函数而不是仅返回。通常尾部调用优化只能在递归调用是函数中的最后一个操作时完成,所以你必须要小心。
答案 3 :(得分:2)
尾部递归存在一个问题,但它与性能无关 - Erlang尾部递归优化还涉及消除堆栈跟踪以进行调试。
例如,请参阅Erlang FAQ的第9.13点:
Why doesn't the stack backtrace show the right functions for this code:
-module(erl).
-export([a/0]).
a() -> b().
b() -> c().
c() -> 3 = 4. %% will cause badmatch
The stack backtrace only shows function c(), rather than a(), b() and c().
This is because of last-call-optimisation; the compiler knows it does not need
to generate a stack frame for a() or b() because the last thing it did was
call another function, hence the stack frame does not appear in the stack
backtrace.
当你遇到崩溃时,这可能会有点痛苦(但它确实与函数式编程有关......)
答案 4 :(得分:1)
将程序文本函数调用与实现函数调用分开的类似优化是“内联”。在现代/周到语言中,函数调用与机器级函数调用几乎没有关系。