【问题标题】:Runtime exception, recursion too deep运行时异常,递归太深
【发布时间】:2011-05-05 15:16:59
【问题描述】:

我将伪代码here 转换为 C#,并让它递归重复 10,000 次。但是我在9217 次之后得到一个 C# 运行时错误,StackOverflow Exception。我怎样才能防止这种情况发生?

编辑如果对任何人有帮助,代码如下:

    private double CalculatePi(int maxRecursion)
    {
        return 2 * CalculatePi(maxRecursion, 1);
    }

    private double CalculatePi(int maxRecursion, int i)
    {
        if (i >= maxRecursion)
            return 1;
        return 1 + i / (2.0 * i + 1) * CalculatePi(maxRecursion, i + 1);
    }

    double pi = CalculatePi(10000); // 10,000 recursions

EDIT2 所以每个人似乎都同意我需要将其转换为迭代...有人可以提供一些代码吗?我似乎无法编写任何有效的迭代代码......

编辑感谢 Paul Rieck 提供的这个答案,我对其进行了测试,并且有效:

    private static double CalculatePi(int maxRecursion)
    {
        double result = 1;
        for (int i = maxRecursion; i >= 1; i-- )
        {
            result = 1 + i / (2.0 * i + 1) * result;
        }
        return result * 2;
    }

【问题讨论】:

  • 编译器将在编译期间显示错误或警告。你得到一个 runtime 异常。你能发布确切的异常细节吗?
  • 将迭代次数从 10000 更改为 51。您会得到相同的结果。 :)
  • 不,实际上。我可以将迭代设置为 1 到 9216 之间的任何位置,而不会出现异常。
  • @TheAdamGaskins:你没有抓住重点。我的意思是你不需要使用 10000 次递归。 51 及以上的任何值都会给出相同的结果。最后的 9949 次递归是无用的,因为它们的贡献超出了 double 的精度。
  • msdn.microsoft.com/en-us/library/s1ax56ch(v=VS.80).aspx 或者,您可以使用十进制,但您可能必须调整调用方以使用该值。有关更多信息,请参阅上面链接的参考。

标签: c# exception recursion


【解决方案1】:

所以每个人似乎都同意我需要将其转换为迭代...有人可以提供一些代码吗?我似乎无法编写任何有效的迭代代码......

您是要一条鱼,还是要学习如何捕鱼?如果本练习的目的是学习如何将递归代码转换为迭代代码,那么仅仅获得答案并不能很好地为您服务。

要将递归代码转换为迭代代码,有很多方法可以做到这一点。在这种情况下,最简单的方法是简单地计算出模式。代码有什么作用?它计算:

(1 + 1 / (2.0 * 1 + 1)) * 
(1 + 2 / (2.0 * 2 + 1)) * 
(1 + 3 / (2.0 * 3 + 1)) * 
(1 + 4 / (2.0 * 4 + 1)) * 
(1 + 5 / (2.0 * 5 + 1)) * 
(1 + 6 / (2.0 * 6 + 1)) * 
(1 + 7 / (2.0 * 7 + 1)) * 
...
(1 + 9999/ (2.0 * 9999+ 1)) *
1

现在你能写一个循环来计算它吗?当然。

double product = 1.0;
for (int i = 9999; i >= 0; --i)
    product *= (1 + i / (2.0 * i + 1));

这是最简单的方法。但是有很多方法可以解决这个问题。

您可以使用聚合器。考虑“总”操作;这是一个聚合器的例子。您有一系列事物,并保持它们的运行总数,将结果累积到累加器中。标准查询运算符为您提供了一个聚合操作:

double seed = 1.0;
Enumerable.Range(0, 10000).Aggregate(seed, 
    (product, i) => product * (1 + i / (2.0 * i + 1))
    

或者,您可以保持算法递归,但通过以下方式消除堆栈溢出:

  • 在堆上构建您自己的堆栈结构
  • 定义虚拟机,用虚拟机语言编写程序,然后实现虚拟机以将其堆栈保留在堆上。
  • 以继续传递样式重写您的程序;然后沿途的每一步都向调用者返回一个延续,调用者调用下一个延续;堆栈永远不会变深

解释这些技术需要很长时间才能得到一个答案。我有一个由六部分组成的博客系列,介绍如何使用这些技术将递归程序转变为不消耗太多堆栈的程序;从这里开始阅读:

Link

【讨论】:

    【解决方案2】:

    “c# 编译器”可以很好地编译它。但是,在运行时,您最终可能会遇到 StackOverflow 异常(在我的机器上,10,000 可以正常工作,但稍后会死掉)。

    这不是“无限递归异常”——堆栈溢出正是它所说的;调用方法意味着将信息放入堆栈并在调用返回时将其删除。您有 10,000 次调用,但没有一个返回,因此堆栈已满并引发异常。

    在这种特殊情况下,您可以通过删除递归并迭代运行来相当容易地解决这个问题。在一般情况下,可以通过迭代、尾递归优化和继续传递来消除递归。

    这里是迭代样式的快速直接翻译:

    private static double CalculatePi(int maxRecursion)
            {
                double result = 1;
                for (int i = maxRecursion -1; i >= 1; i-- )
                {
                    result = 1 + i / (2.0 * i + 1) * result;
                }
                return result * 2;
            }
    

    【讨论】:

    • 您必须从 maxRecursion 中减去 1 才能获得与递归版本完全相同的结果。请参阅我的答案中的代码。
    【解决方案3】:

    不要使用递归。这是一个不应该递归的很好的示例,因为这会导致调用堆栈溢出。

    您可以轻松地将其转换为非递归。

    【讨论】:

      【解决方案4】:

      你无法阻止它,函数调用自身太多次;调用堆栈可以达到的深度以及您的函数可以达到的深度是有限制的。

      【讨论】:

        【解决方案5】:

        了解其他人在这里所说的话——这是一个 Stack Overflow(哇!StackOverflow 上的一个 Stack Overflow 问题。这可能会导致堆栈......)

        无论如何,每次调用你的递归方法时,它都会压栈,也就是说,它会将对自身的引用放入堆栈,这样当最后一次调用CalculatePi时,它可以展开所有的其余的电话。

        堆栈不是无限资源。每次压栈都会占用一点内存,当你用完时,就会出现栈溢出。

        【讨论】:

          【解决方案6】:

          递归调用不是尾递归的,即使是,它也无济于事,因为 C# 编译器当前不优化尾递归调用。编辑:正如 Eric Lippert 和 Gabe 所指出的,即使在发出的 IL 中没有明确的尾调用指令,CLR 也可以选择生成尾调用。

          1. 最佳方法是将这种递归解决方案转变为迭代解决方案。
          2. 由于它几乎完成,一个快速的技巧可能是增加运行此方法的线程的堆栈大小。

          请不要这样做。仅供娱乐:

          static void Main()
          {
              Console.WriteLine(SafeCalculatePi(10000));
          }
          
          // Calculates PI on a separate thread with enough stack-space 
          // to complete the computation
          public static double SafeCalculatePi(int maxRecursion)
          {
              // We 'know' that you can reach a depth of 9217 for a 1MB stack.
              // This lets us calculate the required stack-size, with a safety factor
              double requiredStackSize = (maxRecursion / 9217D) * 1.5 * 1024 * 1024 ; 
          
              double pi = 0;
              ThreadStart ts = delegate { pi = CalculatePi(maxRecursion); };
              Thread t = new Thread(ts, (int)requiredStackSize);
              t.Start();
              t.Join();
              return pi;
          }
          

          【讨论】:

          • 是否有链接器/编译器选项来设置初始线程堆栈大小?
          • 某些版本的抖动确实优化了一些尾递归程序,仅供参考。但一般情况下你不能依赖它。
          • @Ani:C# 编译器不发出尾调用指令是正确的。这和什么有什么关系?您似乎认为未标记为尾调用的调用不允许生成为尾调用;我知道这个想法没有事实依据。抖动完全在它认为合适的优化方法的权利范围内,包括在优化生成正确程序时将返回转换为尾调用。
          • Ani:我写了一个小函数,它调用自己 100,000,000 次,并确认它在 x64 上运行到未调试的发布版本上完成。如果我切换到 x86、进行 Debug 构建或在调试器下运行,则会出现堆栈溢出。
          【解决方案7】:

          这显然不是无限递归,因为它在 10,000 级后停止,但这仍然不是最好的主意。

          一万层堆栈是非常多的——堆栈并不是一个巨大的资源。您应该将其转换为迭代解决方案。

          【讨论】:

            【解决方案8】:

            由于您几乎可以运行该程序,您可以让它使用更少的堆栈空间。只需在发布模式而不是调试模式下运行,它应该可以工作。

            【讨论】:

              【解决方案9】:

              执行此操作的迭代代码很简单:

              private double CalculatePi(int iterations) {
                double pi = 1.0;
                for (int i = iterations - 1; i >= 1; i--) {
                  pi = 1 + i / (2.0 * i + 1) * pi;
                }
                return pi * 2.0;
              }
              

              用法:

              double pi = CalculatePi(51);
              

              【讨论】:

                【解决方案10】:

                这将是非递归变体:

                        private double CalculatePi(int maxRecursion)
                    {
                        return 2 * CalculatePi2(maxRecursion, 1);
                    }
                
                    private double CalculatePi2(int maxRecursion, int i)
                    {
                        double product = 1;
                        while (i < maxRecursion)
                        {
                            product = product * (1f + i / (2.0f * i + 1f));
                            i++;
                        }
                        return product;
                    }
                

                【讨论】:

                  【解决方案11】:

                  迭代版本:

                  public static double CalculatePi(int maxRecursion)
                  {
                      double result=1;
                      for(int i=maxRecursion-1;i>=1;i--)
                      {
                          result=1+i/(2.0*i+1)*result;  
                      }
                      return result*2;
                  }
                  

                  【讨论】:

                    【解决方案12】:

                    递归函数调用几乎总是一种非常糟糕的处理方式。

                    我会简单地从头文件/数学库中获取 pi 的值。

                    【讨论】:

                    • 是的——但这就是重点。不要用任何语言学习糟糕的技术。如果该语言为您提供了一种无需使用递归的方式来做某事的方法,那就是更好地使用该语言。
                    • 触摸。我的最后一条评论是针对你的第二句话,但我明白你的意思:)
                    • 相反,递归调用很少是一种非常糟糕的做事方式。除非它们可以导致无限的堆栈增长(例如遍历任意大小的不平衡树),否则它们会变得糟糕吗?
                    猜你喜欢
                    • 1970-01-01
                    • 2016-03-02
                    • 2015-07-21
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2017-06-01
                    • 2016-05-12
                    • 2015-09-08
                    相关资源
                    最近更新 更多