月の裏側

アプリケーションを作成する際の最も重要な問題の1つは、メモリ消費と応答性(速度)です。



ガベージコレクターは、作業を予測できないブラックボックスであると考えられています。



また、.NETのGCは実際にはカスタマイズできないと言っています。 また、.NET FrameworkクラスとCLR、GCなどの両方のソースコードを見ることができません。



そして、私はどのように言っても!



この記事では、次のことを検討します。





objectsオブジェクトをメモリに配置するための組織構造



CLRオブジェクトのサイズ変更について既に書いたことがあります。 記事を再度語らないために、要点を思い出してみましょう。



変数参照型の場合、CIL命令newobjまたはたとえばC#のnew演算子を使用すると、固定サイズ値がスタックに配置されます(x86の場合は4バイト、DWORD型など)。通常のヒープに作成されたオブジェクトインスタンスのアドレスが含まれます(忘れないでください)マネージヒープは、Small Object HeapとLarge Object Heapに分けられます-詳細については、GCの段落で後述します)。 そのため、C ++ではこの値はオブジェクトへのポインターと呼ばれ、.NETの世界ではオブジェクトへの参照と呼ばれます。



リンクは、メソッドの実行時にスタック上に存在するか、クラスのフィールド内に存在します。



リンクを作成せずにバキュームでオブジェクトを作成することはできません。

オブジェクトのサイズに関する推測がなく、 SOS(Son of Strike) 、GC.TotalMemoryの測定値などを使用してテストを実行します。 -CLRソース、またはむしろ共有ソース共通言語インフラストラクチャ2.0を見てください 。これは一種の研究プロジェクトです。



各タイプには独自のMethodTableがあり、同じタイプのオブジェクトのすべてのインスタンスは同じMethodTableを参照します。 このテーブルには、型自体に関する情報(インターフェイス、抽象クラスなど)が格納されます。



各オブジェクトには、 SyncTableEntryアドレス(syncblkエントリ)を格納するオブジェクトヘッダーとメソッドテーブルポインター(TypeHandle)の2つの追加フィールドが含まれます。



SyncTableEntry -CLRオブジェクトへのリンクとSyncBlock自体へのリンクを格納する構造。



SyncBlockは、オブジェクトのハッシュコードを格納するデータ構造です。



「誰にでも」とは、CLRが特定の数のSyncBlockを事前に初期化することを意味します。 さらに、GetHashCode()またはMonitor.Enter()を呼び出すと、環境はオブジェクトのヘッダーに完成したSyncBlockへのポインターを挿入し、途中でハッシュコードを計算します。



これを行うには、 GetSyncBlockメソッドを呼び出します( % %\sscli20\clr\src\vm\syncblk.cpp)



ファイル% %\sscli20\clr\src\vm\syncblk.cpp)



を確認します% %\sscli20\clr\src\vm\syncblk.cpp)



。 メソッドの本体では、次のコードを見ることができます。



 else if ((bits & BIT_SBLK_IS_HASHCODE) != 0) { DWORD hashCode = bits & MASK_HASHCODE; syncBlock->SetHashCode(hashCode); }
      
      







System.Object.GetHashCodeメソッドは、 SyncBlock :: GetHashCodeメソッドを呼び出すことにより、SyncBlock構造に依存します。



CLR 2.0の初期syncblk値は0ですが、CLR 4.0以降では値は-1です。



Monitor.Exit()を呼び出すと、syncblkは再び-1になります。



また、SyncBlocksの配列は別のメモリに格納されており、 GCにはアクセスできないことに注意してください。



どうして? お願いします。



答えは簡単です-弱いリンク。 CLRは、SyncBlock配列に弱い書き込みリンクを作成します。 CLRオブジェクトが消滅すると、SyncBlockが更新されます。



Monitor.Enter()メソッドの実装は、プラットフォームとJIT自体に依存します。 したがって、SSCLIソースのこのメソッドのエイリアスはJIT_MonEnterです。



オブジェクトをメモリに配置するというトピックとそのサイズに戻ると、オブジェクトのインスタンス(空のクラス)はx86で少なくとも12バイト必要であり、x64ではすでに24バイトあることを思い出してください。



SOSを起動せずにこれを確認します。



ファイル% %\sscli20\clr\src\vm\object.h







 #define MIN_OBJECT_SIZE (2*sizeof(BYTE*) + sizeof(ObjHeader)) class Object { protected: MethodTable* m_pMethTab; }; class ObjHeader { private: DWORD m_SyncBlockValue; // the Index and the Bits };
      
      







