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

VB.NETでも割と楽に状態を扱いたい

はじめに

.NETチームのブログを眺めていたところ、面白そうなライブラリを見つけました。

blogs.msdn.microsoft.com

github.com

オブジェクトの状態遷移を管理するためのライブラリで、状態遷移を引き起こすトリガーとトリガーによって変わる状態を扱ってくれます。 また、状態遷移をイベントとしてデリゲートの実行なども行ってくれます。

購買申請

生産性アプリケーションの状態遷移といったら真っ先に思い浮かぶのが購買申請です。 某拝承系企業では購買申請でリアルスタンプラリーが開催されていると聞きますが、本当なのでしょうか?

混沌を極めた購買申請フローは狂気を感じるところがありますが、ここではとりあえず以下のような簡単な状態遷移を考えてみます。

              "否認"
             +-------> 否認済み
             |         
             |
承認待ち --------> 承認済み --------> 発注済み --------> 検収済み
          "承認"             "発注"             "検収"

また、各状態に遷移したらそれぞれに対応した処理を実行します。

  • 承認済み・否認済み
    • 申請者に通知
  • 発注済み
    • 業者にファックス
  • 検収済み
    • 買掛金元帳に追加

それでは、実際のコードを見てみましょう。

Enum 状態
    承認待ち
    承認済み
    否認
    発注済み
    検収済み
End Enum

Enum 条件
    承認
    否認
    発注
    検収
End Enum

まずはオブジェクトがとりうる状態と、状態遷移のトリガーとなる条件を定義します。

Private _品目 As String
Private _価格 As Decimal

Private _状態 = 状態.承認待ち
Private _状態機械 As StateMachine(Of 状態, 条件)
Private _否認行為 As StateMachine(Of 状態, 条件).TriggerWithParameters(Of String)

Public Sub New(品目 As String, 価格 As Decimal)
    _品目 = 品目
    _価格 = 価格

    _状態機械 = New StateMachine(Of 状態, 条件)(Function() _状態, Sub(状態) _状態 = 状態)
    _否認行為 = _状態機械.SetTriggerParameters(Of String)(条件.否認)

    _状態機械.Configure(状態.承認待ち).
        Permit(条件.承認, 状態.承認済み).
        Permit(条件.否認, 状態.否認)

    _状態機械.Configure(状態.承認済み).
        OnEntry(CType(Sub() 承認時(), Action)).
        Permit(条件.発注, 状態.発注済み)

    _状態機械.Configure(状態.否認).
        OnEntryFrom(_否認行為, Sub(理由) 否認時(理由))

    _状態機械.Configure(状態.発注済み).
        OnEntry(CType(Sub() 発注時(), Action)).
        Permit(条件.検収, 状態.検収済み)

    _状態機械.Configure(状態.検収済み).
        OnEntry(CType(Sub() 検収時(), Action))
End Sub

まずは、

_状態機械 = New StateMachine(Of 状態, 条件)(Function() _状態, Sub(状態) _状態 = 状態)

でステートマシンの状態とオブジェクトの状態を関連付けます。 まぁ、状態を取得するラムダと状態をセットするラムダを突っ込むだけです。

そうしたら定義した状態と因子を用いて、ある状態から遷移可能な状態とその状態遷移を引き起こす条件をポチポチ定義していきます。 日本語でおk状態なので、一部分を切り取って見てみましょう。

              "否認"
             +-------> 否認済み
             |         
             |
承認待ち --------> 承認済み
          "承認"
_状態機械.Configure(状態.承認待ち).
    Permit(条件.承認, 状態.承認済み).
    Permit(条件.否認, 状態.否認)

承認待ちという状態は承認によって承認済みという状態に、否認によって否認済みという状態に遷移します。 それをPermitメソッドを用いてステートマシンに登録します。

また、状態が遷移したタイミング(もしくは今回は使っていませんが状態を離れるタイミング)で実行するデリゲートを登録するためにOnEntry(もしくはOnExit)を使うことができます。

