VB.NETでもオブジェクトサイズの概算をしたい

はじめに

この記事はVisual Basic Advent Calendar 2016の11日目の記事となります。

10日目はamay077さんのVB.NET でパスワード付き共有フォルダにファイルをコピーするでした。

数十・数百万オーダーのオブジェクトをメモリ上にのっけてCPUをギュイーンするようなパワフルプログラミンをすることが稀によくあるのですが、そうなるとどの位メモリを消費するのか気になることがあります。 Windows 10で2TB、Windows Server 2016 Datacenterに至っては24TBまで使用可能なので超課金コンピューティングができる環境なら気にしなくてもいいのかもしれませんが、庶民的なパソコンでは多くても16GB程度ですのでいつメモリ不足に陥るか不安に苛まれながらプログラムを実行することになるので精神衛生上あまりよろしくありません。

そこで、今回はメモリ上のオブジェクトサイズの概算を得られないかとなんかいろいろ頑張った結果を書こうと思います。

環境

今回は以下の環境で検証を行いました。

また、ここでは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_stringLengthSystem.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のバージョン、はたまたプロセッサアーキテクチャによっても変わってきてしまう可能性があるので大まかな指針ぐらいにとどめておく感じですかね。

おわり

参考

github.com