有时我会发现自己编写尾递归函数。我一直在寻找高低,我发现在.NET框架中有尾递归,但我不确定在什么情况下我可以,在什么情况下我不能有效地使用尾递归。例如,我有一个简单的树,让我们称之为
public class Tree
{
public Tree parent = null;
public Tree(Tree parent)
{
this.parent = parent;
this.children = new List<Tree>();
}
public List<Tree> children {get; private set;}
public Tree root
{
get
{
return this.parent == null ? this : parent.root;
}
}
}
对于root属性,编译器会发出一个循环吗?会发出.tail吗?抖动是否会尊重.tail?什么都不会发生,算法会递归运行吗?最重要的是,我应该将其重写为迭代吗?
答案 0 :(得分:9)
C#编译器永远不会发出tail
前缀。
在你的代码中,尾部没有任何东西。原因是使用三元运算符。如果代码被重写为使用if
语句并返回每个分支,那么对parent.root
的调用将处于尾部位置。
在优化方面,编译器(F#或IronScheme)通常会将尾递归调用转换为while (true) { ... }
循环。这样做是因为它删除了尾调用和再次调用该方法的需要。
因此,如果允许C#发出尾调用(它不是),它可能会从以下变换:
public Tree root
{
get
{
if (parent == null) return this;
else return parent.root; // now in tail position
}
}
到(只是一个猜测)
public Tree root
{
get
{
Tree temp = this;
while (true)
{
if (temp.parent == null)
{
return temp;
}
temp = temp.parent;
}
}
}
F#和IronScheme都进行了相同的转换。这称为tail call elimination
(TCE)。是的,它会比C#版本快一点。 (我在C#,F#和IronScheme上通过微基准测试fib
对此进行了测试
答案 1 :(得分:1)
答案类似other answer。
关于速度。尾递归优化与小函数的循环没有什么不同。当尾调用优化触发它时,只需用“jmp”替换“call”指令(在x86上)。当进行相同的循环时,您将获得进入下一循环的相同“jmp”指令。你应该记住的一点是,整个函数体将是循环体,因此你应该尽量减小递归函数的大小。