CLRオブジェクトのサイズに関するその記事のコメントで、証拠なしにSystem.Stringのサイズを計算することの不正確さについて非難されました。



しかし、私は数字と...ソースコードをもっと信頼しています!



.NET 4.0のSystem.Stringは、次のメンバーで構成されています。



画像



Emptyを考慮しません。なぜなら、 これは空の静的文字列です。

m_stringLengthは、文字列の長さを示します。



m_firstCharは、Unicode文字の配列のストレージの先頭へのポインター (!!!)であり、配列の最初の文字ではありません



ここでは魔法は使われません-CLRはオフセットを見つけるだけです。



これを確認するには、archive%\ sscli20 \ clr \ src \ vm \ object.hでfile%フォルダーを再度開きます



ファイルの最初に、コードに関するコメントがあります。



 /* * StringObject - String objects are specialized objects for string * storage/retrieval for higher performance */
      
      







これは、文字列データを格納する内部構造です。



次に、 StringObjectクラスとそのGetBuffer()メソッドを見つけます。



 WCHAR* GetBuffer() { LEAF_CONTRACT; _ASSERTE(this); return (WCHAR*)( PTR_HOST_TO_TADDR(this) + offsetof(StringObject, m_Characters) ); }
      
      







さて、バッファ(文字の配列)は、オフセットによって単純に計算されます。



しかし、System.String自体はどうですか?



archive%\ sscli20 \ clr \ src \ bcl \ system \ string.csでfile%フォルダーを開きます



次の行が表示されます。



 //NOTE NOTE NOTE NOTE //These fields map directly onto the fields in an EE StringObject. See object.h for the layout. // [NonSerialized]private int m_stringLength; [NonSerialized]private char m_firstChar;
      
      







ただし、System.Stringは、その作業において、コンストラクター自体と多くのメソッド(PadLeftなど)を実装するCOMStringに依存しています。

フレームワークのメソッド名と内部C ++実装のメソッド名を正しく一致させるに% %\sscli20\clr\src\vm\ecall.cpp



ファイル% %\sscli20\clr\src\vm\ecall.cpp



を確認することをお勧めします

さて、m_firstCharがポインターであることを最終的に確認するために、たとえばJoinメソッドコードの一部を検討してください。



 fixed (char* ptr = &text.m_firstChar) { UnSafeCharBuffer unSafeCharBuffer = new UnSafeCharBuffer(ptr, num); unSafeCharBuffer.AppendString(value[startIndex]); for (int j = startIndex + 1; j <= num2; j++) { unSafeCharBuffer.AppendString(separator); unSafeCharBuffer.AppendString(value[j]); } }
      
      







計算がわずかに異なるバージョン (ただし同じ結果 )が有名なJon Skeetを導きます。



先に進む前に、スタックについて思い出したいと思います。

スタックは、メソッドが呼び出されるたびに環境によって作成されるコンテナーです。 呼び出しを完了するために必要なすべてのデータ(ローカル変数、パラメーターなどのアドレス)を保存します。



したがって、呼び出しツリーの呼び出しは、スタックで構成されるFIFOコンテナーです。 現在のメソッドの呼び出しが終了すると、スタックがクリアされて破棄され、親ブランチに制御が戻ります。



上記で書いたように、変数参照型の場合、通常のヒープで作成されたオブジェクトのインスタンスのアドレスを含む固定サイズの値(x86の場合は4バイト、DWORD型など)がスタックにプッシュされます。



デフォルトでは、ボクシングに関係しないプリミティブ型のインスタンスがスタックに配置されます。



ただし、いくつかの最適化により、JIT-はRAMをバイパスして、変数の値をプロセッサレジスタにすぐに配置できます。



このようなプロセッサレジスタは、プロセッサ内部で超高速RAMメモリを形成するメモリセルのブロックであり、プロセッサ自体によって使用され、ほとんどの場合プログラマがアクセスできないことを思い出してください。



CPUキャッシュが大きいほど、ソフトウェアプラットフォームに関係なく、より高いパフォーマンスを得ることができます。



▌GCデバイス





ご存じのように、メモリ管理(オブジェクトの作成と破棄)は、ガベージコレクタ(別名Garbage Collector(GC))によって処理されます。



アプリケーションが機能するために、CLRは仮想アドレス空間の2つのセグメント(スモールオブジェクトヒープとラージオブジェクトヒープ)を直ちに初期化します。



