В С#, медленнее ли ссылаться на переменную массива?

У меня есть массив целых чисел, и я просматриваю их:

for (int i = 0; i < data.Length; i++)
{
  // do a lot of stuff here using data[i]
}

Если я это сделаю:

for (int i = 0; i < data.Length; i++)
{
  int value = data[i];
  // do a lot of stuff with value instead of data[i]
}

Есть ли какой-либо выигрыш/потеря производительности?

Из моего понимания, к элементам массива C/С++ обращаются напрямую, т.е. массив из n-элементов целых чисел имеет непрерывный блок памяти длины n * sizeof (int) и элемент доступа к программе i, делая что-то вроде * данных [i] = * data [0] + (i * sizeof (int)). (Пожалуйста, извините мое злоупотребление нотацией, но вы понимаете, что я имею в виду.)

Итак, это означает, что C/С++ не должен иметь прирост/потерю производительности для ссылки на переменные массива.

Как насчет С#? С# имеет кучу дополнительных служебных данных, таких как data.Length, data.IsSynchronized, data.GetLowerBound(), data.GetEnumerator().

Очевидно, что массив С# не совпадает с массивом C/С++.

Так какой вердикт? Должен ли я хранить int value = data [i] и работать со значением, или нет влияния на производительность?

Ответы

Ответ 1

Да, существует потеря производительности из-за проверки границ каждого доступа к массиву.

Нет, вам, скорее всего, не нужно беспокоиться об этом.

Да, вы можете сохранить значение и работать со значением. Нет, это связано не с проблемой производительности, а с тем, что делает код более читаемым (IMHO).


Кстати, компилятор JIT может оптимизировать избыточные проверки, поэтому это не значит, что вы действительно получите чек на каждом звонке. В любом случае, вероятно, вам не стоит беспокоиться об этом; просто используйте его, и если он окажется узким местом, вы всегда можете вернуться и использовать блоки unsafe.

Ответ 2

Вы можете получить торт и съесть его тоже. Существует много случаев, когда оптимизатор джиттера может легко определить, что доступ к индексированию массива является безопасным и его не нужно проверять. Любой из циклов, как у вас в вашем вопросе, является одним из таких случаев, джиттер знает диапазон индексной переменной. И знает, что проверка его снова бессмысленна.

Единственный способ, который вы можете видеть, это сгенерированный машинный код. Я приведу аннотированный пример:

    static void Main(string[] args) {
        int[] array = new int[] { 0, 1, 2, 3 };
        for (int ix = 0; ix < array.Length; ++ix) {
            int value = array[ix];
            Console.WriteLine(value);
        }
    }

Starting at the for loop, ebx has the pointer to the array:

            for (int ix = 0; ix < array.Length; ++ix) {
00000037  xor         esi,esi                       ; ix = 0
00000039  cmp         dword ptr [ebx+4],0           ; array.Length < 0 ?
0000003d  jle         0000005A                      ; skip everything
                int value = array[ix];
0000003f  mov         edi,dword ptr [ebx+esi*4+8]   ; NO BOUNDS CHECK !!!
                Console.WriteLine(value);
00000043  call        6DD5BE38                      ; Console.Out
00000048  mov         ecx,eax                       ; arg = Out
0000004a  mov         edx,edi                       ; arg = value
0000004c  mov         eax,dword ptr [ecx]           ; call WriteLine()
0000004e  call        dword ptr [eax+000000BCh] 
            for (int ix = 0; ix < array.Length; ++ix) {
00000054  inc         esi                           ; ++ix
00000055  cmp         dword ptr [ebx+4],esi         ; array.Length > ix ?
00000058  jg          0000003F                      ; loop

Индексирование массива происходит по адресу 00003f, ebx имеет указатель массива, esi - индекс, 8 - смещение элементов массива в объекте. Обратите внимание, что значение esi снова не проверяется на границах массива. Это выполняется так же быстро, как и код, сгенерированный компилятором C.

Ответ 3

Вы написали это в обоих направлениях. Запустите его в обоих направлениях, измерьте его. Тогда вы узнаете.

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

Ответ 4

Компилятор может выполнять только общую оптимизацию подвыражения, если он может доказать, что к массиву не обращаются другие потоки или любые методы (включая делегаты), вызываемые внутри цикла, возможно, лучше создать локальную копию самостоятельно.

Но читаемость должна быть вашей главной проблемой, если только этот цикл не будет выполняться огромное количество раз.

Все это также верно в C и С++. Индексирование в массив будет медленнее, чем доступ к локальной переменной.

В качестве побочного примечания ваша предлагаемая оптимизация не подходит: value - это ключевое слово, выберите другое имя переменной.

Ответ 5

Не совсем уверен, но, вероятно, не помешает сохранить значение, если вы собираетесь использовать его несколько раз. Вы также можете использовать инструкцию foreach:)