2009-10-05 3 views
7

웹 및이 사이트에서 몇 시간 동안이 질문에 대한 답변을 찾으려고 노력 중입니다. 그곳에.스택 크기 변경을 피하고 C#에서 스택 오버플로가 발생하지 않도록하려면 어떻게해야합니까?

.NET은 앱에 1MB를 할당하며 스택 크기를 강요하는 대신 다시 코딩하여 스택 오버플로를 방지하는 것이 가장 좋습니다.

나는 약 3000 개의 노드까지 훌륭하게 작동하는 "최단 경로"응용 프로그램을 만들고 있습니다.이 응용 프로그램은 오버플로가됩니다. 여기서 문제가 발생시키는 방법이다 :

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

그래프 []이고 : I는를 opimize 시도한

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

참고로

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 클래스는 하나의 부재를 가지고 코드를 사용하여 하나의 반복 (재귀?)에서 필요한 것보다 더 많은 수하물을 운반 할 수 없지만 각 노드의 1-9 가장자리 사이에 100K 노드 그래프를 사용하면 1MB 제한에 도달하는 데 걸리는 시간이 매우 단축됩니다.

어쨌든 누구든지 내게 포인터 (not like this)를 주면 C# 및 코드 최적화에 익숙하지 않아서 고맙겠습니다.

답변

9

나는 내 블로그에서이 문제를 탐구했다. 또는 재귀를 사용하지 않고 이진 트리의 깊이를 어떻게 찾을 수 있습니까? 재귀 트리 깊이 솔루션은 간단하지만 트리의 불균형이 심하면 스택을 불어납니다.

이 간단한 문제를 해결하는 방법을 연구하고 약간 더 복잡한 알고리즘에 적용 할 수있는 알고리즘이 있는지 결정하십시오.

이 기사에서 예제는 전적으로 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

+1

또는 CLR을 사용하여 알고리즘을 꼬리 재귀 형식으로 변환 할 수 있는지 확인할 수 있습니다. – LBushkin

+4

C#이 꼬리 호출 명령을 생성하지 않습니다. 특정 버전의 지터는 tailcall 명령어가 사용되지 않아도 꼬리 재귀를 사용하여 특정 메소드를 최적화 할 수 있음을 알 수 있습니다. 그러나 우리가 발표 한 대부분의 불안감에는이 최적화가 없으므로이 최적화에 의존해서는 안됩니다. –

+3

게다가, 당신의 충고는 실제로 실행 가능하지 않습니다. * 원래 포스터는 "알고리즘을 꼬리 재귀 양식으로 변환 할 수 있는지 확인"하는 방법이 정확히 무엇입니까? 이것은 런타임의 구현 세부 사항에 대한 깊은 이해를 필요로하는 복잡한 프로세스입니다. 빌딩 42에있는 사람 이외에 다른 사람이 소유 할 것이라고는 예상하지 않습니다. –

3

노드가 구조체입니까, 클래스입니까? 그것이 전자의 경우, 스택 대신에 힙에 할당되도록 클래스로 만드십시오.

+3

할 것 모두를 제거하지, 스택 사용을 작게 만들 수 있습니다. 대규모 데이터 구조에 대한 심층적 인 재귀 문제는 여전히 존재할 것입니다. –

+0

사실입니다. 처음에는 그 수를 단지 3000으로 보았고 1MB의 스택 공간을 고려할 때 다소 작다고 생각했지만, 수학을한다면 꽤 합리적인 것처럼 보입니다. –

16

심도있는 재귀 적 스택 다이브를 피하는 고전적인 기법은 알고리즘을 반복적으로 쓰고 적절한 목록 데이터 구조로 자신의 "스택"을 관리함으로써 재귀를 피하는 것입니다. 입력 집합의 크기가 크기 때문에이 방법이 필요합니다.

+0

또는 꼬리 재귀를 사용하여 재귀 호출을 최적화 할 수 있는지 확인하십시오. – LBushkin

7

재귀가 아닌 '작업 대기열'을 사용하도록 코드를 변환 할 수 있습니다. 다음 의사 코드를 따라 무엇인가 :

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

나는 그것이 알고있는 것이지만 당신이해야 할 일의 기본 개념이다. 현재 코드로 3000 노드 만 얻으므로 매개 변수없이 12 ~ 15k가 될 것입니다. 따라서 재귀를 완전히 제거해야합니다.

+0

좋은 지적. 사실, 코드는 본질적으로 OP에서 깊이 첫 번째 접근 방식에 반대하는 노드의 광범위한 첫 탐색입니다. –

0

먼저 스택 오버플로가 발생하는 이유를 알고 있는지 확인해야합니다. 그것은 실제로 재귀 때문입니까? 재귀 적 방법은 스택에 많은 것을 넣지 않습니다. 어쩌면 노드를 저장했기 때문일 수도 있습니다.

또한 BTW, end 매개 변수가 계속 변경되는 것을 볼 수 없습니다. 이것은 각 스택 프레임에 매개 변수가 될 필요가 없다는 것을 의미합니다.

1

은 내가 먼저 당신이 실제로 스택 오버 플로우되어 있는지 확인합니다 : 당신이 실제로 StackOverflowException가 런타임에 의해 던져 참조하십시오. .NET 런타임은 tail-recursive function로 변환 할 수 있도록

  1. 이 재귀 함수를 수정 :이 실제로 경우라면

    , 당신은 몇 가지 옵션이 있습니다.

  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

관련 문제