.NET FrameworkのRegexの文字クラスはバグっているのか?
はじめに
正規表現のお話ですが、『.
は任意の一文字にマッチする』みたいな文法解説ではありません。
そんなの大手サイトが死ぬほど書いてますしね。
妙なバグを踏み抜くことに定評がある弊社ですが、今回は正規表現の文字クラス関係でわりと意味がわからないバグを踏み抜いたのでそれについてです。
.NET Frameworkの正規表現の互換性
正規表現にはいくつかの系統というか規格(PosixとかJavaScript)が存在しますが、.NETはどこに属しているのでしょうか?
.NET Framework では、正規表現のパターンが特殊な構文または言語で定義されます。この構文または言語には、Perl 5 の正規表現と互換性があるほか、右から左への一致処理など、いくつかの機能が追加されています。
ということでPerl 5と互換性があると言っています。信じて良いんですね?
検証環境
ということで楽しい検証の時間です。
それぞれの言語で以下のサイトを使用しました。
- .NET Framework - Regex Storm
- Mono(mono-4.2.1 (C#5, CLI4.5)) - paiza.IO
- JavaScript - regexper、regex101
- PCRE - regex101
また、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では受容されます。
さらにA
〜Z
と_
のみにマッチするという不可解な挙動をします。面白くなってきましたね。
[A-Z-[ ]]
PCRE・JavaScriptではZ]
、[]
、]
、A]
などにマッチしますが、.NET・monoでは相変わらずA
やZ
などの単独の文字にしかマッチしません。
-[ ]
をどこにやった?
おわりに
普通なら思いつかないような謎正規表現をプロダクト環境にぶち込んでくる人がいるので、世の中は多様性で満ち溢れていますね。
バグか他の文法との兼ね合いなのかが分かりませんでしたが、仮にバグだとしても後方互換性を維持するために直すのは難しそうですね。
完全に余談ですが、仮に正規表現が超複雑になるようなものを解析しないといけない場合はIronyやSpracheなんかを使ったほうが後々幸せかもしれません。
おわり
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のバージョン、はたまたプロセッサアーキテクチャによっても変わってきてしまう可能性があるので大まかな指針ぐらいにとどめておく感じですかね。
おわり