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

Что за диво в моём саду? Что это за <PrivateImplementationDetails>blablabla ерундовина? Прежде чем я расскажу что к чему, давайте бросим наш взгляд на Q::Main, где я указал значение в вершине стека перед каждой строкой кода:
Давайте теперь проведём построчный анализ:
IL_0001 и IL_0002 — создаётся новый массив типа System.Int32 и размерностью 3.
На IL_0007 нам попадается первый сюрприз в виде дублированной ссылки на массив. Почему? Предположим, что на IL_0008 и IL_0009 происходит инициализация массива (совсем скоро к этому месту мы вернёмся). А теперь посмотрим на IL_0012, где значение в вершине стека — опять массив — присвоен локальной переменной с индексом 0, т.е. переменной ints. А что если мы присвоим значение переменной ints на IL_0007? А произойдёт вот что:
Присвоение более не будет атомарным: с этого момента, внешний наблюдатель заметит массив в неинициализированном состоянии, без элементов. Именно этим и занимаются строки IL_0008 и IL_0009. Т.о. приведённый в самом начале код не эквивалентен конструкции:
А скорее представляет из себя нечто вроде этого:
Хотя реализация избегает создания двух локальных переменных. Это двигает нас к двум заумным строчкам кода:
Но ничего сложного и/или страшного в этом нет. По сути, мы наблюдаем вызов RuntimeHelpers.InitializeArray, который заполняет поле, токен которого помещён в стек на IL_0008, массивом, ссылка на который находится в вершине стека после выполнения IL_0007. Значение токена соотвествует картинке ниже:

На деле, выделенная линия являет собой статично поле в приватном и, очевидно, сгенерированном компилятором, классе с очевидно непроизносимым названием. Пара моментов, на которые следует обратить внимание. Во-первых, этот класс имеет вложенный класс, названный __StaticArrayInitTypeSize=12. Он являет собой массив фактическим размером в 12 байт (по 4 байта на каждый элемент System.Int32, размер каждого равен 4 байта, итого 12). Во-вторых, следует заметить что тип наследует System.ValueType (я всерьёз надеюсь на то, что читатели знакомы с судьбой экземпляров значимых типов после их создания в стеке, так что не будем на этом застрять внимания — прим. автора.). Но каким образом тип получает те самые 12 байт? Очевидно что просто подсунуть имя недостаточ��о для того, чтобы clr выделила необходимое количество памяти, так что если вы посмотрите на реализацию через ILDASM вы увидите вот что:
Директива .size как-бы говорит и нам, и clr о том, что необходимо выделить блок памяти в 12 байт в момент создания экземпляра этого типа. Если вам любопытно о роли директивы .pack, то суть проста: эта директива указывает выравнивание по указанной степени двойки (поддерживаются только значения от 2 до 128 (при значении 1 выравнивание, очевидно, отсутствует — прим. перев)). Необходимо для COM-совместимости. Вернёмся к полю:
Тип довольно простой, несмотря на то, что имя довольно длинное из-за вложенности типов. В нашем случае, '$$method0x6000001-1' это имя поля. Но самое интересное начинается после "at". Это т.н. data-label, который, в свою очередь, является куском данных где-то в PE-файле на данном смещении. Непосредственно в ILADSM вы увидите нечто подобное:
Это и есть обьявление data label, которое является, как уже видно, последовательностью байт конечного массива в little-endian. Теперь мы должны понимать как работает InitailizeArray:
Передаётся экземпляр массива (мы уже создали его командами IL_0001,IL_0002) и указатель на поле, указанное после ключевого слова "at", в которое завёрнуты данные массива. Т.о. среда исполнения способна посчитать необходимое количество байт для чтения по заданному адресу, конструируя таким образом массив. В свою очередь, смысл значения I_00002050 не являет собой никакой загадки — это преобыкновеннейший RVA. Вы можете в этом убедиться, используя dumpbin:
Но есть не менее занятная деталь: компилятор переиспользует тип __StaticArrayInitTypeSize когда массивы занимают одинаковое количество места в памяти. Т.о. листинг:
Заставляет компилятор использовать один и тот-же тип, ибо все массивы в памяти занимают по 32 байта:
Так-же, для массивов размером в 1 и 2 элемента, будет генерировать такой IL код:
А вот, собственно, и тот самый фокус с двумя локальными переменными: одна из них является временной, в которую и помещаются значения по мере заполнения массива, после чего ссылка на массив передаётся основной переменной. Причины же такого подхода (с отдельным методом для заполнения массива) очевидны: в случае наивной реализации мы бы имели по 4 команды на каждый элемент, что увеличивало бы обьём кода построения массива линейно пропорционально размеру массива, вместо этого обьём кода константен.
p.s. В статье приведено поведение компиляторов C# 2.0 и 3.0 версий от Microsoft. Поведение кода, сгенерированного компиляторами других версий или компиляторов от сторонних разработчиков (например, Mono), может отличаться от приведённого в статье.
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), может отличаться от приведённого в статье.