簡単なメモ:仮想メモリは、物理的ではなくメモリの論理的表現です。 物理メモリは、必要な場合にのみ割り当てられます。 最新のオペレーティングシステムの各プロセスには、ページネーションが可能な最大アドレス可能サイズ(32ビットOSの場合は4GB)の仮想アドレススペースが割り当てられます(x86、IA-64、PowerPC-64プラットフォームの場合、最小サイズは4KB、SPARC-8KB)。 これにより、あるプロセスのアドレス空間を別のプロセスから分離することが可能になり、ディスク上でスワップを使用することも可能になります。



メモリシステムに割り当てて戻すために、GCはWin32関数VirtualAllocおよびVirtualFreeを使用します。



.NETのガベージコレクターは世代を超えています。つまり、 管理ヒープ(それぞれオブジェクト)は世代に分けられます。 すべてのオブジェクトは、ライフサイクルを通じて複数の世代に分割されます。



オブジェクト参照のソースは、いわゆるGCルートです。



この場合、合計で3つの世代があります。



各セグメントの初期サイズ(SOH、LOH)は異なり、特定のマシン(通常はデスクトップ用に16 MB、サーバー用に64 MB)に依存します。 これは仮想メモリであることに注意してください。アプリケーションは一般に5 MBの物理メモリを占有できます。



85,000バイトを超えるオブジェクトだけでなく、一部のタイプの配列もLOHに入ります。



そのため、10.600要素(85000/8バイト)のサイズを持つSystem.Doubleの配列はLOHに分類されるはずです。 ただし、これはすでに1000+のサイズで発生しています。



マネージヒープ内のオブジェクトは次々に配置されます。これにより、多数のオブジェクトが削除されると、断片化が発生する可能性があります。



ただし、この問題を解決するために、CLRは常に (手動のメモリ管理を除き)Small Object Heapを最適化します。



プロセスは次のとおりです。現在のオブジェクトが空きメモリ(自動的に消えるヒープ内のスペース)にコピーされます。



したがって、最小のメモリ消費が達成されますが、これには一定のプロセッサ時間が必要です。 ただし、これは心配しないでください。 Gen0、Gen1オブジェクトの場合、遅延はわずか1 msです。



ラージオブジェクトヒープはどうですか? 最適化されることはありません(ほとんどありません)。 これには時間がかかり、アプリケーションのパフォーマンスに影響を与える可能性があります。 ただし、これは、CLRが理由もなくメモリを消費し始めているという意味ではありません。 Full-GC(Gen0、Gen1、Gen2)の間、システムはまだOSメモリを返し、LOHからすでに死んでいる(またはSOHを最適化する)オブジェクトから自身を解放します。



また、CLRは、たとえばSOHのようにLOHに新しいオブジェクトを次々に配置するだけでなく、Full-GCを待たずに空きメモリの場所にも配置します。

GC.Collect()メソッドの呼び出しを除き、GCの起動は決定的ではありません。



ただし、これを予測できるおおよその基準がまだあります(以下に示す条件はおおよそのものであり、CLR自体がアプリケーションの動作に適応することを覚えておく必要があります。ガベージコレクターの種類によって異なります)。



ガベージコレクションは、システムのメモリが不足したときにも開始されます。 CLRは、このためにWin32関数CreateMemoryResourceNotificationおよびQueryMemoryResourceNotificationを使用します。



メモリを操作する際のもう1つのポイントは、管理されていないリソースの使用です。



なぜなら アンマネージリソースには、その寿命に関係なく任意のオブジェクトを含めることができ、GCは決定論的ではないため、これらの目的のためのファイナライザーがあります。



アプリケーションが起動すると、CLRはファイナライザーで型を検索し、通常のガベージコレクションからそれらを除外します(ただし、これはオブジェクトが世代に関連付けられていないという意味ではありません)。



GCが終了すると、ファイナライズされたオブジェクトは別のスレッドで処理されます(Finalizeメソッドを呼び出します)。