Class 購買申請

    Enum 状態
        承認待ち
        承認済み
        否認
        発注済み
        検収済み
    End Enum

    Enum 条件
        承認
        否認
        発注
        検収
    End Enum

    Private _品目 As String
    Private _価格 As Decimal

    Private _状態 = 状態.承認待ち
    Private _状態機械 As StateMachine(Of 状態, 条件)
    Private _否認行為 As StateMachine(Of 状態, 条件).TriggerWithParameters(Of String)

    Public Sub New(品目 As String, 価格 As Decimal)
        _品目 = 品目
        _価格 = 価格

        _状態機械 = New StateMachine(Of 状態, 条件)(Function() _状態, Sub(状態) _状態 = 状態)
        _否認行為 = _状態機械.SetTriggerParameters(Of String)(条件.否認)

        _状態機械.Configure(状態.承認待ち).
            Permit(条件.承認, 状態.承認済み).
            Permit(条件.否認, 状態.否認)

        _状態機械.Configure(状態.承認済み).
            OnEntry(CType(Sub() 承認時(), Action)).
            Permit(条件.発注, 状態.発注済み)

        _状態機械.Configure(状態.否認).
            OnEntryFrom(_否認行為, Sub(理由) 否認時(理由))

        _状態機械.Configure(状態.発注済み).
            OnEntry(CType(Sub() 発注時(), Action)).
            Permit(条件.検収, 状態.検収済み)

        _状態機械.Configure(状態.検収済み).
            OnEntry(CType(Sub() 検収時(), Action))
    End Sub

    Public Sub 承認()
        _状態機械.Fire(条件.承認)
    End Sub

    Private Sub 承認時()
        Console.WriteLine("申請者に通知")
        Console.WriteLine($"{_品目}の申請が承認されました")
        Console.WriteLine()
    End Sub

    Public Sub 否認(理由 As String)
        _状態機械.Fire(_否認行為, 理由)
    End Sub

    Private Sub 否認時(理由 As String)
        Console.WriteLine("申請者に通知")
        Console.WriteLine($"{_品目}の申請が以下の理由により否認されました")
        Console.WriteLine(理由)
        Console.WriteLine()
    End Sub

    Public Sub 発注()
        _状態機械.Fire(条件.発注)
    End Sub

    Private Sub 発注時()
        Console.WriteLine($"業者に{_品目}の注文をファックス")
        Console.WriteLine()
    End Sub

    Public Sub 検収()
        _状態機械.Fire(条件.検収)
    End Sub

    Private Sub 検収時()
        Console.WriteLine($"買掛金元帳に{_品目}({_価格}円)を追加")
        Console.WriteLine()
    End Sub

End Class

使う方はこんな感じです。 不正な状態遷移を行おうとすると例外が送出されるので、アプリケーションがバグっていても安心ですね。

Module Module1

    Sub Main()
        Dim 筆記用具購買申請 = New 購買申請("ボールペン", 100D)

        筆記用具購買申請.承認()
        ' 申請者に通知
        ' ボールペンの申請が承認されました

        筆記用具購買申請.発注()
        ' 業者にボールペンの注文をファックス

        筆記用具購買申請.検収()
        ' 買掛金元帳にボールペン(100円)を追加


        Dim コンピュータ購買申請 = New 購買申請("HPE Integrity Superdome X", 100000000D)

        コンピュータ購買申請.否認("エクセルを使うのにこのスペックは必要?")
        ' 申請者に通知
        ' HPE Integrity Superdome Xの申請が以下の理由により否認されました
        ' エクセルを使うのにこのスペックは必要?

        Try
            コンピュータ購買申請.発注()
        Catch ex As InvalidOperationException
            Console.WriteLine("否認された購買申請を発注することが何を意味するのか、貴様分かっているのだろうな?")
        End Try

    End Sub

End Module

おわりに

割と煩雑になりがちな状態遷移をいい感じ宣言的に扱えるのがいいですね。

特定の目的を果たすための小さ目のライブラリ、大好きです。

github.com

C#でもUbuntu+Docker+Jenkins+GitBucket+MonoでCIしたい(CI編)

