Подтвердить что ты не робот

Почему так медленно создается массив с встроенной инициализацией?

Почему инициализация встроенного массива настолько медленнее, чем это происходит итеративно? Я выполнил эту программу, чтобы сравнить их, и одиночная инициализация занимает много раз дольше, чем при цикле for.

Здесь программа, которую я написал в LinqPad, чтобы проверить это.

var iterations = 100000000;
var length = 4;

{
    var timer = System.Diagnostics.Stopwatch.StartNew();

    for(int i = 0; i < iterations; i++){
        var arr = new int[] { 1, 2, 3, 4 };
    }
    timer.Stop();
    "Array- Single Init".Dump();
    timer.Elapsed.Dump();
}

{
    var timer = System.Diagnostics.Stopwatch.StartNew();

    for(int i = 0; i < iterations; i++){
        var arr = new int[length];
        for(int j = 0; j < length; j++){
            arr[j] = j;
        }
    }
    timer.Stop();
    "Array- Iterative".Dump();
    timer.Elapsed.Dump();
}

Результаты:

Array - Single Init
00:00:26.9590931

Array - Iterative
00:00:02.0345341

Я также использовал это на VS2013 Community Edition и последнем просмотре VS2015 на другом ПК и получил аналогичные результаты для моих LinqPad результатов.

Я запустил код в режиме Release (т.е.: оптимизация компилятора) и получил очень разные результаты сверху. На этот раз два кода были очень похожи. Это, по-видимому, указывает на проблему оптимизации компилятора.

Array - Single Init
00:00:00.5511516

Array - Iterative
00:00:00.5882975
4b9b3361

Ответ 1

Прежде всего, профилирование на уровне С# не даст нам ничего, так как покажет нам строку кода С#, выполнение которой длится больше всего, что, конечно, является инициализацией встроенного массива, но для спорта:

Profiling Results

Теперь, когда мы увидим ожидаемые результаты, давайте наблюдаем код на уровне IL и пытаемся увидеть, чем отличаются инициализации двух массивов:

  • Прежде всего мы рассмотрим стандартную инициализацию массива:

    For Loop

    Все выглядит хорошо, цикл делает именно то, что мы ожидаем, без заметных накладных расходов.

  • Теперь давайте посмотрим на инициализацию встроенного массива:

    Inline Array Initializer

    • Первые 2 строки создают массив размером 4.
    • Третья строка дублирует созданный указатель массива на стек оценки.
    • В последней строке для массива указывается только что созданный массив.

Теперь сосредоточимся на двух оставшихся строках:

Первая строка (L_001B) загружает некоторый Compilation-Time-Type с именем типа __StaticArrayInitTypeSize=16 и именем поля 1456763F890A84558F99AFA687C36B9037697848, который находится внутри класса с именем <PrivateImplementationDetails> в Root Namespace. если мы посмотрим на это поле, то увидим, что оно содержит искомый массив полностью так, как мы хотим, чтобы он был закодирован в байтах:

.field assembly static initonly valuetype <PrivateImplementationDetails>/__StaticArrayInitTypeSize=16 1456763F890A84558F99AFA687C36B9037697848 = ((01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00))

Во второй строке вызывается метод, который возвращает инициализированный массив, используя пустой массив, который мы только что создали в L_0060, и используя этот тип времени компиляции.

Если мы попытаемся взглянуть на этот код метода, то увидим, что он реализован в CLR:

[MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical, __DynamicallyInvokable]
public static extern void InitializeArray(Array array, RuntimeFieldHandle fldHandle);

Так что либо нам нужно найти его исходный код в опубликованных источниках CLR, который я не смог найти для этого метода, либо мы можем отладить на уровне сборки. Так как у меня сейчас проблемы с моей Visual-Studio и возникают проблемы с ее сборкой, давайте попробуем по-другому взглянуть и посмотрим на запись памяти для каждой инициализации массива.

Начиная с инициализации цикла, в начале мы можем видеть, что инициализирован пустой TG48 (на рисунке 0x724a3c88, показанном в Little-Endian, тип int[], а 0x00000004 - размер массива, чем мы можем видеть 16 байтов нулей).

Empty Array Memory

Когда массив инициализирован, мы видим, что память заполнена индикаторами того же типа и размера, только в ней есть цифры от 0 до 3:

Initialized Array Memory

Когда цикл повторяется, мы видим, что следующий массив (отмечен красным) выделен сразу после нашего первого массива (не подписан), что также подразумевает, что каждый массив потребляет 16 + type + size + padding = 19 bytes:

New Array

Выполняя тот же процесс на inline-type-initializer, мы видим, что после инициализации массива куча содержит другие типы, также отличные от нашего массива; Вероятно, это происходит из-за метода System.Runtime.CompilerServices.InitializeArray, поскольку указатель массива и токен типа времени компиляции загружаются в стек оценки, а не в кучу (строки L_001B и L_0020 в коде IL):

Inline Array Initialization

