VB.NETでもパーサコンビネータを実装したい

はじめに

ヤフーでググったところ、今のところ誰もVBでパーサコンビネータを実装してないっぽかったので実装してみました。

実装にあたり、こちらのブログを参考にさせていただきました。感謝

blog.anatoo.jp

.NETのパーサコンビネータライブラリとしてはSpracheが有名ですが、SelectManyとクエリ構文でパーサの連結を行う機構が想像以上に複雑でちょっと何を言っているのかよくわからない状態なので今回は参考にするのはやめておきました。

github.com

パーサコンビネータとラムダ

ここではパーサは以下の引数を取り、以下の返り値を返す関数として定義します。

  • 引数
    • パース対象の文字列
    • パーサが解析を始める先頭からのオフセット
  • 返り値
    • パースが成功したか否か
    • パーサが消費した文字列(パーサの場合)
    • 内部のパーサの解析結果の列挙(コンビネータの場合)
    • 次に解析を行う先頭からのオフセット

また、今回はパーサをFunc(Of String, Integer, ParseResult)で表されるデリゲートとして実装します。

Public Class ParseResult

    Public ReadOnly Property Success() As Boolean
    Public ReadOnly Property Result() As String
    Public ReadOnly Property InnerResult() As IEnumerable(Of ParseResult)
    Public ReadOnly Property NextPosition() As Integer

    Public Sub New(success As Boolean, result As String, inner As IEnumerable(Of ParseResult), nextPosition As Integer)
        Me.Success = success
        Me.Result = result
        Me.InnerResult = If(inner, Enumerable.Empty(Of ParseResult)())
        Me.NextPosition = nextPosition
    End Sub

    Public Overrides Function ToString() As String
        Return $"[{Success}, {Result}, [{String.Join(",", InnerResult)}], {NextPosition}]"
    End Function

End Class

上記の例として、単純な文字列を解析するパーサを返す関数をメソッドします。

なお、ヘルパメソッドとして以下のメソッドが定義されているとします。 似たようなメソッドでMidがありますが、あちらは序数が1からで混乱のもとになるので使いませんでした。

Private Shared Function Substring(text As String, position As Integer, length As Integer) As String
    If text.Length > position + length Then
        Return text.Substring(position, length)
    ElseIf text.Length > position Then
        Return text.Substring(position)
    Else
        Return ""
    End If
End Function
Public Shared Function Token(word As String) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               If Substring(text, position, word.Length) = word Then
                   Return New ParseResult(True, word, Nothing, position + word.Length)
               Else
                   Return New ParseResult(False, Nothing, Nothing, position)
               End If
           End Function
End Function

上記のメソッドはトークンを引数に取り、指定された文字列とオフセットからトークンに一致するかを判定するパーサを返します。 一致した場合は一致した箇所と新しいオフセットを返します。

Dim hoge  = parser.Token("hoge")
Console.WriteLine(hoge("hogehoge", 0))
[True, hoge, [], 4]

また、同様に指定された文字列中の文字かどうか判定するパーサを返すメソッドを定義します。

Public Shared Function [Char](chars As String) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               Dim c = Substring(text, position, 1)

               If c <> "" AndAlso chars.Contains(c) Then
                   Return New ParseResult(True, c, Nothing, position + 1)
               Else
                   Return New ParseResult(False, Nothing, Nothing, position)
               End If
           End Function
End Function
Dim hoge  = parser.char("abc")
Console.WriteLine(hoge("abc", 0))
[True, a, [], 1]

ここでの基本的なパーサは以上です。あとはこれらをこねくり回して目的のパーサを組み立ててきます。

まずは選択を表すOrパーサです。 このメソッドは複数のパーサを引数にとり、先頭からマッチを繰り返しマッチに成功した時点でその結果を返すパーサを返します。

Public Shared Function [Or](ParamArray parsers As Func(Of String, Integer, ParseResult)()) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               For Each it In parsers
                   Dim r = it(text, position)
                   If r.Success Then
                       Return r
                   End If
               Next

               Return New ParseResult(False, Nothing, Nothing, position)
           End Function
End Function
Dim hogeOrFuga  = Parser.Or(Parser.Token("hoge"), parser.Token("fuga"))
Console.WriteLine(hogeOrFuga("hogehoge", 0))
Console.WriteLine(hogeOrFuga("fugahoge", 0))
[True, hoge, [], 4]
[True, fuga, [], 4]

次に複数のパーサの連結を表すSeqです。 このメソッドは複数のパースを引数をとり、順にマッチを行いすべてのパーサが成功したらその結果を返すパーサを返します。

Public Shared Function Seq(ParamArray parsers As Func(Of String, Integer, ParseResult)()) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               Dim result = New List(Of ParseResult)()
               Dim p = position

               For Each it In parsers
                   Dim r = it(text, p)

                   If r.Success Then
                       result.Add(r)
                       p = r.NextPosition
                   Else
                       Return New ParseResult(False, Nothing, Nothing, position)
                   End If
               Next

               Return New ParseResult(True, Nothing, result, p)
           End Function
