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

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

Я написал небольшой тестовый класс для консоли, чтобы измерить этот эффект. Я инициализирую статический double[] массив items, и два метода выполняют циклические операции с ним, записывая результаты в статический double[] массив буфер. Первоначально мои методы были теми, с которыми я заметил разницу, а именно величину вычисления комплексного числа. Запустив их для массива items длиной 1000000 100 раз, я получил постоянно меньшее время выполнения для того, в котором переменные (6 переменных double) находились внутри цикла: например, 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 во внутренней коде декларации.

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

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

РЕДАКТИРОВАТЬ: Единственное, что я упустил из виду, это проверить его на нескольких компьютерах перед публикацией. Я запустил его на i5, и результаты для двух методов почти идентичны. Приношу свои извинения за столь вводящее в заблуждение наблюдение.


person tethered.sun    schedule 09.02.2017    source источник
comment
Хорошее расследование, вы обязательно заработаете плюс.   -  person NicoRiff    schedule 09.02.2017
comment
@NicoRiff: Действительно, это очень хорошо написанный вопрос. (К сожалению, я думаю, что ответ тривиален.)   -  person Bathsheba    schedule 09.02.2017
comment
Я не могу дождаться ответа @JonSkeet на этот вопрос   -  person NicoRiff    schedule 09.02.2017
comment
Я не могу воспроизвести это поведение с помощью данного кода. Сгенерированный IL определенно переворачивает порядок объявления локальных переменных, но я не вижу существенной разницы в производительности.   -  person Kyle    schedule 09.02.2017
comment
@Kyle: я запустил дома тот же код на другом компьютере, и здесь разница исчезла. Мои извинения. Меня до сих пор интригует, является ли это чем-то систематическим или просто артефактом для одного компьютера. Я проведу больше тестов и обновлю пост завтра.   -  person tethered.sun    schedule 09.02.2017
comment
Можете ли вы показать код, который вы использовали для измерения производительности? Учитывали ли вы компиляцию JIT при первом запуске кода?   -  person Chris Dunaway    schedule 09.02.2017
comment
@Chris Dunaway: я поделился полным кодом здесь: drive.google.com/open?id= 0B3OSs_9bqexqY2ZvSUE1WmJjTDQ Поскольку я выполнил 100 циклов подряд и взял средние значения и значения SD, я должен предположить, что время JIT-компиляции здесь не играет роли.   -  person tethered.sun    schedule 09.02.2017


Ответы (2)


Любой достойный компилятор C# выполнит такую ​​микрооптимизацию за вас. Пропускайте переменную за пределы области видимости только в том случае, если это необходимо.

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

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

person Bathsheba    schedule 09.02.2017
comment
Большое тебе спасибо! Раньше у меня была навязчивая привычка объявлять все мои переменные в начале метода, но теперь я буду думать дважды. Мой главный посыл — протестировать обе схемы, если меня волнует производительность, потому что кажется, что оптимизация может работать в обоих направлениях. - person tethered.sun; 09.02.2017
comment
Сложный ответ: многолетний опыт подсказывает вам, что переменные со слабой областью видимости приводят к полному беспорядку в кодовой базе. - person Bathsheba; 09.02.2017
comment
Ваш ответ, кажется, говорит, что не должно быть разницы в производительности между сохранением переменной внутри цикла и снаружи, но это на самом деле не объясняет измеренные различия, которые испытал OP. - person HugoRune; 09.02.2017
comment
В шутку я виню в этом исключительно концепцию эластичной линейки. - person Bathsheba; 09.02.2017
comment
Компилятор C# иногда удаляет локальные переменные. Я не уверен, при каких условиях ему это удается, но я видел, как он делал это раньше. - person Kyle; 10.02.2017

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

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

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

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

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

person Georg Patscheider    schedule 09.02.2017
comment
Спасибо, это отличная идея. Я хотел уже протестировать его, но на данный момент у меня нет доступа к .NET 4.6. SharpDevelop, похоже, не поддерживает его. Я постараюсь обновить свои инструменты и вернуться к вопросу. - person tethered.sun; 09.02.2017
comment
Сомневаюсь, что это как-то связано с ГК. double — это тип значения, и в этом случае он будет выделен в стеке. Он не генерирует мусор для очистки. - person Kyle; 09.02.2017
comment
Эрик Липперт дает отличную информацию об этом на stackoverflow.com/a/14043763/526724. - person Bradley Uffner; 09.02.2017