Disposeパターンの実装例:



 class Foo : IDisposable { private bool _disposed; ~Foo() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { // Free managed objects } // Free unmanaged objects _disposed = true; } } }
      
      







次に、.NET Frameworkで利用可能なGC自体を検討します。



.NET 4.0より前は、2つのサーバーモードとワークステーションモードが使用可能でした。



ワークステーションモード-GCはクライアントマシンで動作するように最適化されています。 彼はプロセッサをあまりロードしないようにし、UIを備えたアプリケーションの遅延を最小限に抑えて作業します。 パラレルと同期の2つのモードで使用できます。



並列モードでは、GCはGen2世代の別のスレッドで(通常の優先順位で)起動されますが、一時的な世代の作業はブロックされます(新しいオブジェクトの割り当ては不可能で、すべてのスレッドが中断されます)。



アプリケーションの空きメモリが非常に多い(!!!)場合、SOHはコンパクトになりません(GCはアプリケーションの応答性のためにメモリを犠牲にします)。



したがって、ワークステーションモードはGUIアプリケーションに最適です。



さらに、サーバー側GCを使用する必要がある場合は、次のように有効にすることができます。



 <configuration> <runtime> <gcServer enabled="true"/> </runtime> </configuration>
      
      







検証のために、コードでGCSettings.IsServerGCプロパティを使用できます。



Workstation Concurrent GCを強制的にシャットダウンするには、次のパラメーターを使用します。



 <configuration> <runtime> <gcConcurrent enabled="false"/> </runtime> </configuration>
      
      







デフォルトでは、ワークステーションGCのパラレルモードが有効になっています。 ただし、プロセッサがシングルコアの場合、GCは自動的に同期モードになります。



サーバーGCを検討してください。



サーバーGCは、マネージヒープをセグメントに分割します。セグメントの数は論理プロセッサの数に等しく、1つのスレッドを使用して各セグメントを処理します。



簡単なメモ:論理プロセッサは必ずしも物理プロセッサに対応するとは限りません。 複数の物理プロセッサ(つまり、複数のソケット)とマルチコアプロセッサを備えたシステムは、OSに多くの論理プロセッサを提供します。また、カーネル(!!!)は、複数の論理プロセッサにすることもできます(たとえば、Intelハイパースレッディングテクノロジを使用する場合)。



また、主な違いの1つは、.NET Framework 3.5 SP1(3つのモードで構成される)で使用可能なGCSettings.LatencyModeプロパティです。



デフォルトでは、Workstation Concurrent GCのLatencyModeはInteractive、Server-Batchに設定されています。



LowLatencyもありますが、それを使用するとOutOfMemoryExceptionが発生する可能性があります。 このGCモードでは、フルガベージコレクションは、メモリ負荷が高い場合にのみ発生します。 また、サーバーGCに対して有効にすることはできません。



バッチとインタラクティブの違いは何ですか?



なぜなら サーバーGCは、マネージヒープを複数のセグメント(各セグメントが個別の論理プロセッサにサービスする)に分割し、並列ガベージコレクションの必要性がなくなります(別の論理プロセッサで別のスレッドが起動された場合)。 このモードは、gcConcurrentパラメーターを強制的にオーバーライドします。 gcConcurrentモードが有効になっている場合、バッチモードでは、ガベージコレクションが並行して行われなくなります(!!!)。 バッチは、ワークステーションでの非並列ガベージコレクションと同等です。 このモードを使用する場合、大量(!!!)のデータの処理が特徴的です。



GCLatencyModeの値を変更すると、現在実行中のスレッドに影響することに注意してください。つまり、ランタイム自体とアンマネージコードに影響します。



そして以来 スレッドはさまざまな論理プロセッサで実行できるため、GCモードの即時変換の保証はありません。

しかし、別のスレッドがこの値を変更したい場合はどうでしょう。 そして、100のスレッドがある場合はどうなりますか?



マルチスレッドアプリケーションに問題が生じていると感じていますか? 特にCLRの場合-結局のところ、アプリケーションコードではなく環境自体で例外がスローされる可能性があります。



そのような場合のために、制約付き実行領域(CER)があります-すべての例外(同期および非同期の両方)の処理の保証。

CERとしてマークされたコードブロックでは、ランタイムは一部の非同期例外をスローできません。



たとえば、Thread.Abort()を呼び出すとき、CERで実行されたスレッドは、CERで保護されたコードの実行が完了するまで中断されません。

また、CLRは初期化中にCERを準備して、メモリが不足していても動作を保証します。



コードの大きなセクションにはCERを使用しないことをお勧めします。 この種のコードには、ボクシング、仮想メソッドの呼び出し、リフレクションによるメソッドの呼び出し、Monitor.Enterの使用など、多くの制限があります。



しかし、この問題を掘り下げて、LatencyModeモードを安全に切り替える方法を見てみましょう。

 var oldMode = GCSettings.LatencyMode; System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions(); try { GCSettings.LatencyMode = GCLatencyMode.Batch; //       } finally { GCSettings.LatencyMode = oldMode; }
      
      





さて、.NETでのGCの作業の主要部分については既に検討しました。質問はありませんか?



絶対に違う? 気になりませんでしたか?



うーん...本当にはかない世代に新しいオブジェクトを割り当てることができないという問題は興味がありませんでしたか?



そして、ここに.NETコマンドがあります-はい:)



これで、ワークステーションモード用の新しいバックグラウンドGC(.NET 4.5およびサーバーから開始)にアクセスできます。



その作成の目的は、Full-GC、特にGen2の遅延を減らすことでした。



バックグラウンドGCは、コンカレントGCと同じですが、1つの例外があります-Full-GCでは、新しいオブジェクトの割り当てに対して一時的な世代はブロックされません。



Gen2とLOHの処理は非常に高価であることに同意します。 そして、Gen0、Gen1のブロック-つまり アプリケーションの通常の動作により、遅延が発生する場合があります(特定の状況で)。



新しいGCで対処された別の問題は、管理ヒープのサイズに達すると新しいオブジェクトを割り当てる際の遅延です(16 MB-デスクトップ、64-サーバー)。







現在、この状況を防ぐために、Gen2のバックグラウンドスレッドだけでなく、フォアグラウンドスレッド(はい、フォアグラウンドGCもあります)は一時的な世代の死んだオブジェクトをマークし、現在の一時的な世代をGen2と組み合わせます.combiningはコピーよりも安価な操作です)、それらをバックグラウンドスレッド処理に転送します。これにより、新しいオブジェクトにメモリを割り当てることができます(バックグラウンドGC Gen0では、Gen1がGen2のGC操作中にブロックされないことを思い出してください)。







遅延数の削減は、以下のグラフで比較できます。







手動メモリ管理



CLR、特にC#の最も興味深い珍しい機能の1つは、手動メモリ管理です。 ポインターを操作します。



一般に、これは.NETの3番目のタイプ-ポインタータイプです。 これは、任意の値タイプの特定のインスタンスのDWORDアドレスを表します。 つまり 参照タイプは利用できません。



しかし、アンマネージコードを使用することもできます。



そのような目的のために、System.Runtime.InteropServices.GCHandle構造が作成されました-固定アドレスを持つオブジェクトは、アンマネージメモリからマネージオブジェクトにアクセスする機能を提供します。



GCHandleの場合、CLRはAppDomainごとに個別のテーブルを使用します。

GCHandleは、GCHandle.Free()が呼び出されるか、AppDomainがアンロードされると破棄されます。



作成するには、GChandle.Alloc()メソッドを使用します。

次の割り当てモードを使用できます。



GCHandle- MSDNに関する詳細。



手動のメモリ処理が必要な場合、尋ねますか?



たとえば、バイトの配列をコピーします。



 static unsafe void Copy(byte[] source, int sourceOffset, byte[] target, int targetOffset, int count) { fixed (byte* pSource = source, pTarget = target) { // Set the starting points in source and target for the copying. byte* ps = pSource + sourceOffset; byte* pt = pTarget + targetOffset; // Copy the specified number of bytes from source to target. for (int i = 0; i < count; i++) { *pt = *ps; pt++; ps++; } } }
      
      







, GC SOH? SOH.



( .NET — ). – .



|| .NET 4.0+



, .NET 2.0, .NET 4.0.







[ .NET Framework Versions and Dependencies ]



.NET 1.1, , , 2.0.



.NET 3.5 CLR 2.0, 2.0 + 3.0 + 3.5. , , .. .



.NET 4.0 :



CLR 2.0 CLR 4.0 :



 <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/> </startup> </configuration>
      
      







:



 <runtime> <NetFx40_LegacySecurityPolicy enabled="true"/> </runtime>
      
      







SEH- , :



 <configuration> <runtime> <legacyCorruptedStateExceptionsPolicy enabled="true"/> </runtime> </configuration>
      
      







, :



 <startup useLegacyV2RuntimeActivationPolicy="true"> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/> </startup>
      
      







, .NET 4.0.



|| FastCall



, JIT – , VC++, – FastCall.



, ECX, EDX 2 .



x64 – RCX, RDX, R8, R9.

?



, - ( , ).



 class Program { static void Main(string[] args) { int startIndex = 1; int endIndex = 2; int x = 3; int y = 5; int result = Compute(startIndex, endIndex, x, y); Console.WriteLine(result); } public static int Compute(int startIndex, int endIndex, int x, int y) { int result = 0; for (int i = startIndex; i < endIndex; i++) { result += x * startIndex + y * endIndex; } return result; } }
      
      







startIndex endIndex ECX, EDX, (x, y) .



– .



CTRL + D, R



.







CTRL + ALT + D



.










, !



ご清聴ありがとうございました!




All Articles