7

私はこの質問に対する答えを数時間ウェブとこのサイトで見つけようとしてきましたが、まだ答えは出ていません。

.NET がアプリに 1 MB を割り当てること、およびスタック サイズを強制するのではなく、再コーディングしてスタック オーバーフローを回避することが最善であることを理解しています。

私は、約 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 クラスには 1 つのメンバーがあります。

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

グラフ[]は次のとおりです。

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

ある反復 (再帰?) から次の反復まで必要以上の荷物を運ばないようにコードを最適化しようとしましたが、各ノードが 1 ~ 9 個のエッジを持つ 100K ノード グラフを使用すると、次のようになります。すぐに 1MB の制限に達します。

とにかく、私はC#とコードの最適化が初めてです。誰かが私にいくつかのポインタを与えることができれば(これは好きではありません)、私はそれを感謝します。

4

6 に答える 6

16

深い再帰的なスタックダイブを回避するための古典的な手法は、アルゴリズムを繰り返し記述し、適切なリストデータ構造を使用して独自の「スタック」を管理することにより、再帰を回避することです。入力セットのサイズが非常に大きいことを考えると、ここでこのアプローチが必要になる可能性があります。

于 2009-10-05T17:07:52.333 に答える
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

于 2009-10-05T17:50:52.170 に答える
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 に到達します。したがって、再帰を完全に殺す必要があります。

于 2009-10-05T17:28:28.580 に答える
3

ノードは構造体ですか、それともクラスですか? 前者の場合は、スタックではなくヒープに配置されるようにクラス化してください。

于 2009-10-05T17:00:09.170 に答える
1

最初に、実際にスタックがオーバーフローしていることを確認します。実際には、ランタイムによってStackOverflowExceptionがスローされることがわかります。

これが実際に当てはまる場合は、いくつかのオプションがあります。

  1. 再帰関数を変更して、.NET ランタイムが末尾再帰関数に変換できるようにします。
  2. 再帰関数を変更して、反復し、マネージド スタックではなくカスタム データ構造を使用するようにします。

オプション 1 は常に可能であるとは限らず、CLR が末尾再帰呼び出しを生成するために使用するルールが将来も安定していると想定しています。主な利点は、可能な場合、末尾再帰が、明確さを犠牲にすることなく再帰アルゴリズムを記述する便利な方法であることです。

オプション 2 はより手間がかかりますが、CLR の実装に依存せず、任意の再帰アルゴリズムに実装できます (末尾再帰が常に可能であるとは限りません)。一般に、スタックの代わりになるデータ構造 (通常は List<> または Stack<>) を「展開」する方法に関する情報とともに、ループの繰り返しの間に状態情報を取得して渡す必要があります。再帰を反復に展開する 1 つの方法は、継続渡しパターンを使用することです。

C# 末尾再帰に関するその他のリソース:

.NET/C# が末尾呼び出し再帰を最適化しないのはなぜですか?

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

于 2009-10-05T17:52:23.417 に答える
0

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

また、ところで、endパラメーターが変更されることはありません。これは、各スタック フレームで実行されるパラメーターである必要がないことを示唆しています。

于 2009-10-05T17:35:25.663 に答える