はじめに

前回はJenkinsとGitBucketの環境を構築しました。 今回は実際にJenkinsでビルドとテストを実行させてみましょう。

プロジェクトの作成

まずはCIをブンブン回すプロジェクトを作成します。

テストの実装及びJenkinsでのテストの実行のために

  • NUnit
  • NUnit.Runners

をNuGet経由でインストールし、リポジトリ直下のnugetディレクトにNuGetの実行ファイルを突っ込んでおきます。

というわけで今回使ったのがこちら。 なお、弊社はなんとなくGitHubリポジトリが不用意に増えるのが嫌なのでGitHubに上げてるのは汎用リポジトリに突っ込んじゃってますが、今回の内容ではHelloMonoCiが独立したリポジトリだと思って下さい。

github.com

GitBuckerリポジトリの作成

とりあえずリポジトリを作成して、先程作成したVisual Studioプロジェクトを突っ込んでおきましょう。

Jenkinsビルドジョブの作成

ジョブの作成からフリースタイル・プロジェクトのビルドを選択します。

設定画面ではとりあえずこんな感じに設定します。

f:id:jyuch:20161012220806p:plain

名前は自由で構いません。

f:id:jyuch:20161012220807p:plain

Monoはスレーブノードにしか(今回の構成では)インストールされていないので、スレーブノードで実行されるように実行ノードの制限はかけておきましょう。

f:id:jyuch:20161012220810p:plain

この辺は特にこれと言った注意点はありません。

f:id:jyuch:20161012220812p:plain

あとでweb hookを引っ掛けるので、Build when a change is pushed to GitBucketのチェックを入れておきましょう。

f:id:jyuch:20161012220750p:plain

この辺は好みでどうぞ。

f:id:jyuch:20161012220757p:plain

ビルドの最初のステップでNuGet経由でライブラリを復元します。

mono nuget/nuget.exe restore HelloMonoCi/HelloMonoCi.sln


f:id:jyuch:20161012220803p:plain

リリースビルドを実行します。

xbuild /p:Configuration=Release HelloMonoCi/HelloMonoCi.sln


f:id:jyuch:20161012220804p:plain

後続のテストリザルドパブリッシャー殿がNUnit3形式だと『無☆理』と言って死ぬので--result:TestResult.xml;format=nunit2を指定します。

mono HelloMonoCi/packages/NUnit.ConsoleRunner.3.4.1/tools/nunit3-console.exe \
    HelloMonoCi/HelloMonoCi.Test/HelloMonoCi.Test.csproj \
    "--config=Release" "--result:TestResult.xml;format=nunit2" 


f:id:jyuch:20161012220816p:plain

テストリザルドファイルを指定します。

サーバのローカルアドレス192.168.0.10だとかGitBucketのユーザがurikkだとかどうでもいい情報を全世界に垂れ流しつつ、とりあえずJenkinsはこれでokです。

GitBucker Web Hook

GitBucketのリポジトリに戻ってweb hookの設定をします。

f:id:jyuch:20161012224239p:plain

Payload URLにはhttp://192.168.0.10:8090/gitbucket-webhook/を指定して、あとはデフォルトで大丈夫です。

ここまで来ればもう終わり。あとはGitBuckerにプッシュするだけで優秀な執事がビルドとテストを回してくれます。

おわりに

まぁ、ふつーにWindowsでビルドサーバーを立てるかAppVeyorを使ったほうが圧倒的に楽ですね。 公開前に読み返して『これ何かの意味があるのかなぁ?』と頭を抱えています。

あと、全く関係がないのですが弊社は.NETの単体テストライブラリはMSTestしか使ったことがなく、またMSTestで事足りてたので他のライブラリを使うという発想自体がなく今までNUnitを触ったことすらなかったのですが、NUnitって良いですね。

特にTestCaseによるパラメータ化テストはMSTestには無い*1のでそのうち追加されたらいいな〜とおもいました。

おわり

*1:外部ライブラリを導入すれば同じようなことはできるっぽい