Могут ли переменные, объявленные внутри цикла for, влиять на производительность цикла?

Я сделал домашнее задание и нашел неоднократные заверения в том, что не имеет никакого значения в производительности независимо от того, объявляете ли вы свои переменные внутри или вне цикла for и фактически компилируете тот же MSIL. Но я все-таки возился с ним и обнаружил, что перемещение деклараций переменных внутри цикла действительно приводит к значительному и стабильному увеличению производительности.

Я написал небольшой тестовый класс консоли для измерения этого эффекта. Я инициализирую статические элементы массива double[], а два метода выполняют на нем операции цикла, записывая результаты в статический буфер массива double[]. Первоначально моими методами были те, с которыми я заметил разницу, а именно вычисление величины комплексного числа. Запустив их для массива элементов длиной 1000000 в 100 раз, я получил последовательно меньшее время выполнения для того, в котором переменные (6 double variables) находились внутри цикла: например, 32,83 ± 0,64 мс против 43, 24 ± 0,45 мс на пожилой конфигурации с Intel Core 2 Duo @2,66 ГГц. Я попытался выполнить их в другом порядке, но это не повлияло на результаты.

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

    static void Square1()
    {
        double x;

        for (int i = 0; i < buffer.Length; i++) {
            x = items[i];
            buffer[i] = x * x;
        }
    }


    static void Square2()
    {
        for (int i = 0; i < buffer.Length; i++) {
            double x;
            x = items[i];
            buffer[i] = x * x;
        }
    }

При этом результаты получились иначе: объявление переменной вне цикла показалось более благоприятным: 7.07 ± 0.43 мс для Square1() v 12.07 ± 0.51 мс для Square2().

Я не знаком с ILDASM, но я разобрал эти два метода, и единственная разница, по-видимому, является инициализацией локальных переменных:

      .locals init ([0] float64 x,
       [1] int32 i,
       [2] bool CS$4$0000)

in Square1() v

      .locals init ([0] int32 i,
       [1] float64 x,
       [2] bool CS$4$0000)

в Square2(). В соответствии с этим, то, что stloc.1 в одном, это stloc.0 в другом, и наоборот. В более длинном сложном вычислении величины MSIL кодирует даже размер кода, и я видел stloc.s i в коде внешней декларации, где во внутреннем коде декларации был stloc.0.

Итак, как это может быть? Я что-то забываю или это реальный эффект? Если это так, это может существенно повлиять на производительность длинных циклов, поэтому я думаю, что это заслуживает обсуждения.

Ваши мысли очень ценятся.

EDIT: Единственное, что я забыл, это проверить его на нескольких компьютерах перед публикацией. Я запустил его на i5 сейчас, а результаты почти идентичны для двух методов. Извиняюсь за то, что вы опубликовали такое вводящее в заблуждение наблюдение.

Ответ 1

Любой компилятор С#, достойный его соли, будет выполнять такие микро-оптимизации для вас. Если это необходимо, протекайте переменную за пределами области действия.

Итак, держите double x; внутри цикла, если это возможно.

Лично, если items[i] - это доступ к массиву с открытым массивом, тогда я бы написал buffer[i] = items[i] * items[i];. C и С++ оптимизировали бы это, но я не думаю, что С# (пока); ваша разборка подразумевает, что это не так.

Ответ 2

Было бы интересно рассказать, что делает сборщик мусора для этих двух вариантов.

Я могу представить, что в первом случае переменная x не собирается во время цикла, потому что она объявлена ​​во внешней области.

Во втором случае все дескрипторы на x будут удалены на каждой итерации.

Возможно, вы снова запустите свой тест с новыми С# 4.6 GC.TryStartNoGCRegion и GC.EndNoGCRegion, чтобы увидеть, влияет ли влияние производительности на GC.

Предотвратите сборку мусора .NET за короткий период времени