Теперь выделение следующего массива с помощью встроенного инициализатора массива показывает нам, что следующий массив выделяется только через 64 байта после начала первого массива!

2 Inline Initialized Arrays

Поэтому inline-array-initializer какминимум медленнее по нескольким причинам:

  • Намного больше памяти выделяется (нежелательная память из CLR).
  • В дополнение к конструктору массива есть накладные расходы на вызов метода.
  • Кроме того, если CLR выделил больше памяти, кроме массива - он, вероятно, выполняет еще несколько ненужных действий.

Теперь о разнице между Debug и Release в инициализаторе встроенного массива:

Если вы проверяете код сборки отладочной версии, это выглядит так:

00952E46 B9 42 5D FF 71       mov         ecx,71FF5D42h  //The pointer to the array.
00952E4B BA 04 00 00 00       mov         edx,4  //The desired size of the array.
00952E50 E8 D7 03 F7 FF       call        008C322C  //Array constructor.
00952E55 89 45 90             mov         dword ptr [ebp-70h],eax  //The result array (here the memory is an empty array but arr cannot be viewed in the debug yet).
00952E58 B9 E4 0E D7 00       mov         ecx,0D70EE4h  //The token of the compilation-time-type.
00952E5D E8 43 EF FE 72       call        73941DA5  //First I thought that the System.Runtime.CompilerServices.InitializeArray method but thats the part where the junk memory is added so i guess it a part of the token loading process for the compilation-time-type.
00952E62 89 45 8C             mov         dword ptr [ebp-74h],eax
00952E65 8D 45 8C             lea         eax,[ebp-74h]  
00952E68 FF 30                push        dword ptr [eax]  
00952E6A 8B 4D 90             mov         ecx,dword ptr [ebp-70h]  
00952E6D E8 81 ED FE 72       call        73941BF3  //System.Runtime.CompilerServices.InitializeArray method.
00952E72 8B 45 90             mov         eax,dword ptr [ebp-70h]  //Here the result array is complete  
00952E75 89 45 B4             mov         dword ptr [ebp-4Ch],eax  

С другой стороны, код для релизной версии выглядит следующим образом:

003A2DEF B9 42 5D FF 71       mov         ecx,71FF5D42h  //The pointer to the array.
003A2DF4 BA 04 00 00 00       mov         edx,4  //The desired size of the array.
003A2DF9 E8 2E 04 F6 FF       call        0030322C  //Array constructor.
003A2DFE 83 C0 08             add         eax,8  
003A2E01 8B F8                mov         edi,eax  
003A2E03 BE 5C 29 8C 00       mov         esi,8C295Ch  
003A2E08 F3 0F 7E 06          movq        xmm0,mmword ptr [esi]  
003A2E0C 66 0F D6 07          movq        mmword ptr [edi],xmm0  
003A2E10 F3 0F 7E 46 08       movq        xmm0,mmword ptr [esi+8]  
003A2E15 66 0F D6 47 08       movq        mmword ptr [edi+8],xmm0

Оптимизация отладки делает невозможным просмотр памяти arr, поскольку локальный уровень IL никогда не устанавливается. Как вы можете видеть, эта версия использует movq, что, в этом отношении, самый быстрый способ скопировать память типа времени компиляции в инициализированный массив, дважды скопировав QWORD (2 int вместе!), Который является точно содержимым нашего массива, который является 16 bit.

Ответ 2

Инициализация статического массива реализуется бит по-разному. Он будет хранить бит в сборке как внедренный класс, который будет называться как <PrivateImplementationDetails>....

То, что он делает, хранит данные массива как биты внутри сборки в определенном месте; который затем будет загружен из сборки и вызовет RuntimeHelpers.InitializeArray для инициализации массива.

Обратите внимание, что если вы используете рефлектор для просмотра скомпилированного источника как C#, вы не заметите ничего, что я описываю здесь. Вам нужно посмотреть представление IL в отражателе или какие-либо такие инструменты для декомпиляции.

[MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical, __DynamicallyInvokable]
public static extern void InitializeArray(Array array, RuntimeFieldHandle fldHandle);

Вы можете видеть, что это реализовано в CLR (помечено как InternalCall), которое затем отображается на COMArrayInfo::InitializeArray (ecall.cpp в sscli).

FCIntrinsic("InitializeArray", COMArrayInfo::InitializeArray, CORINFO_INTRINSIC_InitializeArray)

COMArrayInfo::InitializeArray (живет в comarrayinfo.cpp) - магический метод, который инициализирует массив значением из бит, встроенным в сборку.

Я не уверен, почему для этого требуется много времени; У меня нет хороших объяснений. Думаю, это потому, что он идет и извлекает данные из физической сборки? Я не уверен. Вы можете самостоятельно разобраться в методах. Но вы можете понять, что он не компилируется, как то, что вы видите в своем коде.

Вы можете использовать такие инструменты, как IlDasm и Dumpbin, чтобы найти больше об этом и, конечно же, скачать sscli.

FWIW: У меня есть эта информация из курса Pluralsight by "bart de smet"