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になる