【问题标题】:How do I avoid changing the Stack Size AND avoid getting a Stack Overflow in C#如何避免更改堆栈大小并避免在 C# 中出现堆栈溢出
【发布时间】:2010-12-04 00:29:44
【问题描述】:

几个小时以来,我一直试图在网上和这个网站上找到这个问题的答案,但我还没有找到答案。

我了解 .NET 会为应用分配 1MB,最好通过重新编码而不是强制堆栈大小来避免堆栈溢出。

我正在开发一个“最短路径”应用程序,该应用程序在大约 3000 个节点上运行良好,此时它会溢出。以下是导致问题的方法:

    public void findShortestPath(int current, int end, int currentCost)
    {
        if (!weight.ContainsKey(current))
        {
            weight.Add(current, currentCost);
        }
        Node currentNode = graph[current];
        var sortedEdges = (from entry in currentNode.edges orderby entry.Value ascending select entry);
        foreach (KeyValuePair<int, int> nextNode in sortedEdges)
        {
            if (!visited.ContainsKey(nextNode.Key) || !visited[nextNode.Key])
            {
                int nextNodeCost = currentCost + nextNode.Value;
                if (!weight.ContainsKey(nextNode.Key))
                {
                    weight.Add(nextNode.Key, nextNodeCost);
                }
                else if (weight[nextNode.Key] > nextNodeCost)
                {
                    weight[nextNode.Key] = nextNodeCost;
                }
            }
        }
        visited.Add(current, true);
        foreach (KeyValuePair<int, int> nextNode in sortedEdges)
        {
            if(!visited.ContainsKey(nextNode.Key) || !visited[nextNode.Key]){
                findShortestPath(nextNode.Key, end, weight[nextNode.Key]);
            }
        }
    }//findShortestPath

作为参考,Node 类有一个成员:

 public Dictionary<int, int> edges = new Dictionary<int, int>();

图[]是​​:

  private Dictionary<int, Node> graph = new Dictonary<int, Node>();

我已尝试优化代码,使其从一次迭代(递归?)到下一次迭代不会携带比所需更多的包袱,而是使用 100K 节点图,每个节点都有 1-9 条边它将很快达到 1MB 的限制。

无论如何,我是 C# 和代码优化的新手,如果有人可以给我一些指点 (not like this),我将不胜感激。

