10

家系図と考えてみましょう。父親には子供がいます。子供には子供がいます。子供には子供がいます。つまり、父親が再帰を使用して子供を取得し、今のところは印刷するだけの再帰関数があります。出力ウィンドウをデバッグするには...しかし、ある時点で(1時間実行して、26000行のように出力した後)、StackOverFlowExceptionが発生します。

では、本当にメモリが不足しているのでしょうか。うーん?次に、「メモリ不足の例外」が発生するべきではありませんか? 他の投稿では、再帰呼び出しの数が多すぎると、SOF例外が発生する可能性があると人々が言っ​​ていることがわかりました...

とにかく、私の最初の考えは、ツリーを小さなサブツリーに分割することでした。ルートの父には常にこれらの5人の子供がいることを知っているので、ルートを渡してメソッドを1回呼び出す代わりに、OKと言いました。それをKidsofroot Passesで5回呼び出します。それは私が思うのに役立ちましたが、それでもそのうちの1つは非常に大きく(クラッシュすると26000行)、まだこの問題があります。

アプリケーションドメインと、実行時に特定のレベルの深さで新しいプロセスを作成するのはどうですか? それは役に立ちますか?

独自のスタックを作成し、再帰メソッドの代わりにそれを使用するのはどうですか?それは役に立ちますか?

ここにも私のコードの高レベルがあります。見てください。実際には、SOFエラーの原因となる何かばかげた問題があるかもしれません。

private void MyLoadMethod(string conceptCKI)
{

// make some script calls to DB, so that moTargetConceptList2 will have Concept-Relations for the current node. 

            // when this is zero, it means its a leaf. 
            int numberofKids = moTargetConceptList2.ConceptReltns.Count();
            if (numberofKids == 0)
                return;
            for (int i = 1; i <= numberofKids; i++)
            {
                oUCMRConceptReltn = moTargetConceptList2.ConceptReltns.get_ItemByIndex(i, false);

                //Get the concept linked to the relation concept
                if (oUCMRConceptReltn.SourceCKI == sConceptCKI)
                {
                    oConcept = moTargetConceptList2.ItemByKeyConceptCKI(oUCMRConceptReltn.TargetCKI, false);
                }
                else
                {
                    oConcept = moTargetConceptList2.ItemByKeyConceptCKI(oUCMRConceptReltn.SourceCKI, false);
                }

                //builder.AppendLine("\t" + oConcept.PrimaryCTerm.SourceString);
                Debug.WriteLine(oConcept.PrimaryCTerm.SourceString);

                MyLoadMethod(oConcept.ConceptCKI);
            }
        }
4

3 に答える 3

10

独自のスタックを作成し、再帰メソッドの代わりにそれを使用するのはどうですか?それは役に立ちますか?

はい!

これをインスタンス化すると、Stack<T>これはヒープ上に存在し、任意に大きくなる可能性があります(アドレス可能なメモリが不足するまで)。

再帰を使用する場合は、呼び出しスタックを使用します。コールスタックはヒープよりはるかに小さいです。デフォルトは、スレッドごとに1MBのコールスタックスペースです。これは変更できますが、お勧めできません。

于 2012-08-11T21:31:20.870 に答える
4

StackOverflowException は OutOfMemoryException とはまったく異なります。

OOME は、プロセスで使用できるメモリがまったくないことを意味します。これは、新しいスタックで新しいスレッドを作成しようとしたとき、またはヒープ上に新しいオブジェクトを作成しようとしたとき (およびその他のいくつかのケース) に発生する可能性があります。

SOE は、スレッドのスタックを意味します。デフォルトでは 1M ですが、スレッドの作成時または実行可能ファイルのデフォルトが異なる場合は、別の方法で設定できます。したがって、ASP.NET スレッドのデフォルトは 1M ではなく 256k で、使い果たされました。これは、メソッドの呼び出し時、またはローカルの割り当て時に発生する可能性があります。

関数 (メソッドまたはプロパティ) を呼び出すと、呼び出しの引数がスタックに置かれ、関数が戻るときに返されるアドレスがスタックに置かれ、実行は呼び出された関数にジャンプします。次に、いくつかのローカルがスタックに配置されます。関数が引き続き実行されると、さらにいくつかのものが配置される場合があります。stackallocまた、そうでなければヒープ割り当てが使用されるスタックスペースを明示的に使用します。

その後、別の関数を呼び出すと、同じことが再び起こります。次に、その関数が戻り、実行が格納された戻りアドレスに戻り、スタック内のポインターが元に戻り (スタックに配置された値をクリーンアップする必要はありません。現在は無視されます)、そのスペースが再び利用可能になります。 .

その 1M のスペースを使い切ると、StackOverflowException. 1M (または 256k) は、このような使用には大量のメモリであるため (実際には大きなオブジェクトをスタックに入れません)、SOE を引き起こす可能性のある 3 つのことは次のとおりです。

  1. stackallocそうでないときに使用して最適化するのは良い考えだと誰かが考え、その 1M をすぐに使い果たしました。
  2. スタックが通常よりも小さいスレッドを作成して最適化するのは良い考えだと誰かが考え、その小さなスタックを使い果たしました。
  3. 再帰的 (直接または複数のステップを介して) 呼び出しは、無限ループに陥ります。
  4. 無限ではありませんでしたが、十分な大きさでした。

