【问题标题】:Is this tail recursive and could it cause a stack overflow? C# .Net这个尾递归是否会导致堆栈溢出? C# .Net
【发布时间】:2016-06-11 22:01:59
【问题描述】:

我想知道为什么我得到 VS/Resharper 的第一种方式注意到尾递归可以用循环替换,而且我确实设法得到类似导致堆栈溢出的东西,所以我阅读了尾递归的方法工作以及它如何增长堆栈等。

第一个生成尾递归注释。

    private static string GetLine()
    {
        string s = Console.ReadLine();

        if (s == null)
        {
            return GetLine();
        }
        return s;
    }

但这样做不会:

    private static string GetLine()
    {
        string s = Console.ReadLine();

        if (s == null)
        {
            s = GetLine();
        }
        return s;
    }

所以我的问题是;第二个不被认为是尾递归的,即它不能创建堆栈溢出,因为它不会生成所有堆栈调用?

【问题讨论】:

  • 你倒过来了,尾递归不会产生栈溢出,但是非尾递归调用可以。
  • 在任何情况下,C# 都不能保证即使尾调用优化可以完成,它实际上已经完成。所以它无论如何都会造成堆栈溢出(它实际上也会发生,除了有时在 x64 上)
  • @LucasTrzesniewski, naive tail recursion 产生堆栈溢出,除非系统中存在(并且被正确使用)运算符来针对该场景进行优化。 CLR 确实有这样的操作码 (tail)。 C# 没有可靠地利用该操作码的工具。
  • @KirkWoll AFAIK RyuJIT 不需要 tail 前缀并生成一个循环(请参阅我的答案),但不能保证,正如 x86 版本所示。
  • @Kirk,C# 本身(即我们)可能没有该车辆,但 编译器 拥有它,并且可以根据其运行的上下文决定使用它。这样实际上更好更安全。

标签: c# .net stack-overflow tail-recursion


【解决方案1】:

正如 usr 在 his answer 中所解释的那样,ReSharper 会尝试在您的代码中找到已知模式,并为其提供重构。

但是让我们来看看这两种情况下生成的代码。


第一个函数:

private static string GetLineA()
{
    string s = Console.ReadLine();

    if (s == null)
    {
        return GetLineA();
    }
    return s;
}

提供这个(x64,发布):

00007FFB34AC43EE  add         byte ptr [rax],al  
00007FFB34AC43F0  sub         rsp,28h  
00007FFB34AC43F4  call        00007FFB8E56F530  // <-- Console.ReadLine
00007FFB34AC43F9  test        rax,rax  
00007FFB34AC43FC  jne         00007FFB34AC440F  
00007FFB34AC43FE  mov         rax,7FFB34AC0F60h  
00007FFB34AC4408  add         rsp,28h  
00007FFB34AC440C  jmp         rax  
00007FFB34AC440F  add         rsp,28h  
00007FFB34AC4413  ret  

您可以清楚地看到它是尾递归的,因为唯一的 call 指令是针对 Console.ReadLine 的。


第二个版本:

private static string GetLineB()
{
    string s = Console.ReadLine();

    if (s == null)
    {
        s = GetLineB();
    }
    return s;
}

给出这个:

00007FFB34AC44CE  add         byte ptr [rax],al  
00007FFB34AC44D0  sub         rsp,28h  
00007FFB34AC44D4  call        00007FFB8E56F530  // <-- Console.ReadLine
00007FFB34AC44D9  test        rax,rax  
00007FFB34AC44DC  jne         00007FFB34AC44E3  
00007FFB34AC44DE  call        00007FFB34AC0F68 // <-- Not good.
00007FFB34AC44E3  nop  
00007FFB34AC44E4  add         rsp,28h  
00007FFB34AC44E8  ret  

那里有第二个call,所以你不会得到尾递归,并且堆栈会增长,如果它增长到足够大,最终会导致堆栈溢出。

嗯,JIT 似乎没有将代码优化为尾递归调用。


无论如何,请小心,因为您受 JIT 的摆布。

这是 x86 中的 GetLineA

00F32DCA  in          al,dx  
00F32DCB  call        72A209DC  // <-- Console.ReadLine
00F32DD0  test        eax,eax  
00F32DD2  jne         00F32DDC  
00F32DD4  call        dword ptr ds:[12B8E94h]  // <-- Ouch
00F32DDA  pop         ebp  
00F32DDB  ret  
00F32DDC  pop         ebp  
00F32DDD  ret  

看到了吗?您不能真正依赖它,而且语言无法提供任何保证。

【讨论】:

    【解决方案2】:

    Resharper 只是没有检测到第二种形式。它不努力。程序分析通常很难而且不可能(参见停机问题)。 Resharper 主要有一些很好且有用的启发式方法。

    如果您将ReadLine 替换为null,您会发现这里很可能发生堆栈溢出。

    【讨论】:

      猜你喜欢
      • 2020-03-06
      • 2018-10-29
      • 1970-01-01
      • 2019-05-28
      • 2013-06-16
      • 2011-02-26
      相关资源
      最近更新 更多