End Function
Dim hogefuga= Parser.Seq(Parser.Token("hoge"), Parser.Token("fuga"))
Console.WriteLine(hogefuga("hogefuga", 0))
Console.WriteLine(hogefuga("fugahoge", 0))
[True, , [[True, hoge, [], 4],[True, fuga, [], 8]], 8]
[False, , [], 0]

Manyは0回以上の任意の回数の繰り返しを表します。

Public Shared Function Many(parser As Func(Of String, Integer, ParseResult)) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               Dim result = New List(Of ParseResult)()
               Dim p = position

               While True
                   Dim r = parser(text, p)

                   If r.Success Then
                       result.Add(r)
                       p = r.NextPosition
                   Else
                       Exit While
                   End If
               End While

               Return New ParseResult(True, Nothing, result, p)
           End Function
End Function
Dim hoge = Parser.Many(Parser.Token("hoge"))
Console.WriteLine(hoge("", 0))
Console.WriteLine(hoge("hoge", 0))
Console.WriteLine(hoge("hogehoge", 0))
[True, , [], 0]
[True, , [[True, hoge, [], 4]], 4]
[True, , [[True, hoge, [], 4],[True, hoge, [], 8]], 8]

また、同様にOptionはあっても無くてもかまわない選択を表し、Lazy再帰的なパーサの定義を行う際にパーサの評価を遅延させるために使い、Accumlate入れ子になった結果を平滑化するのに用います。

Public Shared Function [Option](parser As Func(Of String, Integer, ParseResult)) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               Dim r = parser(text, position)

               If r.Success Then
                   Return r
               Else
                   Return New ParseResult(True, Nothing, Nothing, position)
               End If
           End Function
End Function

Public Shared Function Lazy(f As Func(Of Func(Of String, Integer, ParseResult))) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               Return f()(text, position)
           End Function
End Function

Public Shared Function Accumlate(parser As Func(Of String, Integer, ParseResult)) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               Dim r = parser(text, position)

               If r.Success Then
                   Return Accumlate(r)
               Else
                   Return New ParseResult(False, Nothing, Nothing, position)
               End If
           End Function
End Function

入れ子の括弧の対応をとる単純な式のパーサ

を実装します。

まぁ、やってることは完全に参考サイトのVBへの焼き直しなんでアレがアレですね。

まずは数字と演算子をパースするパーサを定義します。 この辺は簡単ですね。

Property Number As Func(Of String, Integer, ParseResult) =
    Parser.Accumlate(Parser.Seq(Parser.Char("123456789"), Parser.Many(Parser.Char("0123456789"))))

Property [Operator] As Func(Of String, Integer, ParseResult) = Parser.Char("+-")

次に括弧に包まれた式をパースするパーサを定義します。

括弧はいらないので括弧の中身だけを取り出すパーサを定義します。 また、Expressionはまだ定義していないのでLazyでパーサの評価を実行時まで遅延させます。

Property Parenthesis As Func(Of String, Integer, ParseResult) = Parser.Lazy(
    Function() As Func(Of String, Integer, ParseResult)
        Return Function(text As String, position As Integer) As ParseResult
                   Dim p = Parser.Seq(Parser.Token("("), Expression, Parser.Token(")"))
                   Dim r = p(text, position)

                   If r.Success Then
                       Return r.InnerResult.Skip(1).First()
                   Else
                       Return New ParseResult(False, Nothing, Nothing, position)
                   End If
               End Function
    End Function)

オペランドは数字か括弧で包まれた式なので、それをパースするパーサを定義します。

Property Atom As Func(Of String, Integer, ParseResult) = Parser.Or(Number, Parenthesis)

あとは1+2+3

[True, , [[True, 1, [], 1],[True, +, [], 2],[True, 2, [], 3],[True, +, [], 4],[True, 3, [], 5]], 5]

となるように頑張ってパーサを実装します。

Property Expression As Func(Of String, Integer, ParseResult) =
    Function(text As String, position As Integer) As ParseResult
        Dim p = Parser.Seq(Atom, Parser.Many(Parser.Seq([Operator], Atom)))
        Dim r = p(text, position)

        If Not r.Success Then
            Return New ParseResult(False, Nothing, Nothing, position)
        End If

        Dim ir = r.InnerResult.ToList()

        If ir.Count = 1 Then
            Return ir(0)
        Else
            Dim rs = New List(Of ParseResult)()

            rs.Add(ir(0))
            For Each i In ir(1).InnerResult
                For Each it In i.InnerResult
                    rs.Add(it)
                Next
            Next

            Return New ParseResult(True, Nothing, rs, r.NextPosition)
        End If
    End Function

あとはパース結果を見やすくするメソッドと、括弧の対応を取りながら計算するメソッドを定義します。

Sub Display(result As ParseResult, level As Integer)
    If Not ReferenceEquals(result.Result, Nothing) Then
        Console.WriteLine($"{New String(" "c, level)} {result.Result}")
    Else
        For Each it In result.InnerResult
            Display(it, level + 1)
        Next
    End If
End Sub