ケース 4 があります。1 と 2 は非常にまれです (これらのリスクを冒すには、慎重に検討する必要があります)。ケース 3 は群を抜いて最も一般的であり、再帰が無限であってはならないというバグを示していますが、間違いはそうであることを意味します。

皮肉なことに、この場合、反復的ではなく再帰的アプローチを採用したことを喜んでいるはずです。SOE はバグとその場所を明らかにしますが、反復的アプローチではおそらく無限ループが発生してすべてが停止する可能性があります。見つけにくくなります。

ケース 4 では、2 つのオプションがあります。非常にまれなケースですが、呼び出しがわずかに多すぎる場合は、より大きなスタックを持つスレッドで実行できます。これはあなたには当てはまりません。

代わりに、再帰的なアプローチから反復的なアプローチに変更する必要があります。ほとんどの場合、これは非常に難しいことではありません。メソッドは自分自身を再度呼び出す代わりに、ループを使用します。たとえば、階乗法の古典的な教えの例を考えてみましょう。

private static int Fac(int n)
{
  return n <= 1 ? 1 : n * Fac(n - 1);
}

再帰を使用する代わりに、同じメソッドでループします。

private static int Fac(int n)
{
  int ret = 1;
  for(int i = 1; i <= n, ++i)
    ret *= i;
  return ret;
}

ここでスタックスペースが少ない理由がわかります。反復バージョンは、99% の確率で高速になります。ここで、誤っFac(n)て最初に を呼び出し++i、2 番目に を省略したとします。それぞれに同等のバグがあり、最初に SOE が発生し、2 番目に停止しないプログラムが発生します。

あなたが話しているようなコードの場合、以前の結果に基づいて進むにつれてますます多くの結果を生成し続ける場合、取得した結果をデータ構造に配置できます(両方Queue<T> ともStack<T>多くの場合にうまく機能します)ケース) したがって、コードは次のようになります):

private void MyLoadMethod(string firstConceptCKI)
{
  Queue<string> pendingItems = new Queue<string>();
  pendingItems.Enqueue(firstConceptCKI);
  while(pendingItems.Count != 0)
  {
    string conceptCKI = pendingItems.Dequeue();
    // make some script calls to DB, so that moTargetConceptList2 will have Concept-Relations for the current node. 
    // when this is zero, it means its a leaf. 
    int numberofKids = moTargetConceptList2.ConceptReltns.Count();
    for (int i = 1; i <= numberofKids; i++)
    {
        oUCMRConceptReltn = moTargetConceptList2.ConceptReltns.get_ItemByIndex(i, false);

        //Get the concept linked to the relation concept
        if (oUCMRConceptReltn.SourceCKI == sConceptCKI)
        {
            oConcept = moTargetConceptList2.ItemByKeyConceptCKI(oUCMRConceptReltn.TargetCKI, false);
        }
        else
        {
            oConcept = moTargetConceptList2.ItemByKeyConceptCKI(oUCMRConceptReltn.SourceCKI, false);
        }

        //builder.AppendLine("\t" + oConcept.PrimaryCTerm.SourceString);
        Debug.WriteLine(oConcept.PrimaryCTerm.SourceString);

        pendingItems.Enque(oConcept.ConceptCKI);
    }
  }
}

(私はこれを完全にはチェックしていません。質問のコードに再帰する代わりにキューイングを追加しただけです)。

これは、コードとほぼ同じですが、繰り返し実行する必要があります。うまくいけば、それはうまくいくことを意味します。取得するデータにループがある場合、このコードには無限ループが発生する可能性があることに注意してください。その場合、処理しきれないほど多くのものでキューがいっぱいになると、このコードは例外をスローします。ソース データをデバッグするか、 を使用しHashSetて、既に処理されたアイテムをキューに入れないようにすることができます。

編集: HashSet を使用して重複をキャッチする方法を追加することをお勧めします。最初に HashSet を設定します。これは次のようになります。

HashSet<string> seen = new HashSet<string>();

または、文字列が大文字と小文字を区別せずに使用されている場合は、次のようにしたほうがよいでしょう:

HashSet<string> seen = new HashSet<string>(StringComparison.InvariantCultureIgnoreCase) // or StringComparison.CurrentCultureIgnoreCase if that's closer to how the string is used in the rest of the code.

次に、文字列を使用する前に (または、おそらくキューに追加する前に)、次のいずれかを行います。

文字列の重複が発生しない場合:

if(!seen.Add(conceptCKI))
  throw new InvalidOperationException("Attempt to use \" + conceptCKI + "\" which was already seen.");

または、重複する文字列が有効で、2 番目の呼び出しの実行をスキップしたい場合:

if(!seen.Add(conceptCKI))
  continue;//skip rest of loop, and move on to the next one.
于 2012-08-11T22:58:44.787 に答える
2

本当にスタック オーバーフロー エラーではなく、再帰リング (無限再帰) があると思います。スタック用のメモリが多い場合は、オーバーフロー エラーも発生します。

テストするには:

操作可能なオブジェクトを格納するためのグローバル変数を宣言します。

private Dictionary<int,object> _operableIds = new Dictionary<int,object>();

...
private void Start()
{
    _operableIds.Clear();
    Recurtion(start_id);
}
...
private void Recurtion(int object_id)
{
   if(_operableIds.ContainsKey(object_id))
      throw new Exception("Have a ring!");
   else
     _operableIds.Add(object_id, null/*or object*/);

   ...
   Recurtion(other_id)
   ...
   _operableIds.Remove(object_id);
}
于 2012-08-11T21:55:24.043 に答える