2009-10-05 24 views
7

私はこの質問に対する回答をWeb上とこのサイトで数時間確認しようとしています。そこ。スタックサイズの変更を避け、C#でスタックオーバーフローを避けるには

.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>(); 

私はopimizeしようとしました1回の反復(再帰?)から次の手続きまで必要以上に手荷物が運ばれないようにしますが、各ノードが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命令を生成しません。特定のバージョンのジッタは、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

私はまず、スタックオーバーフローが発生している理由を知っていることを確認します。それは実際に再帰のためですか?再帰的な方法は、スタックに多くを置くことはありません。おそらくそれはノードのストレージのためですか?

また、私はendのパラメータも変更されていません。つまり、スタックフレームごとにパラメータを指定する必要はありません。

1

は、私が最初にあなたが実際にスタックをオーバーフローしていることを確認します:あなたが実際にStackOverflowExceptionがランタイムによってスローされます参照してください。

これは確かにそうであるならば、あなたはいくつかのオプションがあります。

  1. を.NETランタイムがtail-recursive functionに変換できるように、再帰関数を変更します。
  2. 反復的で、管理対象スタックではなくカスタムデータ構造を使用するように、再帰関数を変更します。

オプション1は必ずしも可能ではなく、CLRが末尾再帰呼び出しを生成するために使用するルールは今後も安定していると想定しています。主な利点は、可能であれば、テール再帰は、実際には明快さを犠牲にすることなく再帰アルゴリズムを書く便利な方法であるということです。

オプション2はより多くの作業ですが、CLRの実装には敏感ではなく、任意の再帰アルゴリズム(テール再帰は常に可能ではない可能性があります)に対して実装できます。一般的には、スタックの場所を取るデータ構造(通常はList <>またはStack <>)を「アンロール」する方法に関する情報とともに、ループの繰り返しの間に状態情報を取得して渡す必要があります。再帰を反復にアンロールする方法の1つは、continuation passingパターンです。C#の末尾再帰に

その他のリソース:

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

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

関連する問題