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のバージョン、はたまたプロセッサアーキテクチャによっても変わってきてしまう可能性があるので大まかな指針ぐらいにとどめておく感じですかね。
おわり
参考
VB.NETでも割と楽に状態を扱いたい
はじめに
.NETチームのブログを眺めていたところ、面白そうなライブラリを見つけました。
オブジェクトの状態遷移を管理するためのライブラリで、状態遷移を引き起こすトリガーとトリガーによって変わる状態を扱ってくれます。 また、状態遷移をイベントとしてデリゲートの実行なども行ってくれます。
購買申請
生産性アプリケーションの状態遷移といったら真っ先に思い浮かぶのが購買申請です。 某拝承系企業では購買申請でリアルスタンプラリーが開催されていると聞きますが、本当なのでしょうか?
混沌を極めた購買申請フローは狂気を感じるところがありますが、ここではとりあえず以下のような簡単な状態遷移を考えてみます。
"否認" +-------> 否認済み | | 承認待ち --------> 承認済み --------> 発注済み --------> 検収済み "承認" "発注" "検収"
また、各状態に遷移したらそれぞれに対応した処理を実行します。
- 承認済み・否認済み
- 申請者に通知
- 発注済み
- 業者にファックス
- 検収済み
- 買掛金元帳に追加
それでは、実際のコードを見てみましょう。
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
おわりに
割と煩雑になりがちな状態遷移をいい感じ宣言的に扱えるのがいいですね。
特定の目的を果たすための小さ目のライブラリ、大好きです。
C#でもUbuntu+Docker+Jenkins+GitBucket+MonoでCIしたい(CI編)
はじめに
前回はJenkinsとGitBucketの環境を構築しました。 今回は実際にJenkinsでビルドとテストを実行させてみましょう。
プロジェクトの作成
まずはCIをブンブン回すプロジェクトを作成します。
テストの実装及びJenkinsでのテストの実行のために
NUnit
NUnit.Runners
をNuGet経由でインストールし、リポジトリ直下のnuget
ディレクトにNuGetの実行ファイルを突っ込んでおきます。
というわけで今回使ったのがこちら。
なお、弊社はなんとなくGitHubのリポジトリが不用意に増えるのが嫌なのでGitHubに上げてるのは汎用リポジトリに突っ込んじゃってますが、今回の内容ではHelloMonoCi
が独立したリポジトリだと思って下さい。
GitBuckerリポジトリの作成
とりあえずリポジトリを作成して、先程作成したVisual Studioプロジェクトを突っ込んでおきましょう。
Jenkinsビルドジョブの作成
ジョブの作成からフリースタイル・プロジェクトのビルド
を選択します。
設定画面ではとりあえずこんな感じに設定します。
名前は自由で構いません。
Monoはスレーブノードにしか(今回の構成では)インストールされていないので、スレーブノードで実行されるように実行ノードの制限はかけておきましょう。
この辺は特にこれと言った注意点はありません。
あとでweb hookを引っ掛けるので、Build when a change is pushed to GitBucket
のチェックを入れておきましょう。
この辺は好みでどうぞ。
ビルドの最初のステップでNuGet経由でライブラリを復元します。
mono nuget/nuget.exe restore HelloMonoCi/HelloMonoCi.sln
リリースビルドを実行します。
xbuild /p:Configuration=Release HelloMonoCi/HelloMonoCi.sln
後続のテストリザルドパブリッシャー殿が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"
テストリザルドファイルを指定します。
サーバのローカルアドレスが192.168.0.10
だとかGitBucketのユーザがurikk
だとかどうでもいい情報を全世界に垂れ流しつつ、とりあえずJenkinsはこれでokです。
GitBucker Web Hook
GitBucketのリポジトリに戻ってweb hookの設定をします。
Payload URLにはhttp://192.168.0.10:8090/gitbucket-webhook/
を指定して、あとはデフォルトで大丈夫です。
ここまで来ればもう終わり。あとはGitBuckerにプッシュするだけで優秀な執事がビルドとテストを回してくれます。
おわりに
まぁ、ふつーにWindowsでビルドサーバーを立てるかAppVeyorを使ったほうが圧倒的に楽ですね。 公開前に読み返して『これ何かの意味があるのかなぁ?』と頭を抱えています。
あと、全く関係がないのですが弊社は.NETの単体テストライブラリはMSTestしか使ったことがなく、またMSTestで事足りてたので他のライブラリを使うという発想自体がなく今までNUnitを触ったことすらなかったのですが、NUnitって良いですね。
特にTestCase
によるパラメータ化テストはMSTestには無い*1のでそのうち追加されたらいいな〜とおもいました。
おわり
*1:外部ライブラリを導入すれば同じようなことはできるっぽい