我刚刚在csharp中对递归函数的性能进行了一些阅读。我读到由于每次调用时都会创建堆栈帧,递归函数很昂贵。
有没有办法防止在csharp中创建堆栈帧?我环顾四周,但似乎无法找到任何暗示特定呼叫可以抑制堆栈帧的内容。
我希望有一些东西可以添加到代码中,也许是如下属性:
[SurpressStackFrame()]
find(int id, Node currentNode) {
if(currentNode.id == id) {
return true;
}
return find(id, currentNode.child);
}
(p.s。我知道我在这个例子中并没有看多个孩子,这只是假设)。
答案 0 :(得分:2)
在大多数情况下,创建的堆栈帧不是代码的性能瓶颈。但是,如果方法中存在高级别的递归,则可能希望避免扩大堆栈。这称为尾调用优化,通过在调用内部方法之前释放当前堆栈帧来完成。
在您的具体示例中,尾调用可以应用于return
语句,因为内部方法调用的返回值会立即返回给调用者,因此不需要单独的堆栈帧。
唯一的问题是C#不支持尾调用(目前并且可能不会在不久的将来)。但是,CLR确实支持它,所以如果可以,你可以选择像F#这样更专注于递归的语言。
还有其他一些选项 - 动态创建一个使用CIL的方法,最后进行尾调用(但是,我不确定调用动态方法的费用是否比调用正常的递归方法更好) C#)。
这种情况下的最佳选择 - 根本不使用递归。您提供的方法可以轻松地重写,而不需要使用递归,当然,大多数(如果不是所有)递归方法都可以通过这种方式重写:
bool find(int id, Node currentNode)
{
while(currentNode.id != id)
{
currentNode = currentNode.child;
}
return true;
}
答案 1 :(得分:1)
它被称为tail call。它受.NET运行时支持(因为F#需要它),但是C#编译器(甚至是#34; new" Roslyn编译器)不支持它。请参阅request on the github。
答案 2 :(得分:1)
您正在寻找的是尾调用。
有尾巴。 IL中的指令,但C#编译器从不使用它。无论如何,它只是对JIT编译器的一个提示,并且JIT编译器足够聪明,可以在可能的情况下将常规递归方法调用编译为尾调用(在您发布的示例中应该可以)。与其他JIT优化一样,它只发生在发布版本中,而不是在调试中。
您可以进行简单的测试:
class Program
{
static void Main()
{
TailCall(0);
}
private static void TailCall(int i)
{
Console.WriteLine(i);
TailCall(++i);
}
}
在debug中,它将抛出一个StackOverflowException,在Release中它将无限旋转。
您可以在this blog post中找到有关.NET中尾调用优化的更多信息。