Function Calc(results As IEnumerable(Of ParseResult)) As Integer
    Dim r = New Queue(Of ParseResult)(results)
    Dim sum = 0

    Dim first = r.Dequeue()
    If Not ReferenceEquals(first.Result, Nothing) Then
        sum += Integer.Parse(first.Result)
    Else
        sum += Calc(first.InnerResult)
    End If

    While r.Count <> 0
        Dim op = r.Dequeue().Result
        Dim operand = r.Dequeue()
        Dim operandNum As Integer

        If Not ReferenceEquals(operand.Result, Nothing) Then
            operandNum = Integer.Parse(operand.Result)
        Else
            operandNum = Calc(operand.InnerResult)
        End If

        If op = "+" Then
            sum += operandNum
        Else
            sum -= operandNum
        End If
    End While

    Return sum
End Function
Dim r = Expression("1+2+(3+(5-6))", 0)
Console.WriteLine(r)

Display(r, 0)
Console.WriteLine($"result: {Calc(r.InnerResult)}")
  1
  +
  2
  +
   3
   +
    5
    -
    6
result: 5

正しくパースと計算が出来ているっぽいです。

おわりに

250行程度のコードでも再帰的な構造を持つ構文を解析できました。

今回実装したパーサコンビネータはパース結果を他のオブジェクトへマップする機能を完全にオミットしたり静的型付け言語の制約上結果が入れ子になって扱いずらい等、あまり完成度は高くありませんがパーサコンビネータのコンセプトを理解するのには十分だと思います。って偉そうなこと書いてますが、完全に参考サイトの受け売りなのでアレですが、まぁ弊社自身もなるほどなぁといった感じです。

実際に使用する場合はSprache等のライブラリを使うといい感じになると思います。

.NET FrameworkのRegexの文字クラスはバグっているのか?

はじめに

正規表現のお話ですが、『.は任意の一文字にマッチする』みたいな文法解説ではありません。 そんなの大手サイトが死ぬほど書いてますしね。

妙なバグを踏み抜くことに定評がある弊社ですが、今回は正規表現の文字クラス関係でわりと意味がわからないバグを踏み抜いたのでそれについてです。

.NET Framework正規表現の互換性

正規表現にはいくつかの系統というか規格(PosixとかJavaScript)が存在しますが、.NETはどこに属しているのでしょうか?

.NET Framework の正規表現

.NET Framework では、正規表現のパターンが特殊な構文または言語で定義されます。この構文または言語には、Perl 5 の正規表現と互換性があるほか、右から左への一致処理など、いくつかの機能が追加されています。

ということでPerl 5と互換性があると言っています。信じて良いんですね?

検証環境

ということで楽しい検証の時間です。

それぞれの言語で以下のサイトを使用しました。

また、paiza.IOはオンラインの開発環境でRegexテスターではないので以下のコードでテストをしました。

余談ですが、補完文字列はC#6からの機能のはずですが、C#5のはずpaiza.IOで使えてます。 更に余談ですが、mono-4.2.1で補完文字列のバグが修正されてるってリリースノートに書いてあります。なにこれ?

using System;
using System.Text.RegularExpressions;

public class Hello
{
    public static void Main()
    {
        foreach(Match it in Regex.Matches("テスト文字列","正規表現"))
        {
            Console.WriteLine($"\"{it.Value}\"");
        }
    }
}

[]

空の文字クラスは.NET・mono・PCREでは受容されませんが、JavaScriptでは受容されます。 まぁ順当な結果ですね。

[5-3]

逆順の文字クラスは.NET・mono・PCRE・JavaScriptの全てで受容されませんでした。 当たり前ですね。

[Z-[]]

Z[[]にマッチするという表現でPCRE・JavaScriptでは問題なく動作しますが、.NET・monoではUnterminated [] set.ということで弾かれます。 はぁ、そうですか。

[Z-[ ]]

『.NET・monoでは空の文字クラスが弾かれてたじゃん。じゃあ[]にスペース入れれば動くんじゃね?』がまじで動いたパターン。

PCRE・JavaScriptではZ][]]のパターンにマッチしますが、.NET・monoではZのみにマッチします。

[A-Z_-[ ]]

大文字のAからZ_から[のいずれかと]にマッチするという表現はそもそも_>]なので受容されないはずですが、.NET・monoでは受容されます。

さらにAZ_のみにマッチするという不可解な挙動をします。面白くなってきましたね。

[A-Z-[ ]]

PCRE・JavaScriptではZ][]]A]などにマッチしますが、.NET・monoでは相変わらずAZなどの単独の文字にしかマッチしません。 -[ ]をどこにやった?

おわりに

普通なら思いつかないような謎正規表現をプロダクト環境にぶち込んでくる人がいるので、世の中は多様性で満ち溢れていますね。

バグか他の文法との兼ね合いなのかが分かりませんでしたが、仮にバグだとしても後方互換性を維持するために直すのは難しそうですね。

完全に余談ですが、仮に正規表現が超複雑になるようなものを解析しないといけない場合はIronySpracheなんかを使ったほうが後々幸せかもしれません。

おわり

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