VB.NETでもオブジェクトサイズの概算をしたい
はじめに
この記事はVisual Basic Advent Calendar 2016の11日目の記事となります。
10日目はamay077さんのVB.NET でパスワード付き共有フォルダにファイルをコピーするでした。
数十・数百万オーダーのオブジェクトをメモリ上にのっけてCPUをギュイーンするようなパワフルプログラミンをすることが稀によくあるのですが、そうなるとどの位メモリを消費するのか気になることがあります。 Windows 10で2TB、Windows Server 2016 Datacenterに至っては24TBまで使用可能なので超課金コンピューティングができる環境なら気にしなくてもいいのかもしれませんが、庶民的なパソコンでは多くても16GB程度ですのでいつメモリ不足に陥るか不安に苛まれながらプログラムを実行することになるので精神衛生上あまりよろしくありません。
そこで、今回はメモリ上のオブジェクトサイズの概算を得られないかとなんかいろいろ頑張った結果を書こうと思います。
環境
今回は以下の環境で検証を行いました。
- WIndows 10 Pro 64bit
- Visual Studio 2015
- WinDbg 10.0.10586.567
- .NET Framework 4.6.1
また、ここではx64でビルドされたマネージドアセンブリを想定します。 x86とx64ではポインタサイズとメモリアライメントで差異がでますが、結論から言うと同じプログラムでもx64のほうがメモリ上のサイズは大きくなります。
また、デバッグ版で検証を行っております。
かも~
とりあえず以下のようなコードを想定します。
Module Module1 Private Const ArraySize As Integer = 30000 Sub Main() Dim hoges(ArraySize - 1) As Hoge for i = 0 To ArraySize - 1 hoges(i) = CreateRandomHoge() Next ' ここでの hoges のサイズが知りたい。 Console.ReadLine() Console.WriteLine($"{hoges(CInt(ArraySize / 2)).id}, {hoges(cint(ArraySize/2)).Name}") End Sub Function CreateRandomHoge() As Hoge ' 不思議な力でオブジェクトを初期化するメソッド End Function End Module Class Hoge Public Id As Integer ' 100文字のランダムな文字列 Public Name As String End Class
Hoge
のサイズ
まず、Hoge
そのもののサイズを検証してみましょう。
WinDbgでなんとか頑張ってhoges
の要素の1つをダンプしてみましょう。
0:000> !DumpObj /d 000002003a3a6608 Name: ObjectSizeInMemory.Hoge MethodTable: 00007ff8338e5b70 EEClass: 00007ff833a31020 Size: 32(0x20) bytes File: C:\ObjectSizeInMemory\ObjectSizeInMemory\bin\x64\Debug\ObjectSizeInMemory.exe Fields: MT Field Offset Type VT Attr Value Name 00007ff8968e3e98 400000d 10 System.Int32 1 instance 1528591907 Id 00007ff8968e16b8 400000e 8 System.String 0 instance 000002003a3aa468 Name
まぁ、要約すると以下の感じに収まってるっぽいです。
--------------------------------- -8バイト オブジェクトヘッダワード --------------------------------- 0バイト メソッドテーブルポインタ --------------------------------- +8バイト Nameの(ポインタ) --------------------------------- +16バイト Id --------------------------------- +20バイト (パディング) --------------------------------- +24バイト
ここでのオブジェクトヘッダワードやメソッドテーブルポインタは参照型のオブジェクトにはすべて存在している項目なのですが、気にしなくてもいいです。 まぁ、CLRの動作に必要な情報ってことでなんとか。
そいつらそれぞれ8バイト占有するので合計で16バイト。
また、System.String
は参照型なのでName
はポインタを保持し、それで8バイト。
IdはSystem.Int32
で構造体なので実体がそこに配置されるのでそれで4バイト。
x64環境ではヒープ内のオブジェクトは8バイトでアラインされるので4バイトがパディングされて合計で32バイト占有することになります。
100文字分のSystem.String
のサイズ
似たような感じでName
のサイズのほうも計算してみましょう。
Name: System.String MethodTable: 00007ff8968e16b8 EEClass: 00007ff8962647a8 Size: 226(0xe2) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll String: 2695031204841115206595599743842564759607157684463719208609638835237646559844890428903226504096931145 Fields: MT Field Offset Type VT Attr Value Name 00007ff8968e3e98 4000248 8 System.Int32 1 instance 100 m_stringLength 00007ff8968e28c8 4000249 c System.Char 1 instance 32 m_firstChar 00007ff8968e16b8 400024d 90 System.String 0 shared static Empty
同様に以下のような感じに収まっています。
--------------------------------- -8バイト オブジェクトヘッダワード --------------------------------- 0バイト メソッドテーブルポインタ --------------------------------- +8バイト m_stringLength --------------------------------- +12バイト m_firstChar + 100文字分のchar --------------------------------- +218バイト (パディング) --------------------------------- +224バイト
こちらもオブジェクトヘッダワードとメソッドテーブルポインタで16バイト。
文字列長を保持するm_stringLength
がSystem.Int32
なので4バイト。
.NETでの文字列はUTF-16 LEで格納されるので一文字で2バイト、101文字(最後の一文字はヌル文字)で202バイト。
合計で222バイト・・・若干ズレてますね。
0:000> !DumpObj /d 000002a400004538 Name: System.String MethodTable: 00007ff8968e16b8 EEClass: 00007ff8962647a8 Size: 34(0x22) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll String: aaaa Fields: MT Field Offset Type VT Attr Value Name 00007ff8968e3e98 4000248 8 System.Int32 1 instance 4 m_stringLength 00007ff8968e28c8 4000249 c System.Char 1 instance 61 m_firstChar 00007ff8968e16b8 400024d 90 System.String 0 shared static Empty
こちらだと上の計算式に当てはめると8+8+4+(4+1)×2=30になるはずですがどうも違うっぽいです。
まぁ、とりあえず話を戻しましてName
の実体のString
はパディング込みで232バイトとなります。
Hoge
全体のサイズ
と、いうわけで(あやふやさを置いておいて)Hoge
1つの全体のサイズは264バイトと計算できました。
配列サイズ
お次は配列のサイズを試算しましょう。
0:000> !DumpObj /d 00000190900099a8 Name: ObjectSizeInMemory.Hoge[] MethodTable: 00007ff8338d5be8 EEClass: 00007ff896317728 Size: 240024(0x3a998) bytes Array: Rank 1, Number of elements 30000, Type CLASS (Print Array) Fields: None
配列そのもののオブジェクトヘッダワードとメソッドテーブルポインタ、あとは何かのポインタで8×3=24バイト。 配列はメモリ上に連続で並びますのでポインタが30000個ならんでいるので8×30000=240000バイト。 これらを合計して240024バイト。
なお、8の倍数になっているのでパディングはありません。
合計サイズ
あとは加算するだけですね。
264×30000+240024=8160024バイトという結果が出ました。 分かりやすく表すと約7.78MBですね。
0:000> !objsize 00000190900099a8 sizeof(00000190900099a8) = 8160024 (0x7c8318) bytes (ObjectSizeInMemory.Hoge[])
ドンピシャですね。やったぁー
おわりに
概算を得るだけだったら最後の!objsize
を使えば得られちゃうんで、いままでの苦労は何だったんだろうかって感じになりますね。
また、ここまで求めた結果もOSや.NET・CLRのバージョン、はたまたプロセッサアーキテクチャによっても変わってきてしまう可能性があるので大まかな指針ぐらいにとどめておく感じですかね。
おわり