【问题讨论】:

    标签: c# stack-overflow


    【解决方案1】:

    避免深度递归堆栈潜水的经典技术是通过迭代编写算法并使用适当的列表数据结构管理您自己的“堆栈”来简单地避免递归。考虑到输入集的庞大规模,您很可能需要这种方法。

    【讨论】:

    • 或者确保递归调用可以使用尾递归进行优化。
    【解决方案2】:

    不久前,我在博客中探讨了这个问题。或者,更确切地说,我探讨了一个相关的问题:如何在不使用递归的情况下找到二叉树的深度?递归树深度解决方案很简单,但如果树高度不平衡,则会破坏堆栈。

    我的建议是研究解决这个更简单问题的方法,然后决定哪些方法(如果有)可以适应您稍微复杂一点的算法。

    请注意,这些文章中的示例完全用 JScript 给出。不过,让它们适应 C# 应该不难。

    我们从定义问题开始。

    http://blogs.msdn.com/ericlippert/archive/2005/07/27/recursion-part-one-recursive-data-structures-and-functions.aspx

    解决方案的第一次尝试是您可能会采用的经典技术:定义显式堆栈;使用它而不是依赖为您实现堆栈的操作系统和编译器。这是大多数人在遇到这个问题时会做的事情。

    http://blogs.msdn.com/ericlippert/archive/2005/08/01/recursion-part-two-unrolling-a-recursive-function-with-an-explicit-stack.aspx

    该解决方案的问题在于它有点混乱。我们可以走得更远,而不是简单地制作自己的堆栈。我们可以创建自己的小型特定领域虚拟机,它有自己的堆分配堆栈,然后通过编写一个针对该机器的程序来解决问题!这实际上比听起来容易。机器的操作可以达到非常高的水平。

    http://blogs.msdn.com/ericlippert/archive/2005/08/04/recursion-part-three-building-a-dispatch-engine.aspx

    最后,如果你真的是一个喜欢惩罚的人(或编译器开发者),你可以用延续传递风格重写你的程序,从而完全不需要堆栈:

    http://blogs.msdn.com/ericlippert/archive/2005/08/08/recursion-part-four-continuation-passing-style.aspx

    http://blogs.msdn.com/ericlippert/archive/2005/08/11/recursion-part-five-more-on-cps.aspx

    http://blogs.msdn.com/ericlippert/archive/2005/08/15/recursion-part-six-making-cps-work.aspx

    CPS 是将隐式堆栈数据结构从系统堆栈移到堆上的一种特别聪明的方法,方法是在一堆委托之间的关系中对其进行编码。

    这是我所有关于递归的文章:

    http://blogs.msdn.com/ericlippert/archive/tags/Recursion/default.aspx

    【讨论】:

    • 或者......你可以确保你的算法可以被CLR转换成尾递归形式。
    • C# 永远不会生成尾调用指令。某些版本的抖动会注意到,即使不使用尾调用指令,也可以使用尾递归优化特定方法。但是,我们发布的大多数抖动都没有此优化,您不应依赖它。
    • 此外,您的建议实际上并不可行。 如何原始海报应该“确保算法可以转换为尾递归形式”?这是一个复杂的过程,需要深入了解运行时的实现细节,理解我不希望除 42 号楼的人之外的任何人拥有。
    • 我承认,除了用 F#(或 IL)重写之外,在 C# 中可靠地实现这一点可能并不容易 - 特别是因为只有 64 位 CLR 似乎使用 .tailcall 指令执行尾调用优化不见了。
    • 尽管如此,我想知道您是否不能直接通过 C# 中的 CodeDom 发出必要的 IL...
    【解决方案3】:

    您可以将代码转换为使用“工作队列”而不是递归的。以下伪代码:

    Queue<Task> work;
    while( work.Count != 0 )
    {
         Task t = work.Dequeue();
         ... whatever
         foreach(Task more in t.MoreTasks)
             work.Enqueue(more);
    }
    

    我知道这很神秘,但这是您需要做什么的基本概念。由于您当前的代码只能获得 3000 个节点,因此您最多可以在没有任何参数的情况下达到 12~15k。所以你需要完全终止递归。

    【讨论】:

    • 好点。事实上,您的代码本质上是节点的广度优先遍历,而不是来自 OP 的深度优先方法。
    【解决方案4】:

    你的节点是结构体还是类?如果是前者,请将其设为一个类,以便将其分配在堆上而不是堆栈上。

    【讨论】:

    • 所做的只是减少堆栈使用量,而不是消除它。大数据结构的深度回溯问题依然存在。
    • 确实如此——起初我看到这个数字只有 3000,并认为考虑到 1MB 的堆栈空间,这有点小,但如果你算一算,这似乎很合理。
    【解决方案5】:

    我将首先验证您实际上是在溢出堆栈:您实际上看到一个 StackOverflowException 被运行时抛出。

    如果确实如此,您有几个选择:

    1. 修改您的递归函数,以便 .NET 运行时可以将其转换为 tail-recursive function
    2. 修改您的递归函数,使其可迭代并使用自定义数据结构而不是托管堆栈。

    选项 1 并不总是可行的,它假定 CLR 用于生成尾递归调用的规则在未来将保持稳定。主要的好处是,在可能的情况下,尾递归实际上是一种在不牺牲清晰度的情况下编写递归算法的便捷方式。

    选项 2 的工作量更大,但对 CLR 的实现不敏感,并且可以为任何递归算法实现(尾递归可能并不总是可行)。通常,您需要在某个循环的迭代之间捕获和传递状态信息,以及有关如何“展开”占据堆栈位置的数据结构(通常是 List 或 Stack)的信息。将递归展开到迭代中的一种方法是通过continuation passing 模式。

    更多关于 C# 尾递归的资源:

    Why doesn't .NET/C# optimize for tail-call recursion?

    http://geekswithblogs.net/jwhitehorn/archive/2007/06/06/113060.aspx

    【讨论】:

      【解决方案6】:

      我首先要确定我知道为什么会出现堆栈溢出。真的是因为递归吗?递归方法并没有在堆栈上放太多东西。也许是因为节点的存储?

      另外,顺便说一句,我没有看到 end 参数不断变化。这表明它不需要是每个堆栈帧上携带的参数。

      【讨论】:

        猜你喜欢
        • 2020-03-08
        • 2010-11-30
        • 1970-01-01
        • 2013-10-29
        • 2011-09-21
        • 2014-04-13
        • 2015-11-14
        • 2011-11-23
        • 2016-07-12
        相关资源
        最近更新 更多