C#:配列初期化子の内部構造

C#を扱ったほぼ全員が、同様の構造を知っています。



int[] ints = new int[3] { 1,2,3 };//       ,      
      
      





この構造が似たようなものになると予想するのは論理的です:



 int[] ints = new int[3]; ints[0] = 1; ints[1] = 2; ints[2] = 3;
      
      





悲しいかな、実は、ナットは一見したところよりもはるかにしわが寄っていて、後で示されるいくつかの微妙な点があります。 それまでは、着用済みの「ILフリーク」Tシャツ(着用している人)を着用し、実装の腸に突入しました。



最終的に、最初の構成によりコンパイラーはこのような波線に変わります。







私の庭で何が不思議ですか? この<PrivateImplementationDetails> blablablaでたらめは何ですか? 内容を説明する前に、 Q :: Mainを見てみましょう。ここでは、コードの各行の前にスタックの一番上にある値を示しています。



 .method private hidebysig static void Main() cil managed { .entrypoint // Code size 20 (0x14) .maxstack 3 .locals init (int32[] V_0) IL_0000: nop // {} IL_0001: ldc.i4.3 // {3} IL_0002: newarr [mscorlib]System.Int32 // {&int[3]} IL_0007: dup // {&int[3], &int[3]} IL_0008: ldtoken field valuetype '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'/'__StaticArrayInitTypeSize=12' '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'::'$$method0x6000001-1' // {&int[3], &int[3], #'$$method0x6000001-1'} IL_000d: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle) // {&int[3]} IL_0012: stloc.0 // {} IL_0013: ret } // end of method Q::Main
      
      





行ごとの分析を行いましょう。

IL_0001およびIL_0002 - System.Int32型およびディメンション3の新しい配列が作成されます。

IL_0007では 、重複した配列参照という形で最初の驚きに出会いました。 なんで? IL_0008およびIL_0009で配列が初期化されているとします (すぐにこの場所に戻ります)。 IL_0012を見てみましょう。ここでは、スタックの一番上の値(ここでも配列)がインデックス0のローカル変数に割り当てられています。 変数ints しかし、 int変数の値をIL_0007に割り当てるとどうなりますか? そして、これは起こります:



 ldc.i4.3 newarr [mscorlib]System.Int32 stloc.0 //  ldloc.0 //  ldtoken field valuetype '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'/'__StaticArrayInitTypeSize=12' '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'::'$$method0x6000001-1' call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
      
      





割り当てはもはやアトミックではなくなります。これ以降、外部オブザーバーは要素がなく初期化されていない状態の配列に気付くでしょう。 これは、 IL_0008IL_0009の行がまさに行うことです 。 T.O. 最初に与えられたコード構築と同等ではありません



 nt[] ints = new int[3]; ints[0] = 1; ints[1] = 2; ints[2] = 3;
      
      





むしろ、次のようなものです。



 int[] t = new int[3]; t[0] = 1; t[1] = 2; t[2] = 3; int[] ints = t;
      
      





ただし、実装では2つのローカル変数の作成は回避されます。 これにより、次の2行のコードに移動します。



  IL_0008: ldtoken field valuetype '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'/'__StaticArrayInitTypeSize=12' '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'::'$$method0x6000001-1' IL_000d: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
      
      





しかし、これには複雑なものやひどいものはありません。 実際、 RuntimeHelpers.InitializeArrayの呼び出しを観察します。これは、 IL_0007のスタックにトークンがプッシュされるフィールドを、 IL_0007が実行された後のスタックの最上部にあるリンクの配列で埋めます。 トークンの値は、次の図に対応しています。







実際、強調表示された行はプライベートの静的フィールドであり、明らかに、コンパイラによって生成された、明らかに発音できない名前のクラスです。 注意すべき点がいくつかあります。 まず、このクラスには__StaticArrayInitTypeSize = 12というネストされたクラスがあります。 12バイトの実際のサイズの配列です( System.Int32要素ごとに4バイト、各サイズは4バイト、合計12)。 次に、型はSystem.ValueTypeを継承することに注意する必要があります(スタックで作成された後、読者が重要な型のインスタンスの運命に精通していることを真剣に願っています。したがって、これに固執することはありません- 著者注 )。 しかし、タイプはどのようにして同じ12バイトを受信しますか 明らかに、名前をずらすだけではclrが必要なメモリ量を割り当てるのに十分ではないため、ILDASMを介して実装を見ると、次のように表示されます。



 .class private auto ansi '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}' extends [mscorlib]System.Object { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) .class explicit ansi sealed nested private '__StaticArrayInitTypeSize=12' extends [mscorlib]System.ValueType { .pack 1 .size 12 //    } // end of class '__StaticArrayInitTypeSize=12' .field static assembly valuetype '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'/'__StaticArrayInitTypeSize=12' '$$method0x6000001-1' at I_00002050 } // end of class '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'
      
      





.sizeディレクティブ 、そのままで、clrに、このタイプのインスタンスを作成するときに12バイトのメモリブロックを割り当てる必要があることを伝えます。 .packディレクティブの役割に興味がある場合、ポイントは簡単です:このディレクティブは、指定された2の累乗によるアライメントを示します(2から128の値のみがサポートされます(値1の場合、アライメントは明らかに存在しません- およその翻訳 ))。 COMとの互換性のために必要です。 フィールドに戻りましょう。



 .field static assembly valuetype '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'/'__StaticArrayInitTypeSize=12' '$$method0x6000001-1' at I_00002050
      
      







型は入れ子になっているために名前が非常に長いという事実にもかかわらず、型は非常に単純です。 この例では、 「$$ method0x6000001-1」がフィールドの名前です。 しかし、楽しみは「 at 」の後に始まります。 これはいわゆる data-labelは、PEファイル内の特定のオフセットにあるデータの一部です。 ILADSMに直接、次のようなものが表示されます。



 .data cil I_00002050 = bytearray ( 01 00 00 00 02 00 00 00 03 00 00 00)
      
      





これはデータラベルの宣言です 。これは、既に見たように、リトルエンディアンの最終配列のバイトシーケンスです。 ここで、 InitailizeArrayの仕組みを理解する必要があります。



 call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
      
      





配列のインスタンスが転送され (コマンドIL_0001IL_0002で作成済み )、キーワード「 at 」の後に指定されたフィールドへのポインターが転送され、配列データがラップされます。 T.O. ランタイムは、指定されたアドレスで読み取るために必要なバイト数を計算できるため、配列を構築できます。 同様に、値I_0000 2050の意味は謎ではありません。これは最も珍しいRVAです。 これを確認するには、dumpbinを使用します。



ただし、同様に興味深い詳細があります。配列が同じメモリ容量を占有する場合、コンパイラは__StaticArrayInitTypeSize型を再利用します。 T.O. リスト:



 int[] ints = { 1, 2, 3, 4, 5, 6, 7, 8 }; long[] longs = { 1, 2, 3, 4 }; byte[] bytes = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 };
      
      





メモリー内のすべての配列がそれぞれ32バイトを占有するため、コンパイラーは同じタイプを使用するように強制します。



 .field static assembly valuetype '<PrivateImplementationDetails>{AA6C9D77-5FAD-47E0-8B55-1D8739074F1F}'/'__StaticArrayInitTypeSize=32' '$$method0x6000001-1' at I_00002050 .field static assembly valuetype '<PrivateImplementationDetails>{AA6C9D77-5FAD-47E0-8B55-1D8739074F1F}'/'__StaticArrayInitTypeSize=32' '$$method0x6000001-2' at I_00002070 .field static assembly valuetype '<PrivateImplementationDetails>{AA6C9D77-5FAD-47E0-8B55-1D8739074F1F}'/'__StaticArrayInitTypeSize=32' '$$method0x6000001-3' at I_00002090 .data cil I_00002050 = bytearray ( 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00) .data cil I_00002070 = bytearray ( 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00) .data cil I_00002090 = bytearray ( 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20)
      
      





したがって、サイズが1および2の要素の配列の場合、そのようなILコードが生成されます。

 .method private hidebysig static void Main() cil managed { .entrypoint // Code size 19 (0x13) .maxstack 3 .locals init (int32[] V_0, int32[] V_1) IL_0000: nop IL_0001: ldc.i4.2 IL_0002: newarr [mscorlib]System.Int32 IL_0007: stloc.1 // // V_1[0] = 1 // IL_0008: ldloc.1 IL_0009: ldc.i4.0 IL_000a: ldc.i4.1 IL_000b: stelem.i4 // // V_1[1] = 2 // IL_000c: ldloc.1 IL_000d: ldc.i4.1 IL_000e: ldc.i4.2 IL_000f: stelem.i4 // // V_0 = V_1 // IL_0010: ldloc.1 IL_0011: stloc.0 IL_0012: ret } // end of method Q::Main
      
      





実際、ここには2つのローカル変数を使用した同じトリックがあります。1つは一時的なもので、配列がいっぱいになると値が配置され、その後配列へのリンクがメイン変数に転送されます。 このアプローチの理由(配列を埋める別の方法)は明らかです:単純な実装の場合、各要素に対して4つのコマンドがあり、配列のサイズに線形比例して配列を構築するためのコードの量を増やし、代わりにコード量は一定になります。



psこの記事では、MicrosoftのコンパイラC#2.0および3.0バージョンの動作について説明します。 他のバージョンのコンパイラまたはサードパーティの開発者(たとえば、Mono)のコンパイラによって生成されたコードの動作は、記事に記載されているものと異なる場合があります。



All Articles