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

はじめに

VBと言ったな。あれは嘘だ。

前の記事でオブジェクトのメモリ上のサイズを計算してみましたが、結局System.Stringのサイズがよく分かんないというなんでそれ書いたんだよって感じのアレになりました。

そのままだとなんか悔しいので、調べなおしてみました。

github.com

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;
    }
}

https://github.com/dotnet/coreclr/blob/32f0f9721afb584b4a14d69135bea7ddc129f755/src/vm/object.cpp#L1856

メモリをアロケートしているのは(名前から察するに)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));
}

https://github.com/dotnet/coreclr/blob/e67851210d1c03d730a3bc97a87e8a6713bbf772/src/vm/gchelpers.cpp#L828

inline STRINGREF AllocateString( DWORD cchStringLength )
{
    WRAPPER_NO_CONTRACT;

    return SlowAllocateString( cchStringLength );
}

https://github.com/dotnet/coreclr/blob/32f0f9721afb584b4a14d69135bea7ddc129f755/src/vm/gchelpers.h#L86

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) );
}

https://github.com/dotnet/coreclr/blob/e67851210d1c03d730a3bc97a87e8a6713bbf772/src/vm/gchelpers.cpp#L878

余談ですが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));

https://github.com/dotnet/coreclr/blob/e67851210d1c03d730a3bc97a87e8a6713bbf772/src/vm/gchelpers.cpp#L901

__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);
}

https://github.com/dotnet/coreclr/blob/32f0f9721afb584b4a14d69135bea7ddc129f755/src/vm/object.inl#L61

#define ObjSizeOf(c)    (sizeof(c) + sizeof(ObjHeader))

https://github.com/dotnet/coreclr/blob/32f0f9721afb584b4a14d69135bea7ddc129f755/src/vm/syncblk.h#L1368

それでもって各定義が

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];

https://github.com/dotnet/coreclr/blob/910209a77d3311f845c535023d49b409d90e63ef/src/vm/object.h#L1087

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;

https://github.com/dotnet/coreclr/blob/2d9b2ab82c67344aa1d9fee422d7b7d5760cae92/src/gc/env/gcenv.object.h#L18

をアライン

#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

https://github.com/dotnet/coreclr/blob/32f0f9721afb584b4a14d69135bea7ddc129f755/src/vm/amd64/cgencpu.h#L27

とか

#define DATA_ALIGNMENT 4

https://github.com/dotnet/coreclr/blob/14bcf6ddf93ae4f7e897e219aaa8c0e43bb99415/src/vm/i386/cgencpu.h#L27

とか

#define DATA_ALIGNMENT 4

https://github.com/dotnet/coreclr/blob/74967f89e0f43e156cf23cd88840e1f0fc94f997/src/vm/arm/cgencpu.h#L18

とか、プロセッサ固有っぽい。

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;
}

https://github.com/dotnet/coreclr/blob/e67851210d1c03d730a3bc97a87e8a6713bbf772/src/vm/gchelpers.cpp#L114

なげぇ。

GCHeapUtilities::GetGCHeap()->Alloc()でも演算が入ってるかもしれませんが、これ以上はちょっと厳しいです。

というわけで計算してみましょう。

StringObjectで4バイ*1Objectで8バイト、ObjHeaderで8バイト、100文字だと100×2+2で202バイト。 合計で222バイトになり、8バイトでアラインすると224バイトになります。

おわりに

やっぱりなんか実測値とズレますね。

冒頭でも書きましたがCoreCLRは.NET Coreの方なので挙動が違うのかもしれませんし、弊社自身もCLRの動作という点ではまだまだ知識が足りないので今後の弊社の活躍にご期待くださいって感じで。

おわり

*1:WCHAR m_Characters[0]はsizeofで0になる