Наверняка почти каждому, кто имел дело с C#, известна подобная конструкция:

int[] ints = new int[3] { 1,2,3 };//А если уж вдруг и не была известна, то отныне и впредь уж точно

Вполне логично было-бы ожидать превращение этой конструкции в нечто подобное:

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

Увы и ах, на деле орех гораздо более морщинист, чем кажется с первого взгляда, и имеются некоторые тонкости, на которые будет указано позже. А до тех пор, наденем ношеную «IL freak» майку (у кого имеется) и погрузимся в недра реализации.

В конечном итоге, первая конструкция превратится компилятором в такую вот загогулину:



Что за диво в моём саду? Что это за <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. А что если мы присвоим значение переменной ints на 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_0008 и IL_0009. Т.о. приведённый в самом начале код не эквивалентен конструкции:

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;

Хотя реализация избегает создания двух локальных переменных. Это двигает нас к двум заумным строчкам кода:

 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_0008, массивом, ссылка на который находится в вершине стека после выполнения IL_0007. Значение токена соотвествует картинке ниже:



На деле, выделенная линия являет собой статично поле в приватном и, очевидно, сгенерированном компилятором, классе с очевидно непроизносимым названием. Пара моментов, на которые следует обратить внимание. Во-первых, этот класс имеет вложенный класс, названный __StaticArrayInitTypeSize=12. Он являет собой массив фактическим размером в 12 байт (по 4 байта на каждый элемент System.Int32, размер каждого равен 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 до 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)

Это и есть обьявление data label, которое является, как уже видно, последовательностью байт конечного массива в little-endian. Теперь мы должны понимать как работает InitailizeArray:

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

Передаётся экземпляр массива (мы уже создали его командами IL_0001,IL_0002) и указатель на поле, указанное после ключевого слова "at", в которое завёрнуты данные массива. Т.о. среда исполнения способна посчитать необходимое количество байт для чтения по заданному адресу, конструируя таким образом массив. В свою очередь, смысл значения I_00002050 не являет собой никакой загадки — это преобыкновеннейший RVA. Вы можете в этом убедиться, используя dumpbin:

Но есть не менее занятная деталь: компилятор переиспользует тип __StaticArrayInitTypeSize когда массивы занимают одинаковое количество места в памяти. Т.о. листинг:

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

А вот, собственно, и тот самый фокус с двумя локальными переменными: одна из них является временной, в которую и помещаются значения по мере заполнения массива, после чего ссылка на массив передаётся основной переменной. Причины же такого подхода (с отдельным методом для заполнения массива) очевидны: в случае наивной реализации мы бы имели по 4 команды на каждый элемент, что увеличивало бы обьём кода построения массива линейно пропорционально размеру массива, вместо этого обьём кода константен.

p.s. В статье приведено поведение компиляторов C# 2.0 и 3.0 версий от Microsoft. Поведение кода, сгенерированного компиляторами других версий или компиляторов от сторонних разработчиков (например, Mono), может отличаться от приведённого в статье.