はじめに
この記事は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全体のサイズ
と、いうわけで(あやふやさを置いておいて)Hoge1つの全体のサイズは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のバージョン、はたまたプロセッサアーキテクチャによっても変わってきてしまう可能性があるので大まかな指針ぐらいにとどめておく感じですかね。
おわり