まえがき
まず、.NETには、スタックとヒープ(ガベージコレクターによって管理される)にそれぞれ作成される 値型と参照型の 2種類のオブジェクトがあることを思い出してください 。
値型は、数字、文字などの単純なデータを格納するように設計されています。 値を変数に割り当てると、各オブジェクトフィールドがコピーされます。 また、そのようなオブジェクトの寿命はスコープに依存します。 値型の次元は共通型システムで定義されており、次のもので構成されています。
CTSタイプ | バイト数 |
System.Byte | 1 |
System.SByte | 1 |
System.Int16 | 2 |
System.Int32 | 4 |
System.Int64 | 8 |
System.UInt16 | 2 |
System.UInt32 | 4 |
System.UInt64 | 8 |
System.Single | 4 |
System.Double | 8 |
System.char | 2 |
System.Decimal | 16 |
対照的に、参照型は、ヒープ上のオブジェクトのインスタンスが占有するメモリ領域への参照です。
CLRオブジェクトの内部構造は次のとおりです。
変数参照型の場合、固定サイズ値(4バイト、DWORD型)がスタックに配置され、通常のヒープ(ラージオブジェクトヒープ、HighFrequencyHeapなどもありますが、それらに焦点を合わせません)で作成されたオブジェクトインスタンスのアドレスが含まれます) たとえば、C ++では、この値はオブジェクトへのポインターと呼ばれ、.NETの世界では、オブジェクトへの参照と呼ばれます。
初期状態では、SyncBlockの値はゼロです。 ただし、オブジェクトのハッシュコードはSyncBlock( GetHashCodeメソッドを呼び出す場合)またはsyncblkレコード番号に格納できます。これにより、同期中にオブジェクトヘッダーに環境が設定されます( lockまたは直接Monitor.Enterを使用 )。
各タイプには独自のMethodTableがあり、同じタイプのオブジェクトのすべてのインスタンスは同じMethodTableを参照します。 このテーブルには、型自体に関する情報(インターフェイス、抽象クラスなど)が格納されます。
参照型ポインター-オフセット4でスタックに配置された変数に格納されているオブジェクトへの参照。
残りはクラスフィールドです。
SOS
理論から実践に移ります。 標準のCLRツールを使用してオブジェクトのサイズを設定することはできません。 はい、C#にはsizeof演算子がありますが、アンマネージオブジェクトのサイズと値型のサイズを設定することを目的としています。 参照型の質問では-無駄です。
これらの目的のために、Visual Studioデバッガーの拡張機能-SOS(Son of Strike)があります。
開始する前に、アンマネージコードのデバッグを有効にする必要があります。
デバッグ中にSOSを有効にするには、 VS>デバッグ> Windows>イミディエイトウィンドウを開き、次を入力します。
.load sos.dll
その後、ダウンロードが成功します:
SOSには多数のチームがあります。 私たちの場合、次のものだけが必要です。
- !DumpStackObjects(!DSO)-現在のスタック内で検出されたオブジェクトのリストを表示します
- !DumpObj(!DO)-指定されたアドレスにあるオブジェクトに関する情報を表示します
- !ObjSize-オブジェクトのフルサイズを返します。 少し後でその目的を検討します
他のコマンドは、 !Helpと入力すると見つけることができます。
デモのために、単純なコンソールアプリケーションを作成し、 MyExampleClassクラスを記述します 。
class MyExampleClass { byte ByteValue = 255; // 1 sbyte SByteValue = 127; // 1 char CharValue = 'a'; // 2 short ShortValue = 128; // 2 ushort UShortValue = 65000; // 2 int Int32Value = 255; // 4 uint UInt32Value = 255; // 4 long LongValue = 512; // 8 ulong ULongValue = 512; // 8 float FloatValue = 128F; // 4 double DoubleValue = 512D; // 8 decimal DecimalValue = 10M; // 16 string StringValue = "String"; // 4 }
計算機を使用して、クラスのインスタンスの推定サイズを計算します-現時点では、64バイトです。
ただし、オブジェクトの構造に関する記事の冒頭で覚えていますか? したがって、最終的なサイズは次のようになります。
CLRオブジェクト= SyncBlock(4)+ TypeHandle(4)+フィールド(64)= 72
理論を確認してください。
次のコードを追加します。
class Program { static void Main(string[] args) { var myObject = new MyExampleClass(); Console.ReadKey(); // breakpoint } }
デバッグを実行します(F5)。
イミディエイトウィンドウに次のコマンドを入力します。
.load sos.dll
!DSO
上記のスクリーンショットでは、 myObjectオブジェクトのアドレスが強調表示されており、これをパラメーターとして!DOコマンドに渡します。
さて、 myObjectのサイズは72バイトです。 そう?
答えはノーです。 実際、StringValue変数の文字列サイズを追加するのを忘れていました。 その4バイトは単なる参照です。 そして、ここで実際のサイズを確認します。
コマンドを入力しましょう!ObjSize:
したがって、 myObjectの実際のサイズは100バイトです。
追加の28バイトは、StringValue変数によって取得されます。
ただし、これを確認します。 これを行うには、 StringValue 01b8c008変数のアドレスを使用します。
System.Stringのサイズを構成するものは何ですか?
まず、CTSでは、文字(タイプSystem.Char )はUnicodeで表され、2バイトを占有します。
第二に、文字列は文字の配列にすぎません。 そのため、StringValueに値「String」を作成しました。これは12バイトです。
3番目に、System.Stringは参照型です。つまり、GCヒープに配置され、SyncBlock、TypeHandle、参照ポイント+クラスの他のフィールドで構成されます。 ここでは、参照ポイントは考慮されません。 クラスMyExampleClassですでにカウントされています(参照4バイト)。
4番目に、System.Stringの構造は次のとおりです。
追加のクラスフィールドは、Int32型のm_stringLength変数(4バイト)、Char型のm_firstChar(2バイト)です。Empty変数は考慮されません。 空の静的文字列です。
また、サイズに注意してください-以前に計算された28バイトではなく26バイト。 すべてまとめてください:
StringValue = SyncBlock(4)+ TypeHandle(4)+ m_stringLength(4)+ m_firstChar(2)+“ String”(12)= 26
CLRメモリマネージャーによって生成されるアライメントのため、追加の2バイトが作成されます。
x86対 x64
主な違いは、DWORD-メモリポインタのサイズです。 32ビットシステムでは4バイトですが、64ビットシステムでは既に8バイトです。
したがって、x86で空のクラスが12バイトしかない場合、x64には24バイトがあります。
CLRオブジェクトのサイズ制限
System.Stringのサイズは、使用可能なシステムメモリによってのみ制限されることが一般に受け入れられています。
ただし、どのタイプのインスタンスも2 GBを超えるメモリを占有することはできません。 そして、この制限はx86システムとx64システムの両方に適用されます。
したがって、ListにはLongCount()メソッドがありますが、これは2 ^ 64個のオブジェクトを配置できることを意味しません。 解決策は、この目的のために設計されたBigArrayクラスを使用することです。
あとがき
この記事では、CLRオブジェクトのサイズを見つける問題に触れたいと思いました。 もちろん、特に!ObjSizeチームでは、インターン文字列の使用により二重カウントが発生する可能性があるという落とし穴があります。
ほとんどの場合、オブジェクトのサイズの問題である、メモリ内でのオブジェクトのアライメントは、リソースの使用を最適化する可能性という強いニーズがある場合にのみ発生します。
この記事がおもしろくて役立つことを願っています。 ご清聴ありがとうございました!