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

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

はじめに

この記事はVisual Basic Advent Calendar 2016の11日目の記事となります。

10日目はamay077さんのVB.NET でパスワード付き共有フォルダにファイルをコピーするでした。

数十・数百万オーダーのオブジェクトをメモリ上にのっけてCPUをギュイーンするようなパワフルプログラミンをすることが稀によくあるのですが、そうなるとどの位メモリを消費するのか気になることがあります。 Windows 10で2TB、Windows Server 2016 Datacenterに至っては24TBまで使用可能なので超課金コンピューティングができる環境なら気にしなくてもいいのかもしれませんが、庶民的なパソコンでは多くても16GB程度ですのでいつメモリ不足に陥るか不安に苛まれながらプログラムを実行することになるので精神衛生上あまりよろしくありません。

そこで、今回はメモリ上のオブジェクトサイズの概算を得られないかとなんかいろいろ頑張った結果を書こうと思います。

環境

今回は以下の環境で検証を行いました。

また、ここでは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_stringLengthSystem.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全体のサイズ

と、いうわけで(あやふやさを置いておいて)Hoge1つの全体のサイズは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のバージョン、はたまたプロセッサアーキテクチャによっても変わってきてしまう可能性があるので大まかな指針ぐらいにとどめておく感じですかね。

おわり

参考

github.com