Как избежать изменения размера стека и избежать переполнения стека в C#

Я пытаюсь найти ответ на этот вопрос в течение нескольких часов сейчас в интернете и на этом сайте, и я не совсем там.

Я понимаю, что .NET выделяет 1 МБ для приложений, и что лучше всего избегать переполнения стека путем перекодирования вместо принудительного размера стека.

Я работаю над приложением "кратчайший путь", который отлично работает до 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>();

graph[] is:

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

Я попытался опимизировать код, чтобы он не нес больше багажа, чем требуется от одной итерации (рекурсии?) к следующему, но с графом 100K-Node с каждым узлом, имеющим между 1-9 ребрами, он довольно быстро достигнет этого предела 1 МБ.

в любом случае, я новичок в C# и оптимизации кода, если кто-то может дать мне некоторые указатели (не так) я был бы признателен он.

6 ответов


некоторое время назад я исследовал эту проблему в своем блоге. Или, скорее, я исследовал связанную проблему: как найти глубину двоичного дерева без использования рекурсии? Рекурсивное решение глубины дерева тривиально, но взрывает стек, если дерево сильно несбалансировано.

моя рекомендация-изучить способы решения этой более простой проблемы, а затем решить, какие из них, если таковые имеются, могут быть адаптированы к вашему немного более сложному алгоритму.

обратите внимание, что в этих статьях примеры приведены полностью в 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


классический метод, чтобы избежать глубоких рекурсивных погружений стека, - это просто избежать рекурсии, написав алгоритм итеративно и управляя собственным "стеком" с соответствующей структурой данных списка. Скорее всего, вам понадобится этот подход здесь, учитывая размер вашего входного набора.


вы можете преобразовать код в "рабочую очередь", а не рекурсивный. Что-то вроде следующего псевдокода:

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

Я знаю, что это загадочно, но это основная концепция того, что вам нужно делать. Поскольку вы получаете только 3000 узлов с вашим текущим кодом, вы в лучшем случае получите 12~15k без каких-либо параметров. Таким образом, вам нужно полностью убить рекурсию.


является ли ваш узел структурой или классом? Если это первый, сделайте его классом, чтобы он был выделен в куче, а не в стеке.


сначала я бы проверил, что вы действительно переполняете стек: вы действительно видите StackOverflowException получить брошенный средой выполнения.

Если это действительно так, у вас есть несколько вариантов:

  1. измените рекурсивную функцию, чтобы среда выполнения .NET могла преобразовать ее в хвост-рекурсивной функции.
  2. измените рекурсивную функцию так, чтобы она была итеративной и использовала пользовательскую структуру данных, а не управляемый стек.

Вариант 1 не всегда возможен и предполагает, что правила, используемые средой CLR для генерации хвостовых рекурсивных вызовов, останутся стабильными в будущем. Основное преимущество заключается в том, что, когда это возможно, хвостовая рекурсия на самом деле является удобным способом написания рекурсивных алгоритмов без ущерба для ясности.

Вариант 2 является более трудоемким, но не чувствительным к реализации среды CLR и может быть реализован для любого рекурсивного алгоритма (где tail рекурсия может быть не всегда возможна). Как правило, вам нужно захватить и передать информацию о состоянии между итерациями некоторого цикла вместе с информацией о том, как" развернуть " структуру данных, которая занимает места стека (обычно List или Stack). Один из способов развернуть рекурсию в итерацию-через продолжение прохождения узор.

дополнительные ресурсы на C# хвост рекурсии:

почему .NET / C# не оптимизируется для хвостового вызова рекурсия?

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


Я бы сначала удостоверился, что знаю, почему я получаю переполнение стека. Это действительно из-за рекурсии? Рекурсивный метод не ставит много в стек. Может быть, это из-за хранения узлов?

кроме того, кстати, я не вижу end параметр постоянно меняется. Это говорит о том, что он не должен быть параметром, переносимым на каждый кадр стека.