25

昔から、変数 (値型または参照型) がどこに格納されるかを正確に知りたいと思っていました。スタックまたはヒープに配置されますか?

同じことに関するEric Lippert の記事を読みました。

好奇心から、私が望んでいたのは、私が理解したことを同じように相互検証することです。同じためのツールはありますか?または、.NET プログラムが実行されている間に、どの変数がスタックに格納されるかを知るようになりますか? そして、どれがヒープに保存されますか?

ありがとう

4

1 に答える 1

72

ストレージがスタックとヒープで分割されていると考えるのは便利な抽象化であり、うまく機能します。しかし、それはもっと複雑で、.NET プログラムの変数には 6 つの異なる格納場所があります。

ここで選択するツールはデバッガーです。変数が格納されている場所を正確に表示できます。それには、マシンコードがどのように機能するかについての洞察が必要です。Debug + Windows + Disassembly を使用して、マシン コードを確認します。また、プログラムのリリース ビルドを確認し、デバッグ時にもコードが最適化されるように設定を変更することも重要です。[ツール] + [オプション]、[デバッグ]、[全般] で、[モジュールの読み込み時に JIT 最適化を抑制する] オプションのチェックを外します。これで、マシン コードがユーザーのマシンで実行される方法が表示されます。

すべてを理解するために事前に知っておくべきこと:

  • 参照型のオブジェクトは GC ヒープに格納されます。参照を格納する変数には、値型の値と同じ種類のストレージの選択肢があります。

  • 値型の値またはオブジェクト参照には、次の 6 つの保存場所があります。

    • 変数が参照型のメンバーである場合、それらは GC ヒープに格納されます。
    • 変数が静的であると宣言されている場合、それらは AppDomain のローダー ヒープに格納されます。
    • 変数が [ThreadStatic] の場合、スレッド ローカル ストレージに格納されます。
    • 変数がメソッド引数またはローカル変数の場合、スタック フレームに格納できます。
    • 変数がメソッド引数またはローカル変数の場合、CPU レジスタに格納できます。
    • x86 ジッターに固有の、Single または Double 型の変数を FPU スタックに格納できます。

後半の 3 つの箇条書きは、複雑になる箇所であり、マシン コードを調べて格納場所を調べる必要がある理由です。これは実装固有であり、ジッターの種類が重要です。また、ジッター オプティマイザーを有効にしているかどうかによって大きく影響を受けます。ここで適切な選択を行うことは、パフォーマンスにとって非常に重要です。大まかな概要 (ARM ジッターをスキップ):

  • 最初の 2 つのメソッド引数は、x86 ジッターの CPU レジスタに格納され、インスタンス メソッドの値が含まます。x64 ジッタは 4 つのレジスタを使用します。浮動小数点プロセッサ レジスタは、x86 では Single および Double 型の変数を渡すために使用され、x64 では XMM レジスタが使用されます。

  • 関数の戻り値は、適合する場合は EAX または RAX レジスタを使用して CPU レジスタに返されます。浮動小数点値の場合は ST0 です。収まらない場合、呼び出し元は値用にスタック フレームのスペースを予約し、その値へのポインターを渡しました。

  • ジッタ オプティマイザは、ローカル変数を CPU レジスタに格納する機会を探します。レジスターが不足しているために強制的にレジスターをスタック フレームにスピルバックする場合があります。

これらの実装の詳細には、いくつかの観察可能な副作用があります。

  • CPU レジスタに格納されたローカル変数を取得すると、コードのデバッグが困難になります。デバッガーは、ストレージの場所について十分に認識していません。これがデバッグ ビルドが存在する主な理由です。最適化が抑制されるため、ローカル変数を簡単に調べることができます。デバッガーは、変数に使用されるスタック フレーム スロットを認識します。
  • メソッドの戻り値を検査することはできず、デバッグ中は非常に不便です。デバッガーは、ジッターによって選択されたストレージの場所について十分に認識していないため、値を確実に見つけることができません。編集:VS2013で修正
  • 変数が CPU レジスタに格納されるように最適化されているため、スレッドの問題をデバッグするのが難しくなる可能性があります。ループまたは if() ステートメントで値をテストすると、メモリに格納されている値ではなく、レジスタ内の値のコピーが生成されます。特に x86 ジッターの問題と、この最適化を抑制するキーワードであるvolatileキーワードの理由
  • 固定する必要なく、ローカル変数へのポインターを初期化できます。ガベージ コレクションによって移動される可能性があり、固定が必要な GC ヒープに格納されている変数とは異なり、ローカル変数には、メソッド本体全体で有効な固定ストレージ アドレスがあります。
  • スタック フレームに割り当てられるスペースの量は、ジッターによって決まります。ただし、自分でチャンクを割り当てることは可能です。C# のstackallocキーワードでサポートされています。これは、直接割り当てることができる最速のメモリです
  • 浮動小数点値を FPU レジスタに格納すると、浮動小数点の精度の問題が発生します。FPU に格納される間、値は 80 ビット精度で格納されます。ただし、メモリにスピルされると、32 ビットまたは 64 ビットの精度に切り捨てられます。このスピルが発生するタイミングの予測不可能性 (および x64 ジッターの異なる戦略) により、計算で多くの有効桁数が失われると、小さな変更で計算結果に大きな違いが生じる可能性がある浮動小数点の結果が生成されます。
于 2012-12-24T16:33:06.380 に答える