VB.NETでも文字列オブジェクトのサイズを計算したい
はじめに
VBと言ったな。あれは嘘だ。
前の記事でオブジェクトのメモリ上のサイズを計算してみましたが、結局System.String
のサイズがよく分かんないというなんでそれ書いたんだよって感じのアレになりました。
そのままだとなんか悔しいので、調べなおしてみました。
CoreCLRは.NET Coreの方だけど、まぁ同じっしょ。
今回もx64と仮定して確認します。
スタートポイント
多分ここから追っていけばいいのです。 自分で
**Action: Creates a System.String object.
って言っているので間違いないのです。
/*==================================NewString=================================== **Action: Creates a System.String object. **Returns: **Arguments: **Exceptions: ==============================================================================*/ STRINGREF StringObject::NewString(INT32 length) { CONTRACTL { GC_TRIGGERS; MODE_COOPERATIVE; PRECONDITION(length>=0); } CONTRACTL_END; STRINGREF pString; if (length<0) { return NULL; } else if (length == 0) { return GetEmptyString(); } else { pString = AllocateString(length); _ASSERTE(pString->GetBuffer()[length] == 0); return pString; } }
メモリをアロケートしているのは(名前から察するに)AllocateString(length)
なので、その定義を探します。
GitHubのリポジトリ内を検索すると以下の2つが出てきますが、最初のは#if defined(_TARGET_X86_)
なので多分二つ目です。
STRINGREF AllocateString( DWORD cchStringLength ) { CONTRACTL { THROWS; GC_TRIGGERS; MODE_COOPERATIVE; // returns an objref without pinning it => cooperative } CONTRACTL_END; #ifdef _DEBUG // fastStringAllocator is called by VM and managed code. If called from managed code, we // make sure that the thread is in SOTolerantState. #ifdef FEATURE_STACK_PROBE Thread::DisableSOCheckInHCALL disableSOCheckInHCALL; #endif // FEATURE_STACK_PROBE #endif // _DEBUG return STRINGREF(HCCALL1(fastStringAllocator, cchStringLength)); }
inline STRINGREF AllocateString( DWORD cchStringLength ) { WRAPPER_NO_CONTRACT; return SlowAllocateString( cchStringLength ); }
STRINGREF SlowAllocateString( DWORD cchStringLength ) { CONTRACTL { THROWS; GC_TRIGGERS; MODE_COOPERATIVE; // returns an objref without pinning it => cooperative } CONTRACTL_END; StringObject *orObject = NULL; #ifdef _DEBUG if (g_pConfig->ShouldInjectFault(INJECTFAULT_GCHEAP)) { char *a = new char; delete a; } #endif // Limit the maximum string size to <2GB to mitigate risk of security issues caused by 32-bit integer // overflows in buffer size calculations. if (cchStringLength > 0x3FFFFFDF) ThrowOutOfMemory(); SIZE_T ObjectSize = PtrAlign(StringObject::GetSize(cchStringLength)); _ASSERTE(ObjectSize > cchStringLength); SetTypeHandleOnThreadForAlloc(TypeHandle(g_pStringClass)); orObject = (StringObject *)Alloc( ObjectSize, FALSE, FALSE ); // Object is zero-init already _ASSERTE( orObject->HasEmptySyncBlockInfo() ); // Initialize Object //<TODO>@TODO need to build a LARGE g_pStringMethodTable before</TODO> orObject->SetMethodTable( g_pStringClass ); orObject->SetStringLength( cchStringLength ); if (ObjectSize >= LARGE_OBJECT_SIZE) { GCHeapUtilities::GetGCHeap()->PublishObject((BYTE*)orObject); } // Notify the profiler of the allocation if (TrackAllocations()) { OBJECTREF objref = ObjectToOBJECTREF((Object*)orObject); GCPROTECT_BEGIN(objref); ProfilerObjectAllocatedCallback(objref, (ClassID) orObject->GetTypeHandle().AsPtr()); GCPROTECT_END(); orObject = (StringObject *) OBJECTREFToObject(objref); } #ifdef FEATURE_EVENT_TRACE // Send ETW event for allocation if(ETW::TypeSystemLog::IsHeapAllocEventEnabled()) { ETW::TypeSystemLog::SendObjectAllocatedEvent(orObject); } #endif // FEATURE_EVENT_TRACE LogAlloc(ObjectSize, g_pStringClass, orObject); #if CHECK_APP_DOMAIN_LEAKS if (g_pConfig->AppDomainLeaks()) orObject->SetAppDomain(); #endif return( ObjectToSTRINGREF(orObject) ); }
余談ですが2GB以上の文字列を確保しようとすると問答無用でアウトオブメモリでぬっ殺されるらしいです。
0x3FFFFFDF
って約1GBなのですが・・・。
// Limit the maximum string size to <2GB to mitigate risk of security issues caused by 32-bit integer // overflows in buffer size calculations. if (cchStringLength > 0x3FFFFFDF) ThrowOutOfMemory();
注目するのはここ。
SIZE_T ObjectSize = PtrAlign(StringObject::GetSize(cchStringLength));
__forceinline /*static*/ SIZE_T StringObject::GetSize(DWORD strLen) { LIMITED_METHOD_DAC_CONTRACT; // Extra WCHAR for null terminator return ObjSizeOf(StringObject) + sizeof(WCHAR) + strLen * sizeof(WCHAR); }
#define ObjSizeOf(c) (sizeof(c) + sizeof(ObjHeader))
それでもって各定義が
class StringObject : public Object { #ifdef DACCESS_COMPILE friend class ClrDataAccess; #endif friend class GCHeap; friend class JIT_TrialAlloc; friend class CheckAsmOffsets; friend class COMString; private: DWORD m_StringLength; WCHAR m_Characters[0];
と
class Object { protected: PTR_MethodTable m_pMethTab;
https://github.com/dotnet/coreclr/blob/910209a77d3311f845c535023d49b409d90e63ef/src/vm/object.h#L188
と
class ObjHeader { private: #if defined(BIT64) uint32_t m_uAlignpad; #endif // BIT64 uint32_t m_uSyncBlockValue;
をアライン
#define PTRALIGNCONST (DATA_ALIGNMENT-1) #ifndef PtrAlign #define PtrAlign(size) \ ((size + PTRALIGNCONST) & (~PTRALIGNCONST)) #endif //!PtrAlign
https://github.com/dotnet/coreclr/blob/910209a77d3311f845c535023d49b409d90e63ef/src/vm/object.h#L174
#define DATA_ALIGNMENT 8
とか
#define DATA_ALIGNMENT 4
とか
#define DATA_ALIGNMENT 4
とか、プロセッサ固有っぽい。
inline Object* Alloc(size_t size, BOOL bFinalize, BOOL bContainsPointers ) { CONTRACTL { THROWS; GC_TRIGGERS; MODE_COOPERATIVE; // returns an objref without pinning it => cooperative } CONTRACTL_END; _ASSERTE(!NingenEnabled() && "You cannot allocate managed objects inside the ngen compilation process."); #ifdef _DEBUG if (g_pConfig->ShouldInjectFault(INJECTFAULT_GCHEAP)) { char *a = new char; delete a; } #endif DWORD flags = ((bContainsPointers ? GC_ALLOC_CONTAINS_REF : 0) | (bFinalize ? GC_ALLOC_FINALIZE : 0)); Object *retVal = NULL; CheckObjectSize(size); // We don't want to throw an SO during the GC, so make sure we have plenty // of stack before calling in. INTERIOR_STACK_PROBE_FOR(GetThread(), static_cast<unsigned>(DEFAULT_ENTRY_PROBE_AMOUNT * 1.5)); if (GCHeapUtilities::UseAllocationContexts()) retVal = GCHeapUtilities::GetGCHeap()->Alloc(GetThreadAllocContext(), size, flags); else retVal = GCHeapUtilities::GetGCHeap()->Alloc(size, flags); if (!retVal) { ThrowOutOfMemory(); } END_INTERIOR_STACK_PROBE; return retVal; }
なげぇ。
GCHeapUtilities::GetGCHeap()->Alloc()
でも演算が入ってるかもしれませんが、これ以上はちょっと厳しいです。
というわけで計算してみましょう。
StringObject
で4バイト*1、Object
で8バイト、ObjHeader
で8バイト、100文字だと100×2+2で202バイト。
合計で222バイトになり、8バイトでアラインすると224バイトになります。
おわりに
やっぱりなんか実測値とズレますね。
冒頭でも書きましたがCoreCLRは.NET Coreの方なので挙動が違うのかもしれませんし、弊社自身もCLRの動作という点ではまだまだ知識が足りないので今後の弊社の活躍にご期待くださいって感じで。
おわり
*1:WCHAR m_Characters[0]はsizeofで0になる
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
おわりに
割と煩雑になりがちな状態遷移をいい感じ宣言的に扱えるのがいいですね。
特定の目的を果たすための小さ目のライブラリ、大好きです。